声明:本章内容摘自《深入理解Java虚拟机》第二版,有需要深入学习的小伙伴请自行购买书籍。
为了更加深入地理解线程安全,在这里我们可以不把线程安全当做一个非真即假的二元排他选项看待,按照线程安全的“安全程度”由强至弱来排序,我们可以将Java语言中各种操作共享的数据分为以下5类:不可变,绝对线程安全,相对线程安全,线程兼容和线程对立。
1.不可变
在Java语言中(特指JDk1.5以后,即Java内存模型被修正之后的Java语言),不可变(Immutable)的对象一定是线程安全的,无论是对象的方法实现还是方法的调用者,都不需要再采用任何的线程安全保障措施,在只要一个不可变的对象被正确地构建出来,那其外部的可见状态永远也不会改变,永远也不会看到它在多个线程之中处于不一致的状态。“不可变”带来的安全性是最简单和最纯粹的。
Java语言中,如果共享数据是一个基本数据类型,那么只要在定义时使用final关键字修饰它就可以保证它是不可变的,如果共享数据是一个对象,那就需要保证对象的行为不会对其状态产生任何影响才行,如java.lang.String类的对象,它是一个典型的不可变对象,我们调用它的substring(),replace()和concat()这些方法都不会影响它原来的值,只会返回一个新构造的字符串对象。
保证对象行为不影响自己状态的途径有很多种,其中最简单的就是把对象中带有状态的变量都声明为final,这样在构造函数结束之后,它就是不可变的,java.lang.Integer构造函数所示的,它通过将内部状态变量value定义为final来保障状态不可变。
private final int value;
public Integer(int value){
this.value=value;
}
在JavaApi中符合不可变要求的类型,除了上面提到的String之外,常用的还有枚举类型,以及java.lang.Number的部分子类,如Long和Double等数值包装类型,BigInteger和BigDecimal等大数据类型;但同为Number的子类型的原子类AtomicInteger和AtomicLong则并非不可变的。
2.绝对线程安全
这个定义其实是很严格的,一个类要达到不管运行时环境如何,调用者都不需要任何额外的同步措施,通常需要付出很大的,甚至有时候是不切实际的代码,在JavaApi中标注自己是线程安全的类,大多数都不是绝对的线程安全。
java.util.vactor是一个线程安全的容器,因为它的add(),get(),size()这类方法都被synchronized修饰的,尽管效率很低,但确实是安全的,但是即使它所有的方法都被修饰成同步,也不意味着调用它的时候永远不再需要同步手段了。
示例:在一个静态Vector容器中添加10条元素,开启两条线程一条执行vector.remove(i)的for循环,另一条执行vector.get(i)的for循环。
很明显,尽管这里使用到了Vector的get(),remove()和size()方法都是同步的,但是在多线程的环境中,如果不在方法调用端做额外的同步措施的话,使用这段代码仍然是不安全的,因为如果另一个线程恰好在错误的时间删除了一个元素,导致序号i已经不再可用的话,再用i访问数组就会抛出ArrayIndexOutOfBoundsException,若要保证线程安全应在两个for循环外加上synchronized锁。
3.相对线程安全
相对的线程安全就是我们通常意义上所讲的线程安全,它需要保证对这个对象单独的操作是线程安全的,我们在调用的时候不需要做额外的保障措施,但是对于一些特定顺序的连续调用,就可能需要在调用端使用额外的同步手段来保证调用的正确性。
在Java语言中,大部分的线程安全类都属于这种类型,例如Vector,HashTable,Collections的synchronizedCollection()方法包装的集合等。
4.线程兼容
线程兼容是指对象本身并不是线程安全的,但是可以通过在调用端正确地使用同步手段来保证对象在并发环境中可以安全地使用,我们平常说一个类不是线程安全的,绝大多数时候指的是这一种情况。JavaApi中大部分的类都是属于线程兼容的,如集合类 ArrayList和HashMap等。
5.线程对立
线程对立是指无论调用端是否采取了同步措施,都无法在多线程环境中并发使用的代码,由于Java语言天生就具备多线程特性,线程对立这种排斥多线程的代码是很少出现的,而且通常都是有害的,应当尽量避免。
一个线程对立的例子是Thread类的suspend()方法和resume()方法,如果有两个线程同时持有一个线程对象,一个尝试去中断线程,另一个尝试去恢复线程,如果并发进行的话,无论调用时是否进行了同步,目标线程都是存在死锁风险的。如果suspend()中断的线程就是即将要执行resume()的那个线程,那就肯定要产生死锁了。