前言
AbstractQueuedSynchronizer 应该算是JUC包里面最重要的一个类,也是最复杂的一个类。对它的理解决定这你对JUC编程的掌握程度。本篇文章开始就是要来啃JUC包下面这一块最难啃的骨头-AQS,但是在开始之前还是要来清理一下拦在我们面前的volatile关键字!
特殊的变量Volatile
volatile的特性
- 可见性:对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入。
- 原子性:对任意单个volatile变量的读/写 具有原子性,但类似 volatile++这种复合操作不具有原子性。
volatile变量的内存语义及实现
- volatile写的内存语义,当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存
- volatile读内存抑郁,当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效,线程接下来将从主内存中读取共享变量
JVM对volatile内存语义的实现 - 在每个volatile写操作的前面插入一个StoreStore屏障
- 在每个volatile写操作的后面插入一个StoreLoad屏障
- 在每个volatile读操作的后面插入一个LoadLoad屏障
- 在每个volatile读操作的后面插入一个LoadStore屏障
这里对StoreStore、StoreLoad、LoadStore、LoadLoad屏障做一些必要的解释:
- StoreStore:相当于store1;StoreStore;store2 ,确保store1数据对其他的处理器可见(刷新到内存)先于store2指令以及所有后续存储指令的存储
- StoreLoad:相当于store1;StoreLoad;load2,确保store1数据对其他处理器可见,先于load2及后续所有装载指令的装载,会使之前的所有内存访问指令(存储和装载指令)完成之后,才执行屏障之后的内存访问指令。
- LoadStore:相当于load1;LoadStore;Store2,确保load1数据装载先于store2及后续所有的存储指令刷新到内存
- LoadLoad:相当于load1;LoadLoad;load2,确保load1数据的装载先于load2及后续所有的装载指令的装载
对照上面我们对屏障指令的解释 我们再来看volatile的写内存语义实现
StoreStore屏障;volatile写;StoreLoad屏障
StoreStore作用:确保volatile写之前的写对其他的处理器可见
StoreLoad作用:确保volatile写对其他的处理器可见,同时会使屏障之前的所有内存访问指令完成才执行屏障后面的内存访问指令。
volatile读;LoadStore屏障;LoadLoad屏障
LoadStore作用:确保volatile读先于后续的存储指令
LoadLoad作用:确保volatile读先于后续的所有装载指令
案例分析
为了对上面加深印象我们通过一个demo来说明
new Thread(()->{
b.write();
}).start();
new Thread(()->{
b.read();
}).start();
public void write(){
a = 10; //1
b = true; //2
}
public void read(){
while (true){
if (b){ //3
System.out.println(a); //4
}
}
}
上述demo启动两个线程A和B,A调用write方法设置共享变量a和volatile变量b的值。B线程读取a的值。
通过happens-before关系分析:
1happens-before2 (根据程序顺序规则得出)
2happens-before3(根据volatile规则)
3happens-before4(根据程序顺序规则得出)
1happens-before4(根据传递性规则)
所以通过分析我们可以得出结论 4处是可以打印10的。
通过我们的内存语义的实现分析
线程A:
a = 10
StoreStore屏障 //1
b = true //2
StoreLoad屏障 //3
1中屏障使a=10普通写对所有线程可视。3处的屏障使2处的volatile写对所有线程可视。
所以线程B必定是看到线程A中所做的修改的。
结束语
说到这 大家可以想一下如果write方法 写共享变量和写volatile变量换一个顺序B线程还会不会对共享变量可见?
public void write(){
b = true;
a = 10;
}