13.2 线程安全
“线程安全”这个名称,相信稍有经验的程序员都会听说过,甚至在代码编写和走查的时候可能还会经常挂在嘴边,但是如何找到一个不太拗口的概念来定义线程安全却不是一件容易的事情,笔者尝试在Google中搜索它的概念,找到的是类似于“如果一个对象可以安全地被多个线程同时使用,那它就是线程安全的”这样的定义——并不能说它不正确,但是人们无法从中获取到任何有用的信息。
笔者认为《Java Concurrency In Practice》的作者Brian Goetz对“线程安全”有一个比较恰当的定义:“当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象是线程安全的”。
这个定义比较严谨,它要求线程安全的代码都必须具备一个特征:代码本身封装了所有必要的正确性保障手段(如互斥同步等),令调用者无须关心多线程的问题,更无须自己采取任何措施来保证多线程的正确调用。这点听起来简单,但其实并不容易做到,在大多数场景中,我们都会将这个定义弱化一些,如果把“调用这个对象的行为”限定为“单次调用”,这个定义的其他描述也能够成立的话,我们就可以称它是线程安全了,为什么要弱化这个定义,现在暂且放下,稍后再详细探讨。
13.2.1 Java语言中的线程安全
我们已经有了线程安全的一个抽象定义,那接下来就讨论一下在Java语言中,线程安全具体是如何体现的?有哪些操作是线程安全的?我们这里讨论的线程安全,就限定于多个线程之间存在共享数据访问这个前提,因为如果一段代码根本不会与其他线程共享数据,那么从线程安全的角度来看,程序是串行执行还是多线程执行对它来说是完全没有区别的。
为了更加深入地理解线程安全,在这里我们可以不把线程安全当做一个非真即假的二元排他选项来看待,按照线程安全的“安全程度”由强至弱来排序,我们[1]可以将Java语言中各种操作共享的数据分为以下5类:不可变、绝对线程安全、相对线程安全、线程兼容和线程对立。
1.不可变
在Java语言中(特指JDK 1.5以后,即Java内存模型被修正之后的Java语言),不可变(Immutable)的对象一定是线程安全的,无论是对象的方法实现还是方法的调用者,都不需要再采取任何的线程安全保障措施,在第12章我们谈到final关键字带来的可见性时曾经提到过这一点,只要一个不可变的对象被正确地构建出来(没有发生this引用逃逸的情况),那其外部的可见状态永远也不会改变,永远也不会看到它在多个线程之中处于不一致的状态。“不可变”带来的安全性是最简单和最纯粹的。
Java语言中,如果共享数据是一个基本数据类型,那么只要在定义时使用final关键字修饰它就可以保证它是不可变的。如果共享数据是一个对象,那就需要保证对象的行为不会对其状态产生任何影响才行,如果读者还没想明白这句话,不妨想一想java.lang.String类的对象,它是一个典型的不可变对象,我们调用它的substring()、replace()和concat()这些方法都不会影响它原来的值,只会返回一个新构造的字符串对象。
保证对象行为不影响自己状态的途径有很多种,其中最简单的就是把对象中带有状态的变量都声明为final,这样在构造函数结束之后,它就是不可变的,例如代码清单13-1中java.lang.Integer构造函数所示的,它通过将内部状态变量value定义为final来保障状态不变。
代码清单13-1 JDK中Integer类的构造函数
/**
*The value of the<code>Integer</code>.
*@serial
*/
private final int value;
/**
*Constructs a newly allocated<code>Integer</code>object that
*represents the specified<code>int</code>value.
*
*@param value the value to be represented by the
*<code>Integer</code>object.
*/
public Integer(int value){
this.value=value;
}
在Java API中符合不可变要求的类型,除了上面提到的String之外,常用的还有枚举类型,以及java.lang.Number的部分子类,如Long和Double等数值包装类型,BigInteger和BigDecimal等大数据类型;但同为Number的子类型的原子类AtomicInteger和AtomicLong则并非不可变的,读者不妨看看这两个原子类的源码,想一想为什么。
2.绝对线程安全
绝对的线程安全完全满足Brian Goetz给出的线程安全的定义,这个定义其实是很严格的,一个类要达到“不管运行时环境如何,调用者都不需要任何额外的同步措施”通常需要付出很大的,甚至有时候是不切实际的代价。在Java API中标注自己是线程安全的类,大多数都不是绝对的线程安全。我们可以通过Java API中一个不是“绝对线程安全”的线程安全类来看看这里的“绝对”是什么意思。
如果说java.util.Vector是一个线程安全的容器,相信所有的Java程序员对此都不会有异议,因为它的add()、get()和size()这类方法都是被synchronized修饰的,尽管这样效率很低,但确实是安全的。但是,即使它所有的方法都被修饰成同步,也不意味着调用它的时候永远都不再需要同步手段了,请看一下代码清单13-2中的测试代码。
代码清单13-2 对Vector线程安全的测试
private static Vector<Integer>vector=new Vector<Integer>();
public static void main(String[]args){
while(true){
for(int i=0;i<10;i++){
vector.add(i);
}
Thread removeThread=new Thread(new Runnable(){
@Override
public void run(){
for(int i=0;i<vector.size();i++){
vector.remove(i);
}
}
});
Thread printThread=new Thread(new Runnable(){
@Override
public void run(){
for(int i=0;i<vector.size();i++){
System.out.println((vector.get(i)));
}
}
});
removeThread.start();
printThread.start();
//不要同时产生过多的线程,否则会导致操作系统假死
while(Thread.activeCount()>20);
}
}
运行结果如下:
Exception in thread"Thread-132"java.lang.ArrayIndexOutOfBoundsException:
Array index out of range:17
at java.util.Vector.remove(Vector.java:777)
at org.fenixsoft.mulithread.VectorTest$1.run(VectorTest.java:21)
at java.lang.Thread.run(Thread.java:662)
很明显,尽管这里使用到的Vector的get()、remove()和size()方法都是同步的,但是在多线程的环境中,如果不在方法调用端做额外的同步措施的话,使用这段代码仍然是不安全的,因为如果另一个线程恰好在错误的时间里删除了一个元素,导致序号i已经不再可用的话,再用i访问数组就会抛出一个ArrayIndexOutOfBoundsException。如果要保证这段代码能正确执行下去,我们不得不把removeThread和printThread的定义改成如代码清单13-3所示的样子。
代码清单13-3 必须加入同步以保证Vector访问的线程安全性
Thread removeThread=new Thread(new Runnable(){
@Override
public void run(){
synchronized(vector){
for(int i=0;i<vector.size();i++){
vector.remove(i);
}
}
}
});
Thread printThread=new Thread(new Runnable(){
@Override
public void run(){
synchronized(vector){
for(int i=0;i<vector.size();i++){
System.out.println((vector.get(i)));
}
}
}
});
3.相对线程安全
相对的线程安全就是我们通常意义上所讲的线程安全,它需要保证对这个对象单独的操作是线程安全的,我们在调用的时候不需要做额外的保障措施,但是对于一些特定顺序的连续调用,就可能需要在调用端使用额外的同步手段来保证调用的正确性。上面代码清单13-2和代码清单13-3就是相对线程安全的明显的案例。
在Java语言中,大部分的线程安全类都属于这种类型,例如Vector、HashTable、Collections的synchronizedCollection()方法包装的集合等。
4.线程兼容
线程兼容是指对象本身并不是线程安全的,但是可以通过在调用端正确地使用同步手段来保证对象在并发环境中可以安全地使用,我们平常说一个类不是线程安全的,绝大多数时候指的是这一种情况。Java API中大部分的类都是属于线程兼容的,如与前面的Vector和HashTable相对应的集合类ArrayList和HashMap等。
5.线程对立
线程对立是指无论调用端是否采取了同步措施,都无法在多线程环境中并发使用的代码。由于Java语言天生就具备多线程特性,线程对立这种排斥多线程的代码是很少出现的,而且通常都是有害的,应当尽量避免。
一个线程对立的例子是Thread类的suspend()和resume()方法,如果有两个线程同时持有一个线程对象,一个尝试去中断线程,另一个尝试去恢复线程,如果并发进行的话,无论调用时是否进行了同步,目标线程都是存在死锁风险的,如果suspend()中断的线程就是即将要执行resume()的那个线程,那就肯定要产生死锁了。也正是由于这个原因,suspend()和resume()方法已经被JDK声明废弃(@Deprecated)了。常见的线程对立的操作还有System.setIn()、Sytem.setOut()和System.runFinalizersOnExit()等。
[1]这种划分方法也是Brian Goetz在IBM developWorkers上发表的一篇论文中提出的,这里写“我们”纯粹是笔者下笔行文中的语言用法。