共计 2999 个字符,预计需要花费 8 分钟才能阅读完成。
前言
对于从事 java 开发工作的敌人来说,在工作中可能会常常接触 volatile 关键字。即便有些敌人没有间接应用 volatile 关键字,然而如果应用过:ConcurrentHashMap、AtomicInteger、FutureTask、ThreadPoolExecutor 等性能,它们的底层都应用了 volatile 关键字,你就不想理解一下它们为什么要应用 volatile 关键字,它的底层原理是什么?
从双重查看锁开始
面试时被要求写个单例模式的代码,很多敌人可能写的是双重查看锁。代码如下:
public class SimpleSingleton4 {
private static SimpleSingleton4 INSTANCE;
private SimpleSingleton4() {}
public static SimpleSingleton4 getInstance() {if (INSTANCE == null) {synchronized (SimpleSingleton4.class) {if (INSTANCE == null) {INSTANCE = new SimpleSingleton4();
}
}
}
return INSTANCE;
}
}
有些敌人看到这里感觉有点相熟,平时可能就是这个写的。
然而,我要通知你的是,这个代码有问题,它在有些时候不是单例的。为什么会呈现问题呢?
答案,在前面揭晓。
JMM(java 内存模型)
在介绍 volatile 底层原理之前,让咱们先看看什么是 JMM(即 java 内存模型)。
java 内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都能够拜访,但线程对变量的操作 (读取赋值等) 必须在工作内存中进行,首先要将变量从主内存拷贝的本人的工作内存空间,而后对变量进行操作,操作实现后再将变量写回主内存,不能间接操作主内存中的变量,工作内存中存储着主内存中的变量正本拷贝。后面说过,工作内存是每个线程的公有数据区域,因而不同的线程间无法访问对方的工作内存,线程间的通信 (传值) 必须通过主内存来实现。
java 内存模型会带来三个问题:
1. 可见性问题
线程 A 和线程 B 同时操作共享数据 C,线程 A 批改的后果,线程 B 是不晓得的,即不可见的
2. 竞争问题
刚开始数据 C 的值为 1,线程 A 和线程 B 同时执行加 1 操作,失常状况下数据 C 应该为 3,然而在并发的状况下,数据 C 却还是 2
3. 重排序问题
JVM 为了优化指令的执行效率,会对一下代码指令进行重排序。
那么如何解决问题呢?
volatile 的底层原理
java 编译器在生成指令序列的适当地位会插入内存屏障指令来禁止特定类 型的处理器重排序,从而让程序按咱们料想的流程去执行。
1、保障特定操作的执行程序。
2、影响某些数据 (或则是某条指令的执行后果) 的内存可见性。
java 的内存屏障指令如下:
对于 volatile 的写操作,在其前后别离加上 StoreStore 和 StoreLoad 指令
对于 volatile 的读操作,在其后加上 LoadLoad 和 LoadStore 指令
由上图能够看到,内存屏障是能够保障 volatile 变量前后读写程序的。
此外,对 volatile 变量写操作时,应用 store 指令会强制线程刷新数据到主内存,读操作应用 load 指令会强制从主内存读取变量值。
再看看这个例子:
public class DataTest {
private volatile int count = 0;
public int getCount() {return count;}
public void setCount(int count) {this.count = count;}
public void incr() {count++;}
}
下面列子中的 getCount 和 setCount 办法这种单操作是能够保障原子性的,然而像 incr 办法无奈保障原子性。
由此可见,volatile 关键字能够解决可见性 和 重排序问题。然而不能解决竞争问题,无奈保障操作的原子性,解决竞争问题须要加锁,或者应用 cas 等无锁技术。
再看双重查看锁问题
从下面能够看出 JMM 会有重排序问题,之前双重查看锁为什么有问题呢?
public static SimpleSingleton4 getInstance() {if (INSTANCE == null) {synchronized (SimpleSingleton4.class) {if (INSTANCE == null) {
//1. 分配内存空间
//2. 初始化援用
//3. 将理论的内存地址赋值给以后援用
INSTANCE = new SimpleSingleton4();}
}
}
return INSTANCE;
}
从代码中的正文能够看出,INSTANCE = new SimpleSingleton4();这一行代码其实经验了三个过程:
1. 分配内存空间
2. 初始化援用
3. 将理论的内存地址赋值给以后援用
失常状况下是依照 1、2、3 的程序执行的,然而指令重排之后也不排除依照 1、3、2 的程序执行的可能性,如果依照 1、3、2 的程序。
下面谬误双重查看锁定的示例代码中,如果线程 1 获取到锁进入创建对象实例,这个时候产生了指令重排序。当线程 1 执行到 t3 时刻,线程 2 刚好进入,因为此时对象曾经不为 Null,所以线程 2 能够自在拜访该对象。而后该对象还未初始化,所以线程 2 拜访时将会产生异样。
解决这个问题,能够把 INSTANCE 定义成 volatile 的。
private volatile static SimpleSingleton4 INSTANCE;
其实,创立单例的办法有很多,最好的还是动态外部类。
public class SimpleSingleton5 {private SimpleSingleton5() { }
public static SimpleSingleton5 getInstance() {return Inner.INSTANCE;}
private static class Inner {private static final SimpleSingleton5 INSTANCE = new SimpleSingleton5();
}
}
总结
volatile 的底层是通过:store,load 等内存屏障命令,解决 JMM 的可见性和重排序问题的。然而它无奈解决竞争问题,要解决竞争问题须要加锁,或应用 cas 等无锁技术。单例模式不倡议应用双重查看锁,举荐应用动态外部类的形式创立。
彩蛋
应用 volatile 保障线程间的可见性和重排序问题,绝对于 synchronized 等加锁机制更轻量级,然而对性能还是有肯定的耗费,如何优化性能呢?
能够参考 spring 中 DefaultNamespaceHandlerResolver 类的 getHandlerMappings 办法
@Nullable
private volatile Map<String, Object> handlerMappings;
该办法就应用了双重查看锁,能够看到办法外部应用局部变量,首先将实例变量值赋值给该局部变量,而后再进行判断。最初内容先写入局部变量,而后再将局部变量赋值给实例变量。应用局部变量绝对于不应用局部变量,能够进步性能。次要是因为 volatile 变量创建对象时须要禁止指令重排序,这就须要一些额定的操作。
如果这篇文章对您有所帮忙,或者有所启发的话,帮忙关注一下:苏三说技术,或者点赞,转发一下,保持原创不易,您的反对是我后退最大的能源,谢谢。