基本概述
一致性
JVM内存的会分为主内存和线程内存,比如一个对象等数据就会放在主内存的堆里面,堆内存主要是物理内存,如果一个线程需要多次访问主内存中的一个对象,那么就会出现多次物理内存的查找,这样带来的性能损耗会比较大,为了解决这个问题,JVM会把线程使用频繁的对象缓存到线程之中,在缓存中高速访问,那么就会降低损耗。但是在多线程的环境下会出现幻读,或者重复读的问题,为了解决这个问题,Java推出了volatile关键字。
volatile是一个特殊的修饰符,只有成员变量才能使用它。在Java并发程序缺少同步类的情况下,多线程对成员变量的操作对其它线程是透明的。volatile变量可以保证下一个读取操作会在前一个写操作之后发生。线程都会直接从内存中读取该变量并且不缓存它。这就确保了线程读取到的变量是同内存中是一致的。
有序性
JVM在编译字节码的时候,就是把.java文件编译为.class文件的时候,为了将性能优化,有的时候会将指令进行重新排序。这在一些并发场景就会产生一些问题。就比如在双重判断的懒汉单例模式中,对instance判断是否存在那部分代码被两个线程访问,会导致一个线程拿到未完全实例化的一个对象。可以使用volatile来解决这个问题。
用volatile关键字修饰成员变量,可以确保被修饰的成员变量在JVM编译字节码的时候不被重排序。
实现原理
volatile 的底层实现原理是内存屏障,Memory Barrier(Memory Fence)
- 对 volatile 变量的写指令后会加入写屏障
- 对 volatile 变量的读指令前会加入读屏障
如何保证可见性
- 写屏障(sfence)保证在该屏障之前的,对共享变量的改动,都同步到主存当中
- 读屏障(lfence)保证在该屏障之后,对共享变量的读取,加载的是主存中最新数据
如何保证有序性
- 写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后
- 读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前
- 注意,这仅仅是在单线程角度上的有序性
解决单例模式的双重检测问题
public final class Singleton {
private Singleton() { }
private static Singleton INSTANCE = null;
public static Singleton getInstance() {
if(INSTANCE == null) { // t2
// 首次访问会同步,而之后的使用没有 synchronized
synchronized(Singleton.class) {
if (INSTANCE == null) { // t1
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}
这是没有加volatile关键字的双重检测懒汉单例模式,这种单例模式可以保证线程安全,并且减小了锁的粒度,以及加锁的次数,首次使用 getInstance() 才使用 synchronized 加锁,后续使用时无需加锁,但是问题在于第一个INSTANCE的判断,这个判断是在同步代码块之外的。在多线程环境下会出现问题。
0: getstatic #2
3: ifnonnull 37
6: ldc #3
8: dup
9: astore_0
10: monitorenter
11: getstatic #2
14: ifnonnull 27
17: new #3
20: dup
21: invokespecial #4 // Method "<init>":()V
24: putstatic #2
27: aload_0
28: monitorexit
29: goto 37
32: astore_1
33: aload_0
34: monitorexit
35: aload_1
36: athrow
37: getstatic #2
40: areturn
这是getInstance对应的字节码
其中
- 17 表示创建对象,将对象引用入栈 // new Singleton
- 20 表示复制一份对象引用 // 引用地址
- 21 表示利用一个对象引用,调用构造方法
- 24 表示利用一个对象引用,赋值给 static INSTANCE
也许 jvm 会优化为:先执行 24,再执行 21。如果两个线程 t1,t2 按如下时间序列执行:
关键在于 0: getstatic 这行代码在 monitor 控制之外,它就像之前举例中不守规则的人,可以越过 monitor 读取 INSTANCE 变量的值
这时 t1 还未完全将构造方法执行完毕,如果在构造方法中要执行很多初始化操作,那么 t2 拿到的是将是一个未初始化完毕的单例
对 INSTANCE 使用 volatile 修饰即可,可以禁用指令重排,但要注意在 JDK 5 以上的版本的 volatile 才会真正有效
public final class Singleton {
private Singleton() { }
private static volatile Singleton INSTANCE = null;
public static Singleton getInstance() {
// 实例没创建,才会进入内部的 synchronized代码块
if (INSTANCE == null) {
synchronized (Singleton.class) { // t2
// 也许有其它线程已经创建实例,所以再判断一次
if (INSTANCE == null) { // t1
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}