所属文集:一起掌握并发
1.禁止重排序
为了性能优化,JMM 在不改变正确语义的前提下,会允许编译器和处理器对指令序列进行重排序。
JMM 提供了内存屏障阻止这种重排序;Java 编译器会在生成指令系列时在适当的位置会插入内存屏障指令来禁止特定类型的处理器重排序。说白了就是保证代码中读写变量的顺序,谁先谁后不能乱。顺序错乱了,读写的结果就不对啦
1.1 JMM 针对编译器制定 volatile 重排序规则表:
读写的类型分为:
- 无volatile关键字的变量的读写(普通读写)
- volatile关键字修饰的变量的读(volatile读)
- volatile关键字修饰的变量的写(volatile写)
对有volatile关键字所修饰的变量的读写操作,其前后其他的读写操作的重排优化要注意(必须保证可见性)
image.png上图中NO是禁止重排,划重点:
1)当第二个操作是volatile写时,不管第一个操作是什么,都不能重排序。这个规则确保volatile写之前的操作不会被编译器重排序到volatile写之后。
2)当第一个操作是volatile读时,不管第二个操作是什么,都不能重排序。这个规则确保volatile读之后的操作不会被编译器重排序到volatile读之前。
3)当第一个操作是volatile写,第二个操作是volatile读时,不能重排序。
1.2 指令序列中插入内存屏障来禁止特定类型的处理器重排序
为了实现 volatile 内存语义时,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序,JMM采取了保守的内存屏障插入策略,不求最优,但求不错:
1)在每个volatile写操作的前面插入一个StoreStore屏障。
2)在每个volatile写操作的后面插入一个StoreLoad屏障。
3)在每个volatile读操作的后面插入一个LoadLoad屏障。
4)在每个volatile读操作的后面插入一个LoadStore屏障。
volatile 写是在前面和后面分别插入内存屏障,而 volatile 读操作是在后面插入两个内存屏障。
image.png
- volatile 写是在前面和后面分别插入内存屏障
-
volatile 读操作是在后面插入两个内存屏障。
image.png
happens-before原则之volatile 变量规则
对一个 volatile 域的写,happens-before 于任意后续对这个 volatile 域的读。
3使用场景:
双重检查(double-checked)单例模式
几个主要流程可能产生重排序:
- 分配内存
- 初始化内存
- 地址赋值给变量
若2,3发生重排序,可能出现拿到了地址,但是数据还未初始化好。
所以不能忽略 volatile 关键字
class Singleton {
private volatile static Singleton instance;
public static Singleton getInstance() {
if (instance == null) {
syschronized(Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
推荐懒加载优雅写法 Initialization on Demand Holder(IODH)。
public class Singleton {
static class SingletonHolder {
static Singleton instance = new Singleton();
}
public static Singleton getInstance(){
return SingletonHolder.instance;
}
}
volatile 与 synchronized 的区别
- 首先volatile关键字本身就包含了禁止指令重排序的语义,而synchronized(及其它的锁)是通过“一个变量在同一时刻只允许一条线程对其进行lock操作”这条规则获得的,这条规则决定了持有同一个锁的两个同步块智能串行的进入。
- synchronized可以保证原子性操作,volatile不可以
- 。。。
【Java 并发笔记】volatile 相关整理
http://ifeve.com/from-singleton-happens-before/
网友评论