线程安全的级别与实现
在之前的理解中,以为一个类要么是线程安全的,要么是不安全的,但其实这里面还有细分和区别。此篇讨论一下多线程的不同安全级别,以及操作系统和程序是如何来实现线程安全的。
安全级别
在《深入JVM》一书中,按照线程安全的“安全程度”由强到弱可以分为以下5个级别:
不可变
在Java中,不可变的对象一定是线程安全的,在使用的时候不需要任何额外的手段来保证线程安全。如果是基本数据类型,那么在定义的时候加上final关键字就可以保证它是不可变的,如果是对象,那么要保证对象的行为不会对自身的状态产生影响。例如String就是一个典型的不可变对象,它所有的操作方法都是返回一个新的对象而不改变原来的值。因为String是不可变的,所以在常量池里的字符串对象可以被所有的线程共享而不存在安全性的问题。绝对线程安全
“绝对”的线程安全其实是一种很严格的定义,我们平时常说的线程安全大多都不是绝对的线程安全。如果一个类要达到绝对线程安全,那么就是说不管在什么情况下,都不需要额外的手段来保障线程安全。来看一个例子,一个线程安全的类在多线程下的操作依然产生异常:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38public class VectorTest {
/**
* Vector不是绝对的线程安全
*/
private static Vector<Integer> vector=new Vector<>();
public static void main(String[] args) {
while(true){
for (int i = 0; i < 10; i++) {
vector.add(i);
}
Thread removeThread=new Thread(){
@Override
public void run() {
for (int i = 0; i < vector.size(); i++) {
vector.remove(i);
}
}
};
Thread printThread=new Thread(){
@Override
public void run() {
for (int i = 0; i < vector.size(); i++) {
vector.get(i);
}
}
};
removeThread.start();
printThread.start();
while(Thread.activeCount()>20);
}
}
}例子会抛出以下异常:
1
2
3Exception in thread "Thread-1497" java.lang.ArrayIndexOutOfBoundsException: Array index out of range: 26
at java.util.Vector.get(Vector.java:748)
at com.kevll.temp.VectorTest$2.run(VectorTest.java:36)我们都知道Vector的add(),remove(),get()方法都是被synchronize修饰的,但是这样并不能保证在任何情况下调用它们都是线程安全的。确实如果单独调用remove()或是单独get()是不会报错的,但是同时使用的时候我们必须手动在程序中为这两个方法加上互斥,这样才能保证线程安全。此时,因为有了额外手段地加入,所以它并不是绝对地线程安全了。
相对线程安全
相对线程安全就是我们通常说的线程安全,它保证了对象地单独操作是安全的,Java中声明线程安全的类都是相对线程安全的,例如Vector和HashTable等。线程兼容
线程兼容指的是对象本身不是线程安全的,但是可以通过额外的手段达到线程安全,Java中的大多数类都属于这种。线程对立
线程对立是无论采用什么措施都不能在多线程下保证安全使用的代码。
线程的实现
实现“线程”主要有3种方式:
使用内核线程实现
内核线程是直接由操作系统支持和进行调度和切换的线程,但是程序一般使用的是内核线程的另一种接口:轻量级进程(Light Weight Process, LWP),就是我们通常说的线程,系统只有支持了内核线程才有轻量级进程。但是轻量级进程还是基于内核线程实现的,每次线程的操作都会消耗一定的内核资源,系统级的调用代价相对较高,需要在用户态和内核态之间来回切换。而且一个系统支持的轻量级进程的数量也是有限的。使用用户线程实现
用户线程则是完全建立在用户空间的线程库上,线程的操作都在用户态中完成,不需要去调用内核。这种线程操作速度快、消耗低,所以支持更大规模的线程数量。但是此时线程的调度和切换的问题都要有用户程序来自己控制,复杂度较大,在处理多核间线程和阻塞的问题上就比较难以实现了。现在仅仅使用用户线程的程序越来越少,Java、Ruby等语言曾经使用过,后来抛弃了。使用用户线程加轻量级进程混合实现
这种方式下,用户线程和轻量级进程同时存在,有了用户线程高效廉价和对大规模线程数量的支持,又可以通过轻量级进程与系统内核进行交互,是大多数程序采用的线程实现方式。
Java线程的实现:
在jdk1.2之前,Java线程基于用户线程实现,之后采用基于操作系统原生线程模型来实现。
线程安全的实现
实现线程安全主要有3种方式,其下有又有多种实现方式:
互斥同步
保证共享数据在同一时刻只被一个线程使用。在Java中,提供了synchronized关键字可以实现互斥同步,它其实是monitorenter和monitorexit字节码指令对使用者开放的接口。synchronized是一种重量级的操作,前面说过Java线程是基于操作系统的原生线程,这是对线程的阻塞或唤醒操作都要在用户态和核心态之间切换。在jdk1.5的并发包之后,还可以使用ReentrantLock来实现同步,它是API层的互斥锁,而synchronized为原生语法层面的互斥锁。非阻塞同步
上面的互斥同步也可称为阻塞同步,不管会不会发生冲突,直接对方法加锁,挂起线程以避免冲突。而非阻塞同步是不挂起线程,直接进行操作,遇到冲突再解决。这种同步需要保证操作和冲突检测这两个步骤的原子性,这有依赖于“硬件指令集的发展”。无同步方案
我们要保证线程安全,才采用了同步这种手段,而如果一个方法不涉及数据共享或是代码本身就是线程安全的,则不需要采用同步,例如可重入代码和线程本地存储。
可重入代码:
这种代码有些相同的特性,它们不依赖存储在堆上的数据或是公用的系统资源,不调用非可重入的方法。也可以通过以下原则来判定:如果一个方法的结果可以预测,就是说每个给定参数都能返回相同的结果,那么它就是可重入的,是线程安全的。线程本地存储:
如果共享数据的代码可以在同一线程内执行完的话,就可以将共享数据的可见范围控制在同一线程内,在Java中可以使用ThreadLocal实现线程本地存储的功能。