线程安全
“线程安全”的定义:
当多个线程同时访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那就称这个对象时线程安全的。
该定义要求线程安全的代码都必须具备一个共同特征:
代码本身封装了所有必要的正确性保障手段(如互斥同步等),令调用者无须关心多线程下的调用问题,更无须自己实现任何措施来保证多线程环境下的正确调用。
java语言中的线程安全
线程安全,将以多个线程之间存在共享数据访问为前提。为了更深入地理解线程安全,我们不把线程安全当作一个非真即假的二元排他选项来看待,而是按照线程安全的“安全程度”由强至弱来排序,可以将java语言中各种操作共享的数据分为以下五类:
- 不可变
- 绝对线程安全
- 相对线程安全
- 线程兼容
- 线程对立
不可变
在java语言里(JDK5之后),不可变(Immutable)的对象一定是线程安全的,无论是对象的方法实现还是方法的调用者,都不需要再进行任何线程安全保障措施。
“final关键字带来的可见性”提到过:只要一个不可变的对象被正确的构建出来(即没有发生this引用逃逸的情况),那其外部的可见状态永远都不会改变,永远都不会看到它在多个线程之中处于不一致的状态。“不可变”带来的安全性是最直接,最纯粹的。
java语言中,如果多线程共享数据分为两类:
- 基本数据类型,只要在定义时使用final关键字修饰它就可以保证它是不可变的。
- 对象类型,需要对象自行保证其行为不会对其状态产生任何影响。
- 可以类比java.lang.String类的对象实例,它是一个典型的不可变对象,用户调用它的substring()、replace()和concat()这些方法都不会影响它原来的值,只会返回一个新构造的字符串对象。
保证对象行为不影响自己状态的途径有很多种,最简单的一种就是把对象里面带有状态的变量都声明为final,这样构造函数结束之后,它就是不可变的,如下java.lang.Integer构造函数,通过将内部状态变量value定义为final来保障状态不变。
/**
* The value of the {@code Integer}.
*
* @serial
*/
private final int value;
/**
* Constructs a newly allocated {@code Integer} object that
* represents the specified {@code int} value.
*
* @param value the value to be represented by the
* {@code Integer} object.
*/
public Integer(int value) {
this.value = value;
}
java类库API中符合不可变要求的类型:
- java.lang.String。
- 枚举类型。
- java.lang.Number的部分子类。
- Long和Double等数值包装类型。
- BigInteger和BigDecimal等大数据类型。
例外:同为Number子类型的原子类AtomicInteger和AtomicLong则是可变的。为啥这样设计?
绝对线程安全
绝对线程安全的定义是很严格的,一个类要达到“不管运行时环境如何,调用者都不需要任何额外的同步措施”可能需要付出非常高昂的,甚至不切实际的代价。在java API中标注自己是线程安全的类,大多数都不是绝对的线程安全。我们可以通过java API中一个不是“绝对线程安全”的“线程安全类型”来看看这个语境里的“绝对”究竟是什么意思。
Java.util.Vector是一个线程安全的容器,因为它的add()、get()和size()等方法都是被synchronized修饰的,尽管这样效率不高,但保证了具备原子性、可见性和有序性。不过,即使它所有的方法都被修饰成synchronized,也不意味着调用它的时候就永远都不再需要同步手段了。
package com.example.xuniji;
import java.util.Vector;
/**
* @ClassName VectorTest
* @Author jia_xx
* @Date 2022/5/27 21:22
*/
public class VectorTest {
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-1007" Exception in thread "Thread-1008" java.lang.ArrayIndexOutOfBoundsException: Array index out of range: 9
at java.util.Vector.get(Vector.java:751)
at com.example.xuniji.VectorTest$2.run(VectorTest.java:32)
at java.lang.Thread.run(Thread.java:748)
java.lang.ArrayIndexOutOfBoundsException: Array index out of range: 7
at java.util.Vector.remove(Vector.java:834)
at com.example.xuniji.VectorTest$1.run(VectorTest.java:23)
at java.lang.Thread.run(Thread.java:748)
尽管这里使用到的Vector的get()、remove()和size()方法都是同步的,但是在多线程的环境中,如果不在方法调用端做额外的同步措施,使用这段代码仍然是不安全的。因为如果另一个线程恰好在错误的时间里删除了一个元素,导致序号i已经不再可用,再用i访问数组就会抛出一个ArrayIndexOutOfBoundsException异常。
如果要保证这段代码能正确执行下去,我们不得不把removeThread和printThread定义成如下所示:
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));
}
}
}
});
假如Vector一定要做到绝对的线程安全,那就必须在它内部维护一组一致性的快照访问才行,每次对其中元素进行改动都要产生新的快照,这样要付出的时间和空间成本都是非常大的。
相对线程安全
相对线程安全就是我们通常意义上所讲的线程安全,它需要保证对这个对象单次的操作是线程安全的,我们在调用的时候不需要进行额外的保障措施,但是对于一些特定顺序的连续调用,就可能需要在调用端使用额外的同步手段来保证调用的正确性。以上代码(Vector)就是相对线程安全的案例。
在java语言中,大部分声称线程安全的类都属于这种类型,例如Vector、HashTable、Collections的synchronizedCollection()方法包装的集合等。
线程兼容
线程兼容是指对象本身并不是线程安全的,但是可以通过在调用端正确地使用同步手段来保证对象在并发环境中可以安全地使用。平常我们说一个类不是线程安全的,通常就是指这种情况。Java类库API中大部分的类都是线程兼容的,比如ArrayList和HashMap。
线程对立
线程对立是指不管调用端是否采取了同步措施,都无法在多线程环境中并发使用代码。java语言天生就支持多线程的特性,线程对立这种排斥多线程的代码是很少出现的,通常都是有害的,应当尽量避免。
一个线程对立的例子是Thread类的suspend()和resume()方法。如果有两个线程同时持有一个线程对象,一个尝试去中断线程,一个尝试去恢复线程,在并发进行的情况下,无论调用时是否进行了同步,目标线程都存在死锁风险。