故事背景
团队内部前几天讨论了一个面试题,在本地用乐观锁和悲观锁实现计数器需要volatile关键字吗?毫无疑问,使用乐观锁一定是需要的。但使用悲观锁需要呢?
张三:不需要吧,每次不都是一个线程访问变量吗?
李四:还是需要的,加锁只是保证了该变量被一个线程独占,但是不能保证拿到变量最新的值,因为可能上个线程操作后数据还在线程本地内存里,导致本线程读取的数据是脏数据!
张三:嘶,好像有点道理,不过如果加锁的话,这个本地内存的数据什么时候刷到主内存呢?会不会加锁后就直接读取到最新数据了?
李四:诶?问得好,这个得研究研究。
预备知识
Hppens-Before 规则
Java Memory Model(JMM) 里定义了一些跨线程操作的 Happens-Before 关系,并据此来决定线程间一些操作的相对顺序。如果说操作 A “Happens-Before” B,则有两个含义:
- 可见性:A 的操作对 B 可见
- 顺序性:A 要在 B 之前执行
Happens-before 规则有多条,本文只借助几条来进行解释
- 程序顺序规则:如果程序中操作 A 在操作 B 之前,那么在线程中操作 A Happens-Before 操作 B
- 监视器锁规则:监视器上的 unlock 操作 Happens-Before 同一个监视器的 lock 操作
- volatile 变量规则:写入 volatile 变量 Happens-Before 读取该变量
- 传递性:如果
hb(A, B)
且hb(B, C)
,则hb(A, C)
- …
volatile 的内存语义
从内存语义的角度来说,volatile的写-读
与锁的释放-获取
有相同的内存效果。
volatile 写的内存语义:当写一个 volatile 变量时,JMM 会把该线程对应的本地内存中写 volatile 前所有的共享变量刷新到主内存中,并让其他 core 的缓存失效,不管这些变量是否volatile,不仅仅只是 volatile 变量本身。
volatile 读的内存语义:当读一个 volatile 变量时,JMM 会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。
关于 volatile 写的说明:https://jenkov.com/tutorials/java-concurrency/volatile.html
synchronized 实现
synchronized 可见性原理
Synchronized 的 Happens-Before 规则,即监视器锁规则
:对同一个监视器的解锁,Happens-Before 于对该监视器的加锁。
图中每一个箭头连接的两个节点就代表之间的 Happens-Before 关系,红色的为监视器锁规则推导而出:线程A释放锁 Happens-Before 线程B加锁;蓝色的则是通过程序顺序规则和监视器锁规则推测出来 Happens-Before 关系,通过传递性规则进一步推导的 Happens-Before 关系。
根据 Happens-Before 规则的程序顺序规则:如果 A Happens-Before B,则 A 的执行结果对 B 可见,并且 A 的执行顺序先于 B。因此,如果线程 A 修改了计数器的值,对线程 B 是可见的。
synchronized 验证代码
public class TestSynchronizedCounter {
private static int count = 0;
public static void main(String[] args) {
new Thread(() -> {
for (int i = 0; i < 100000; i++) {
synchronized (TestSynchronizedCounter.class) {
count++;
}