对Java在并发环境下线程安全的实现方式做点笔记。
基础知识
先介绍一些Java线程相关的基础概念
线程生命周期
Java中线程也是一个对象,由Thread
类创建,其状态STATE
有:
- NEW 已创建未启动
- RUNABLE:已获得CPU资源(RUNNING)和可被CPU调度(READY)状态
- BLOCKED:阻塞状态,进行I/O调用和请求被其他线程占有的锁
- WAITING:等待其他线程特定的操作
- TIMED_WATING:并非无限制等待,到达一定时间后变为RUNABLE状态
- TERMINATED:终止
线程生命周期如下:
竞态
相同输入得不到相同输入,计算结果与时间有关的状态即为竞态。
竞态产生的原因一般是多个线程在不加控制的条件下并发更新、读取了同一个共享变量,引发竞态的模式主要有两种:
- read-and-modify
- check-then-action
线程安全
一个类在单线程下运行正常,在多线程环境下也能运行正常,就称这个类是线程安全的。如果一个类是线程安全的,就不会引发竞态。标准库中一些常见的类都不是线程安全的,如ArrayList
、HashMap
、SimpleDateFormat
。
线程安全需要保证三个条件:原子性、可见性和有序性。
原子性 Atomic
线程读写某个变量的操作,从其他线程看不存在中间状态,要么执行发生,要么未发生,就称其满足原子性。
Java原子性的方式主要是:
- 锁(指代码中的锁,也叫软件锁)
- CAS(硬件锁)
Java中对基本类型的写操作,除了long和double外都是原子性的。
可见性 Visibility
线程对共享变量的更新对其他线程可见,即其他线程在线程更新之后立即可以读到最新值,就称其满足可见性。
下面两个问题会影响可见性:
- 编译优化,如将
while
循环内的判断优化至循环外。 - 计算机存储结构,处理器会缓存一些变量的更新。
CPU有自己的缓存单元,包括寄存器 Registerr,高速缓存 Cache,写缓冲器 Store Buffer,无效化队列 Invalidate Queue
Java的一些同步机制会使CPU缓存同步,即使CPU可以读到另一个CPU上的缓存:
- 写线程冲刷处理器缓存:更新一个变量后使处理器对变量的修改写入Cache或RAM中,而不是停留在Store Cache
- 读线程刷新处理器缓存:读取一个变量的值时,如果其他处理器已经更新,需要进行缓存同步
有序性 Odering
线程执行的内存访问操作在其他线程看来没有发生变化,就称其满足有序性。重排序会影响有序性。
上下文切换
一个线程被暂停,另一个线程获取CPU控制权的过程,称作上下文切换。切换过程包括线程切出 Switch out和线程切入 Switch in。
线程在进行上下文切换时需要在内存中读取或保存自己的运行状态,包括通用寄存器的内容和程序计数器的内容。
上下文切换可分为两种方式:
- 自发性上下文切换:线程主动让出CPU控制权,如调用
wait
/sleep
/yield
/join
/park
、进行I/O操作或请求其他线程持有的锁。 - 非自发性上下文切换:线程的CPU时间片耗尽或被其他高优先级线程(如GC)夺取CPU控制权。
上下文切换时有开销的:
- 直接开销:CPU调度的开销和保存/恢复上下文的开销。
- 间接开销:高速缓存重新加载和高速缓存被冲刷(Flush到二级缓冲)。
Java的线程同步机制
每种同步机制需要分析其是否满足原子性、可见性和有序性,是否存在上下文切换开销。
锁 Lock
Java中锁的实现有两种
- 内部锁:
synchronize
关键字 - 显式锁:
java.concurrent.locks.Lock
接口的实现类
用锁实现线程同步可以满足原子性、可见性、有序性,存在上下文开销
- 原子性:被锁同步的代码块称作临界区,同一时间只有一个线程可以访问临界区。
- 可见性:Java中获得锁(Acquire)刷新处理器缓存,释放锁(Release)会冲刷处理器缓存。
- 有序性:临界区内的指令不会被重排序到临界区外。
- 上下文切换开销:请求被其他线程持有的锁会发生上下文切换。
volatile
用volatile
修饰的变量满足可见性、有序性和写操作的原子性,但不具有计算的原子性,不存在上下文切换。
- 可见性:变量不会被缓存在Store Cache中,即写操作之后会冲刷处理器缓存,读操作之前会刷新处理器缓存。
- 有序性:编译器不会对该变量上的操作与其他内存操作进行重排序。
- 原子性:不具有自增等计算的原子性
CAS
java.util.concurrent.atomic
中的原子类借助CAS实现了可见性和原子性,它的计算实现基于一种乐观锁的思想:执行某种操作,如果不成功就一直执行直到成功为止。这里借助一段源码加强理解。
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
CAS的问题是ABA
问题,可以借助AtomicStampedReference
类解决。
安全发布
安全发布一个对象有以下几种方式:
- 在静态初始化函数中初始化一个对象的引用
static
- 将某个对象的引用保存在
volatile
类型的域或AtomicReference
对象中 - 将对象的引用保存在正确构造对象的
final
类型的域中 - 将对象的引用保存在由锁保护的域中
(End)