内存屏障(Memory Barrier)
1.可见性
- 写屏障(Sfence)保证该屏障之前的,对共享变量改动都同步到主内存中去
- 读屏障(Ifence)保证该屏障之后的,对共享变量读取加载的为主内存中最新数据
2.有序性
- 写屏障在指令重排序时,不会将写屏障之前的代码排到屏障之后
- 读屏障在指令重排序时,不会将读屏障之后的代码排到屏障之前
volatile原理
volatile底层原理基于内存屏障
- 对volatile变量写指令会在之后加入写屏障
- 对volatile变量读指令会在之前加入读屏障
如何保证可见性?
- 写屏障(sfence)保证在该屏障之前的,对共享变量的改动,都同步到主存当中
public void actor2(I_Result r) {
num = 2;
ready = true; // ready 是 volatile 赋值带写屏障
// 写屏障
}
- 而读屏障(lfence)保证在该屏障之后,对共享变量的读取,加载的是主存中最新数据
public void actor1(I_Result r) {
// 读屏障
// ready 是 volatile 读取值带读屏障
if(ready) {
r.r1 = num + num;
} else {
r.r1 = 1;
}
}
T1线程 num volatile ready=false T2 num=2 写屏障ready = true num=2 读屏障ready=true T1线程 num volatile ready=false T2
如何保证有序性?
- 写屏障在指令重排序时,确保不会将写屏障之前代码排到写屏障之后
public void actor2(I_Result r) {
num = 2;
ready = true; // ready 是 volatile 赋值带写屏障
// 写屏障
}
- 写屏障在指令重排序时,确保不会将读屏障之后代码排到读屏障之前
public void actor1(I_Result r) {
// 读屏障
// ready 是 volatile 读取值带读屏障
if(ready) {
r.r1 = num + num;
} else {
r.r1 = 1;
}
}
不能解决指令交错:
写屏障仅仅是保证之后的读能够读到最新的结果,但不能保证读跑到它前面去
而有序性的保证也只是保证了本线程内相关代码不被重排序
double-checking lock问题
- 方式一
public class Singleton {
// 私有化构造
private void Singleton(){}
private static Singleton singleton = null;
//在静态方法上添加synchronized锁住的事类对象
public static synchronized Singleton getInstance(){
if(singleton == null){
singleton = new Singleton();
}
return singleton;
}
//与getInstance相同
public static Singleton getInstance1(){
synchronized(Singleton.class){
if(singleton == null){
singleton = new Singleton();
}
}
return singleton;
}
}
上述代码本质上是没有问题,但是在性能方面不太优化。因为多线程情况下,每次调用getInstance方法,都要进行判断并且都要获取锁。
- 方式二
public class Singleton {
// 私有化构造
private void Singleton(){}
private static Singleton singleton = null;
//解决了如果已经存在实例对象无须进行加解锁操作,提高性
public static Singleton getInstance(){
if(singleton == null){
// 首次访问会同步,而之后的使用没有 synchronized
synchronized(Singleton.class){
if(singleton == null){
singleton = new Singleton();
}
}
}
return singleton;
}
}
特点:
- 懒惰实例化
- 首次使用 getInstance() 才使用 synchronized 加锁,后续使用时无需加锁
有隐含的,但很关键的一点:第一个 if 使用了 INSTANCE 变量,是在同步块之外
但在多线程环境下,上面的代码是有问题的,getInstance 方法对应的字节码为:
0: getstatic #2 // 获取静态变量Instance Field INSTANCE:com/single/Singleton;
3: ifnonnull 37 // 判断不是null 跳转到37行
6: ldc #3 // 获取类对象锁 class com/single/Singleton
8: dup
9: astore_0
10: monitorenter
11: getstatic #2 // Field INSTANCE:com/single/Singleton;
14: ifnonnull 27
17: new #3 //创建实例 class com/single/Singleton
20: dup //复制引用
21: invokespecial #4 // 复制的引用调用构造方法Method "<init>":()V
24: putstatic #2 // 赋值 Field INSTANCE:com/single/Singleton;
27: aload_0
28: monitorexit
29: goto 37
32: astore_1
33: aload_0
34: monitorexit
35: aload_1
36: athrow
37: getstatic #2 // Field INSTANCE:com/single/Singleton;
40: areturn
其中
- 17 表示创建对象,将对象引用入栈 // new Singleton
- 20 表示复制一份对象引用 // 引用地址
- 21 表示利用一个对象引用,调用构造方法
- 24 表示利用一个对象引用,赋值给 static INSTANCE
也许 jvm 会优化为:先执行 24,再执行 21。如果两个线程 t1,t2 按如下时间序列执行:
T1 T2 INSTANCE 17:new Singleton 20:dup 21:putstatic 对象引用赋值 0:获取静态变量Instance 3: ifnonnull 37 // 判断不是null 跳转到37行 37: getstatic 获取变量 40: areturn 使用对象 21:nvokespecial T1 T2 INSTANCE
- 关键在于 0: getstatic 这行代码在 monitor 控制之外,一些线程可以越过 monitor 读取
INSTANCE 变量的值 - 这时 t1 还未完全将构造方法执行完毕,如果在构造方法中要执行很多初始化操作,那么 t2 拿到的是将是一个未初始化完毕的单例
- 对 INSTANCE 使用 volatile 修饰即可,可以禁用指令重排,但要注意在 JDK 5 以上的版本的 volatile 才会真正有效
double-checking lock解决
public class Singleton {
// 私有化构造
private void Singleton(){}
private static volatile Singleton singleton = null;
//解决了如果已经存在实例对象无须进行加解锁操作,提高性
public static Singleton getInstance(){
if(singleton == null){
// 首次访问会同步,而之后的使用没有 synchronized
synchronized(Singleton.class){
if(singleton == null){
singleton = new Singleton();
}
}
}
return singleton;
}
}
字节码上看不出volatile执行
// -------------------------------------> 加入对 INSTANCE 变量的读屏障
0: getstatic #2 // Field INSTANCE:com/single/Singleton;
3: ifnonnull 37
6: ldc #3 // class com/single/Singleton
8: dup
9: astore_0
10: monitorenter -----------------------> 保证原子性、可见性
11: getstatic #2 // Field INSTANCE:com/single/Singleton;
14: ifnonnull 27
17: new #3 // class com/single/Singleton
20: dup
21: invokespecial #4 // Method "<init>":()V
24: putstatic #2 // Field INSTANCE:com/single/Singleton;
// -------------------------------------> 加入对 INSTANCE 变量的写屏障
27: aload_0
28: monitorexit ------------------------> 保证原子性、可见性
29: goto 37
32: astore_1
33: aload_0
34: monitorexit
35: aload_1
36: athrow
37: getstatic #2 // Field INSTANCE:com/single/Singleton;
40: areturn
如上面的注释内容所示,读写 volatile 变量时会加入内存屏障(Memory Barrier(Memory Fence)),保证下面
两点:
可见性
- 写屏障(sfence)保证在该屏障之前的 t1 对共享变量的改动,都同步到主存当中
- 而读屏障(lfence)保证在该屏障之后 t2 对共享变量的读取,加载的是主存中最新数据
有序性
- 写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后
- 读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前
- 更底层是读写变量时使用 lock 指令来多核 CPU 之间的可见性与有序性