volatile的特性
tips: 为好理解volatile特性,可将对volatile变量的单个读/写看成是使用同一个锁对这些单个读/写操作做了同步。
1 | 示例代码: |
锁的happens-before规则保证释放锁和获取锁的两个线程之间内存可见,意味着对一个volatile变量的读,总能看到对这个变量最后的写入。
volatile变量自身具有两个特性:
- 可见性:一个volatile变量的读,总能看到对这个变量最后的写入;
- 原子性: 对任意单个volatile变量的读/写具有原子性,但类似于volatile++这种复合操作不具有原子性。
volatile写-读建立的happens-before关系
从jdk1.5开始,volatile变量的读-写可以实现线程之间的通信
从内存语义角度来说,volatile的写-读与锁的释放-获取具有相同的内存效果(写->释放,读->获取)。
volatile写-读的内存语义
写-内存语义:当写一个volatile变量时,JMM会把该线程对应的本地内存中共享变量值刷新到主内存
读-内存语义:当读一个volatile变量时,JMM会把该线程本地内存置为无效。线程接下来将从主内存中读取共享变量
volatile内存语义的实现
volatile重排序规则表
| 是否能重排序 | 第二个操作 | ||
|---|---|---|---|
| 第一个操作 | 普通读/写 | volatile读 | volatile写 |
| 普通读/写 | NO | ||
| volatile读 | NO | NO | NO |
| volatile写 | NO | NO | |
- 为实现volatile的内存语义,编译器生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。保守策略的JMM内存屏障插入策略:
- 在每个volatile写操作前面插入一个StoreStore屏障;
- 在每个volatile写操作后面插入一个StoreLoad屏障;
- 在每个volatile读操作后面插入一个LoadLoad屏障;
- 在每个volatile读操作后面插入一个LoadStore屏障;
保守策略下,volatile写插入内存屏障后生成的指令序列示意图如下:
保守策略下,volatile读插入内存屏障后生成的指令序列示意图如下:
锁的内存语义
锁是Java并发编程中最重要的同步机制,它除了让临界区互斥执行外,还可以让释放锁的线程向获取锁的另一个线程发送消息。
锁内存语义的实现
1 | 示例代码 |
ReentrantLock实现依赖于Java同步框架AbstractQueuedSynchronizer,该框架使用一个整形的volatile变量来维护同步状态,具体ReentrantLock类图如下:
ReentrantLock分为公平锁和非公平锁,内存语义如下:
- 公平锁和非公平锁释放时,最后都要写一个volatile变量state;
- 公平锁获取时,首先会去读取volatile变量;
- 非公平锁获取时,首先会用CAS更新volatile变量,这个操作同时具有volatile读/写的内存语义。
同样对reentrantLock的分析可以看出,锁释放-获取的内存语义的实现至少由以下两种方式:
- 利用volatile变量的读-写所有具有的内存语义;
- 利用CAS所附带的volatile读和volatile写的内存语义。
concurrent包的实现
Java线程之间的通信由以下4种方式
- A线程写volatile变量,随后B线程读这个volatile变量;
- A线程写volatile变量,随后B线程用CAS更新这个volatile变量;
- A线程用CAS更新一个volatile变量,随后B线程用CAS更新这个volatile变量;
- A线程用CAS更新一个volatile变量,随后B线程读这个volatile变量;
从整体看,concurrent包的实现示意图如下:
final域的内存语义
final域的重排序规则
对于final域,编译器和处理器遵守以下两个重排序规则:
- 在构造函数内,对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这个操作之间不能重排序;
- 初次读一个包含final域的对象的引用,与随后读这个final域的引用,这个操作之间不能重排序