锁(synchronized)除了让临界区互斥执行,其内存语义也要重视。
1、锁释放、获取与happens-before关系
锁除了互斥执行代码,还能让释放锁的线程向另一个拿锁的线程发消息。
public class SynchronizedExample {
int a = 0;
public synchronized void writer() { //1
a++; //2
} //3
public synchronized void reader() { //4
int b = a; //5
System.out.println("b -> " + b); //6
//...
}
}
假如线程A执行完writer(),B线程开始执行reader()。根据happens-before原则(hb原则)可分为两类:
- 程序次序规则
- 1 > 2 > 3
- 4 > 5 > 6
- 监视器锁规则
- 3 > 4
综合1)、2) ,可知 2 > 5
画成图就是:
总结来说就是:
A在释放锁之前所有可见的共享变量,在B拿锁之后就对B可见!
2、锁释放、获取的内存语义
锁释放时,会将持锁线程的工作内存的共享变量值刷到主内存;
拿到锁时,JMM将持锁线程的工作内存置为无效,临界区代码必须从主内存中读共享变量。
锁释放与volatile写;锁获取与volatile读有相同内存语义!
小结下:
- A释放锁 ,即 A向后来将拿锁的线程B发送一个消息
- B拿锁,即B接收了之前释放锁的线程A的一个消息
- A释放锁、B拿锁,其实就是A向B发送个消息。
3、锁内存语义的实现
本文借助ReentrantLock
的一个demo说明
public class ReentrantLockExample {
int a = 0;
ReentrantLock lock = new ReentrantLock();
public void writer() {
lock.lock();
try {
a++;
} finally {
lock.unlock();
}
}
public void reader() {
lock.lock();
try {
int i = a;
System.out.println(" i --> " + i);
} finally {
lock.unlock();
}
}
}
这里首先补充下:CAS是同时具有volatile
的读写内存语义的。
原因简单来说就是CAS指令执行前会有lock
前缀指令。lock
前缀指令能
- 1)确保对内存的读-改-写操作原子执行
- 2)禁止该指令和之前、之后的读和写指令重排
- 3)将写缓冲区的数据刷到内存
2)3)具有的内存屏障效果,足以同时实现volatile
读写的内存语义。可见:CAS是有volatile
读写内存语义的。
ReentrantLock
的内部实现依赖于AQS
。AQS使用一个整型的volatile
维护同步状态。ReentrantLock
分公平锁与非公平锁。
- 对于公平锁: 加锁方法首先读
volatile
变量state
;在释放锁时,最后写volatile
变量state
- 对于非公平锁:加锁首先会用CAS更新
volatile
变量state
,该操作同时具有volatile
的读写语义。
释放锁时,同样要写volatile
变量state
。
此处省略具体代码分析
其实从中也可以看出:要实现synchronized
释放、获取的内存语义,至少有两种方式:
- 利用
volatile
读写的内存语义 - 利用CAS附带的
volatile
读与volatile
写的内存语义。
4、concurrent包的实现
CAS同时有volatile
读写的内存语义,因此Java线程有4种通信方式。
- A写
volatile
,B读 - A写
volatile
,B用CAS更新 - A用CAS更新,B随后也用CAS更新
- A用CAS更新,B读这个
volatile
1)CAS是机器级别的指令,能以原子方式对内存执行读-该-写操作;
2)同时,volatile
的读写和CAS可以实现线程间的通信。
将1) 2) 整合在一起,能实现强大的功能的,构成JUC实现的基石。
看JUC源码后,能抽象出一个通用模式:
- 声明一个volatile
- 用CAS更新线程间同步
- 配合
volatile
读写和CAS所具有的volatile
读写内存语义来实现线程间通信。
从实践上来说,JUC的 AQS、非阻塞数据结构、原子类等基础类就是使用这种模式实现的。下图是JUC的实现示意图: