在之前的文章 深入分析 Synchronized 原理 介绍了 Synchronized是一种锁的机制,存在阻塞和性能的问题,而 volatile 是 java 虚拟机提供的最轻量级的同步机制,volatile 主要提供修饰共享变量赋予 “可见性” 和 “有序性”。从简单的 Demo 引出我们今天的主题 -- volatile。
Demo -- 多线程共享对象 控制执行开关。
public class Demo {
private static boolean switchStatus = false;
public static void main(String[] args) {
new Thread(() -> {
System.out.println("开始工作");
while (!switchStatus) ;
System.out.println("结束工作");
}).start();
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
switchStatus = true;
System.out.println("命令停止工作");
}
}
本意是想通过 switchStatus 作为控制工作线程的开关,但是实际执行后,会发现结果并没有按照预期 输出"结束工作",而是失联了一样停不下来了,在死循环中出不来了。
但是如果在上面的 Demo 进行稍微的修改即可满足预期: private static volatile boolean switchStatus = false; 此时符合预期关闭开关时,工作线程也随之关闭了。接下我会针对这2个现象原理进行解答,为了读者更好的理解,得先引入几个知识点(计算机内存模型、JMM-Java 内存模型)。
为了更好地理解后续 JMM 和 volatile,我们先了解下计算机内存模型,简单地介绍下:
程序执行时,CPU接收到指令 需要进行计算时,读取所需要的数据,会先尝试从 CPU Cache 中获取,若没有再从主内存中获取,计算完成后,将结果写入 CPU Cache ,若没有特殊指令的情况下,会根据操作系统自身定义的时间 一段时间会将 CPU Cache 刷新到主内存中(未被volatile 修饰的普通变量);当然遇到特殊的指令会将 CPU Cache 刷新到主内存中(被volatile 修饰的变量 就是依赖这个特性实现可见性)。
CPU和其他功能部件是通过总线通信的,如果在总线加LOCK#锁,那么在锁住总线期间,其他CPU是无法访问内存,这样一来,效率就比较低了。因此需要进行优化,细化控制锁的粒度,我们只需要保证,对于被多个CPU缓存的同一份数据是一致的就行,所以引入了缓存锁,他的核心机制就是缓存一致性协议。
为了达成数据访问的一致性,需要各个处理器在访问内存时,遵循一些协议,在读写时根据协议来操作,常见的协议有,MSI,MESI,MOSI等等,最常见的就是MESI协议;MESI表示缓存行的四种状态(modify、 Exclusive、Shared、 Invalid)。
如何保证当前处理器的内部缓存、主内存和其他处理器的缓存数据在总线上保持一致的?多处理器总线嗅探。
在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己的缓存值是不是过期了,如果处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据库读到处理器缓存中。
举个例子:
# 初始值
i = 0;
# 线程A 和 线程B同时进行操作
i = i + 1;
首先,执行线程A从主内存中读取到 i=0,到工作内存。然后在工作内存中,赋值 i+1,工作内存就得到 i=1,最后把结果写回主内存。如果是单线程的话,该语句执行是没问题的。但是在多线程的情况下,线程B的本地工作内存和线程A的工作内存读取的时间相同都是 i=0,但是线程A将 i=1写入主内存中,线程B不知情的情况下,也做了 i+1 的操作,此时就出现可见性带来问题了:连续2次的 i=i+1 最终的结果是1。
在之前的文章 深入分析 Synchronized 原理 已经介绍过 原子性、可见性、有序性定义,这里也就不展开说了。
先说结论:依赖于 CPU 缓存一致性协议 和 内存屏障 解决了可见性的问题。
正常来说,volatile 基于缓存一致性协议就应该可以实现可见性(在上面已经介绍过 缓存一致性协议和嗅探技术),但是由于 Java 为了提高性能允许重排序(编译器重排序 和 处理器重排序),因此需要通过内存屏障来防止重排序,来保证每个线程执行的每个指令有一定的顺序性。
java的内存屏障通常所谓的四种即 LoadLoad、StoreStore、 LoadStore、StoreLoad 实际上也是上述两种的组合,完成一系列的屏障和数据同步功能。
java 中 DLC单例模式 大家应该很熟悉了,只不过大家是否有注意到 uniqueInstance 被 volatile 修饰的作用吗? 就是为了防止指令重排。
public class Singleton {
private volatile static Singleton uniqueInstance;
private Singleton() {
}
public static Singleton getInstance() {
if (uniqueInstance != null) {
synchronized (Singleton.class) {
if (uniqueInstance != null) {
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}
初始化一个类,会产生多条汇编指令,总结下来主要执行下面三点:
理想的状态下:1 -> 2 -> 3,但是 Java 为了提高性能允许重排序,可能会将初始化一个类的顺序进行变化,比如:1 -> 3 -> 2,这种情况下就可能会出现NPE,修饰了volatile 防止重排序,避免获取到 uniqueInstance 未初始化完成,导致NPE
最后简单总结下:volatile 在指令之间插入内存屏障 + 缓存一致性协议,保证按照特定顺序执行和某些变量的可见性。volatile 通过 内存屏障通知 CPU 和编译器阻止指令重排优化来维持有序性。
新闻名称:面试官:谈谈你对Volatile的理解吧
URL网址:http://www.mswzjz.cn/qtweb/news39/362639.html
攀枝花网站建设、攀枝花网站运维推广公司-贝锐智能,是专注品牌与效果的网络营销公司;服务项目有等
声明:本网站发布的内容(图片、视频和文字)以用户投稿、用户转载内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。文章观点不代表本网站立场,如需处理请联系客服。电话:028-86922220;邮箱:631063699@qq.com。内容未经允许不得转载,或转载时需注明来源: 贝锐智能