volatile的作用
public class LazySingleton {
//volatile 防止指令重排
private volatile static LazySingleton instance;
private LazySingleton(){
}
public static LazySingleton getInstance(){
if(instance == null){
synchronized (LazySingleton.class){
if (instance == null) {
instance = new LazySingleton();
}
}
}
return instance;
}
}
我们进行了两次 if (singleton == null) 检查,这种写法是可以保证线程安全的,假设有两个线程同时到达 synchronized 语句块,那么实例化代码只会由其中先抢到锁的线程执行一次,而后抢到锁的线程会在第二个 if 判断中发现 singleton 不为 null,所以跳过创建实例的语句。再后面的其他线程再来调用 getInstance 方法时,只需判断第一次的 if (singleton == null) ,然后会跳过整个 if 块,直接 return 实例化后的对象。
这种写法的优点是不仅线程安全,而且延迟加载、效率也更高。
volatile 在其中又起到什么作用呢?
public class Main {
public static void main(String[] args) {
// write your code here
Book book = new Book();
}
}
这Main函数在字节码层做了哪些操作呢,我们对Main函数的Class文件进行反编译(javap -v Main.class)
1.new会在堆空间里开辟一个空间,并将该空间的引用地址返回,存入到栈中。
2.然后dup指令为复制操作数栈顶值,并将其压入栈顶,也就是说此时操作数栈上有连续相同的两个对象地址;
3.invokespecial指令调用实例初始化方法<init>:()V,注意这个方法是一个实例方法,所以需要从操作数栈顶弹出一个this引用,也就是说这一步会弹出一个之前入栈的对象地址;
4.此时栈中还有一个对象的引用地址,astore_1指令将引用地址赋值给对应的变量
Book book = new Book() 并非是一个原子操作。
事实上,在 JVM 中上述语句至少做了以下这 3 件事::1.分配控制2.初始化 3.引用赋值
例如:在单列模式中此时有两个线程,当第一个线程获取锁后,判instance是否为空,此时还未有则new一个,而恰巧此是程序发生了指令重排2,3步骤发生调换,先赋值后初始化,而在赋值后,还未初始化时,第二个线程判断instance是否空,以为该变量已经赋值,则返回Ture,返回单例对象,但此对象还未进行初始化,则程序将有可能引发空指针等错误。
此时给instance 添加volatile 就是为了防止JIT 、CPU等进行指令重排,相当于是表明了该字段的更新可能是在其他线程中发生的,因此应确保在读取另一个线程写入的值时,可以顺利执行接下来所需的操作。