线程之间的“竞争条件”:作用于同一个mutable数据上的多个线程,彼此之间存在对该数据的访问竞争并导致interleaving,导致post-condition可能被违反,这是不安全的。
线程安全:ADT或方法在多线程中要执行正确。
一、Confinement 限制数据共享
核心思想:线程之间不共享mutable数据类型
避免使用全局变量
二、Immutability 共享不可变数据
使用不可变数据类型和不可变引用,避免多线程之间的race condition。
对于并发编程,这种隐藏的变化有时是不安全的,如果ADT中使用了beneficent mutation,必须要通过“加锁”机制来保证线程安全。
以下是对不变性的更强有力的定义:
– No mutator methods
– All fields are private and final
– No representation exposure
– No mutation whatsoever of mutable objects in the rep – not
even beneficent mutation
相比起策略1 Confinement,该策略2 Immutability允许有全局rep,但是只能是immutable的。因此自然会产生第三种方法。
三、Threadsafe data type 共享线程安全的可变数据
如果必须要用mutable的数据类型在多线程之间共享数据,要使用线程安全的数据类型。在JDK中的类,文档中明确指明了是否threadsafe。一般来说,JDK同时提供两个相同功能的类,一个是threadsafe,另一个不是。
原因:threadsafe的类一般性能上受影响
举一个很简单的例子:
在平时的Java编程中,我们经常使用List、Set以及Map等容器类。实际上,它们都是线程不安全的。
因此,Java提供了线程安全的包裹类型:
public static <T> Collection<T> synchronizedCollection(Collection<T> c);
public static <T> Set<T> synchronizedSet(Set<T> s);
public static <T> List<T> synchronizedList(List<T> list);
public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m);
public static <T> SortedSet<T> synchronizedSortedSet(SortedSet<T> s);
public static <K,V> SortedMap<K,V> synchronizedSortedMap(SortedMap<K,V> m);
同时,即使在线程安全的集合类上,使用iterator也是不安全的, 除非使用lock机制,因此会产生第四种方法。
四、Synchronization 同步机制:通过锁的机制共享线程不安全的可变数据,变并行为串行
使用锁机制,获得对数据的独家mutation权,其他线程被阻塞,不得访问。
Lock是Java语言提供的内嵌机制,每个object都有相关联的lock。
Object lock = new Object();
synchronized (lock) { // thread blocks here until lock is free
// now this thread has the lock
balance = balance + 1;
// exiting the block releases the lock
}
Java提供两种基本的同步习语:
-同步语句/同步代码块
-同步方法
拥有lock的线程可独占式的执行该部分代码,进而Lock可以用来保护共享数据。注意:要互斥,必须使用同一个lock进行保护。
锁只能确保与其他请求获取相同对象锁的线程互斥访问,如果其他线程没有使用synchronized(obj)或者利用了不同的锁,则同步会失效,需要仔细检查和设计同步块和同步方法。
Example1:
Example2:
同步方法
当线程调用同步方法时,它会自动获取该方法所在对象的内部锁,并在方法返回时释放它。即使返回是由未捕获的异常引起的,也会释放锁。同一对象上的同步方法的两次调用不会有交叉现象。
当一个线程在执行一个对象的同步方法时,所有其他线程如果调用同一对象的同步方法块,则会挂起执行,直到第一个线程针对此对象的操作完成。
当一个同步方法退出时,它会自动建立一个与之后调用同
一个对象的同步方法的happens-before关系,这保证对象状态的更改对所有线程都是可见的。
前一个事件的结果可以被后续的事件获取(即使出于优化的目的,实际运行中并不是按照指定顺序执行),在Java中,采用happened-before机制,保证了语句A对内存的写入对语句B是可见的,也就是在B开始读数据之前,A已经完成了数据的写入。
由于静态方法与类关联,而不是对象,此时线程获取与该类关联的Class对象的内部锁,对类的静态字段的访问由与该类的任何实例的锁截然不同的锁来控制。