Java-线程安全
1 什么是线程安全
线程安全是针对某个对象来说,如果当多线程访问此对象时,不用考虑这些线程在运行时环境下的调度和交替执行,也不用再用额外方式如同步锁等、不用调用方进行任何其他协调操作,总能运行获得正确结果,那就可以说这个对象代码线程安全。
也就是说,被调用的线程安全代码已经封装了必要的线程安全保证手段(如互斥同步等),调用者无需关心多线程调用问题、无需是线程任何线程安全措施。但以上定义其实并不容易做到,很多时候需要弱化一些。
实际上,在多线程编程中我们需要同时关注可见性、顺序性和原子性问题。
本篇文章将讲线程安全各个级别,以及从这三个问题出发,结合实例详解volatile如何保证可见性及一定程序上保证顺序性以及synchronized如何同时保证可见性和原子性,最后对比volatile和synchronized的适用场景。
2 Java中的线程安全
Java中线程安全强度由强到弱是:
不可变 -> 绝对线程安全 -> 相对线程安全 -> 线程兼容 -> 线程对立。
2.1 不可变
就是指那些一旦初始化就不可改变的对象,这当然是线程安全的:
- final修饰的基本数据类型
- final修饰的不可变对象如String, 基本类型的封装类型(Long,Integer等,因为他们的状态变量都为final,而AtomicLong等对象内部value只用了volatile修饰,并非不可变对象)(注意this指针不可逃逸)
这类对象永远不会改变,多线程状态下也永远处于一致的状态。
这种方式实现不可变最简洁,但使用场景较少。
2.2 绝对线程安全
不用考虑运行时环境,调用者不用加任何同步措施,就能保证多线程情况下拥有正确结果。
这很难达到,Java中很多所谓线程安全类都不是绝对线程安全。
比如Vector
,他每个方法都是synchronized
的,但如果多线程同时读写,可能会刚刚某线程remove
了某个序号上的元素,另一个线程去访问这个序号的元素就可能导致报错。
解决方案就是读写方法都对这个vector实例使用synchronized
加上同步锁,实现排他性。
如果要实现我们前面定义中提到的绝对线程安全,就必须在Vector内部加上快照,每次有元素改动时就产生新的快照。读取时,就读那一刻的不可变快照。但是这样开销很大,维护麻烦。
以下实例可说明Vector非绝对线程安全:
@Test
public void test3 () {
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++) {
Thread.yield();
try{
vector.remove(i);
} catch (Exception e){
e.printStackTrace();
System.exit(-1);
}
}
}
});
Thread printThread = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < vector.size(); i++) {
Thread.yield();
try{
System.out.println(vector.get(i));
} catch (Exception e){
e.printStackTrace();
System.exit(-1);
}
}
}
});
removeThread.start();
printThread.start();
while (Thread.activeCount() > 20) ;//防止线程太多计算机卡死
}
}
以上代码很有可能出现删除线程下标越界的问题:
java.lang.ArrayIndexOutOfBoundsException: Array index out of range: 9
at java.util.Vector.remove(Vector.java:836)
at demos.collection.TestVectorNoSafe$3.run(TestVectorNoSafe.java:74)
at java.lang.Thread.run(Thread.java:748)
java.lang.ArrayIndexOutOfBoundsException: Array index out of range: 14
at java.util.Vector.remove(Vector.java:836)
at demos.collection.TestVectorNoSafe$3.run(TestVectorNoSafe.java:74)
at java.lang.Thread.run(Thread.java:748)
java.lang.ArrayIndexOutOfBoundsException: Array index out of range: 13
at java.util.Vector.remove(Vector.java:836)
at demos.collection.TestVectorNoSafe$3.run(TestVectorNoSafe.java:74)
at java.lang.Thread.run(Thread.java:748)
java.lang.ArrayIndexOutOfBoundsException: Array index out of range: 11
at java.util.Vector.remove(Vector.java:836)
以上报错是因为别的线程已经remove了某个下标的元素,而报错的线程在失去cpu时间片、重新获取时间片后还试图去移除这个不存在元素,导致报错。
Vector所谓的线程安全是指调用Vector类的所有成员方法时加入了synchronized
修饰,所以其他线程不能再访问该Vector对象,这是相对线程安全。
但在调用两个Vector成员方法时,当前线程有可能在完成第一个方法后时间片到期,这时其他线程可以访问该Vector对象,造成调用第二个成员方法的结果可能与预想结果不同。这时为保证线程安全,需要加synchronized
。
2.3 相对线程安全
相对线程安全保证了对这个对象单独的操作是线程安全的,即在调用的时候不需要做额外的保障措施。但对于一些特定顺序的连续调用,就可能需要在调用端使用额外的同步手段来保证调用的正确性。
我们上面例子中的Vector、ConcurrentHashMap就是相对线程安全。
2.4 线程兼容
就是说本身并不线程安全,需要额外的如同步锁之类的手段,就可以确保线程安全。
常用的容器都是此类,如HashMap
, HashSet
等。
2.5 线程对立
指那些无法通过手段达到线程安全目的的代码。
如suspend
的时候加同步锁,那他会一直持有锁直到resume
。而当对这个线程调用resume
之前也申请同步锁,那就肯定发生死锁。
3 线程安全实现
3.1 互斥方式同步(悲观同步)
相关概念可参考
-
互斥是实现同步的手段,主要互斥方法有:
- 临界区(Critical Section,控制统一进程内拥有权限线程才能访问临界区资源,如Synchronized)
- 互斥量(Mutex,因为互斥量可以命名所以可以跨进程,同一时刻允许一个线程访问共享数据,如ReentrantLock)
- 信号量(Semaphore,同一时刻允许若干拿到许可的线程访问共享数据,如
java.util.concurrent.Semaphore
)
-
同步指多线程访问共享数据时,只能被一个线程使用(如果用
Semaphore
就是若干个)
常见实现方法如下:
synchronized
java.util.concurrent
如ReentrantLock
本方式无论如何都是先加锁,所以也称为悲观同步。
3.2 非阻塞同步(乐观同步)
3.2.1 概念
上述互斥同步方式最大的开销是线程阻塞和唤醒,而且属于是悲观策略。
乐观同步常见的就是CAS(offset, expectVal, newVal)
。
该方法思想是,如果当前内存offset
的值还是为expectVal
,那就设为newVal
;如果值不是我们期望的,就不改变。
而且在这个过程是原子的,且这个原子性是现代处理器新增的硬件指令支持的。
3.2.2 实现
-
java.util.concurrent.atomic
包内的一些类如AtomicInteger就有如compareAndSet
这样封装好了的cas方法。 -
Unsafe
类也有许多cas的方法,并在jdk源码中大量采用,但最好不要直接去用这个类。
3.2.3 问题
CAS有个问题就是ABA问题,详见Java-并发-CAS
3.3 无同步实现线程安全
主要是一些不会访问共享变量、不依赖全局变量的情况,他们是线程安全的。
3.3.1 可重入代码
不依赖全局变量、一个方法的结果只依赖参数,也就是说结果完全可以依据传入参数预测。(但并不能说线程安全代码一定可重入)
3.3.2 线程本地存储
ThreadLocal, 即线程独有变量。详见Java-多线程-ThreadLocal全解析
4 关于锁
关于锁的内容,可以参考另一篇文章Java-并发-LockLike
5 关于可见性、顺序性和原子性问题
请参考:
6 线程安全实例
参考文档
《深入理解Java虚拟机-第二版》 作者周志明