当前位置:网站首页>Volatile:JVM 我警告你,我的人你别乱动
Volatile:JVM 我警告你,我的人你别乱动
2022-08-09 16:36:00 【肥肥技术宅】
Volatile 算是一个面试中的高频问题了。我们都知道 Volatile 有两个作用:
- 禁止指令重排
- 保证内存可见
指令重排序
指令重排序的问题,基本上都是通过 DCL 问题来考察。
DCL,Double Check Look
面试中通常会是下面这种情景:
面试官:用过单例吗?
你:用过。
面试官:如何实现一个线程安全的懒汉式单例
你:DCL。
面试官:DCL 可以保证线程绝对安全吗?
你:加 Volatile。
面试官满意的点点头。通常情况下,面试中这个问题聊到这里也就结束了。
但这个问题,还有一些可挖掘的内容。我们顺着单例的代码继续往下挖:
public class Singleton { private static volatile Singleton instance = null; private Singleton() { } public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; }}
如果不加 Volatile,会有什么问题呢?问题就出现在下面这行代码:
instance = new Singleton();
上面这行代码看起来也平平无奇呀,就是一个赋值操作,还能整什么幺蛾子呢?我们只写了一行代码,但 JVM 则需要做好几步操作。那 JVM 究竟干了啥呢?大概也许可能差不多就是把大象给放冰箱里了。
Java 代码中的一条赋值语句,到了 JVM 指令层面大概分三步:
- 分配一块内存空间
- 初始化
- 返回内存地址
下面通过字节码来一探究竟,为了简化问题,我们替换成下面的代码:
Object o = new Object();
编译以后,通过 javap -v 命令,或者 IDEA 中的 JClassLib 插件可以看到如下图所示的内容:
通过上面的字节码信息,可以更加清楚的看到上面提到的那三个步骤:
- new 用来分配一块内存空间
- invokspecial 调用了 Object 的 init() 方法,做了初始化
- astore_1 就是将 o 指向了 Object 实例对象的内存地址,完成赋值
dup 指令会做一些入栈操作,跟我们要讨论的问题关系不大,这里可以先忽略。
到这里,问题就比较明了了。重排的问题会发生在第 2 和 3 步。因为先初始化还是先把对象的内存地址赋值给 o,并没有必然的前后制约关系。因此,这类的指令在某些情况下会被重排序。
单线程下,这种重排序完全没有问题。但是多线程的场景下,就有可能出问题:A 线程进入到 instance = new Singleton(); 后,由于指令重排,在 init 之前,将地址给了 o。此时 B 线程来了,发现 instance 不为 null,于是直接拿去用了,然而此时 instance 并没有初始化,只是个半成品。所以,当 B 拿到 instance 进行操作的时候就会出现问题了。
因此,instance 需要使用 volatile 来修饰,从而禁止进行指令重排。
到这里,你可能要说了,我用单例不加 volatile,这么长时间了也没遇到你说的重排序问题。你怎么证明「重排序」的存在呢?好问题,下面咱们通过一个小例子来验证一下重排序是否真的存在。
private static int x = 0;private static int y = 0;private static int a = 0;private static int b = 0;public static void main(String[] args) throws InterruptedException { int i = 0; while (true) { i++; x = 0; y = 0; a = 0; b = 0; Thread one = new Thread(() -> { a = 1; x = b; }); Thread two = new Thread(() -> { b = 1; y = a; }); one.start(); two.start(); one.join(); two.join(); if(x == 0 && y == 0) { log.info("第 {} 次,x = {}, y = {}", i, x, y); break; } }}
代码很简单,就是几个赋值操作,但却很巧妙。x、y、a、b 初始都为 0,两个线程分别给 a、x 和 b、y 赋值,线程 one 先让 a = 1,然后再让 x = b;two 线程先让 b = 1,然后再让 y = a。
假如不发生重排序,那么以上程序只会有下面六种可能:
每一列,从上到下代表代码执行的顺序。
也就是说,在没有重排序的情况下,不可能出现 x、y 同时为 0 的情况。而如果 x、y 同时为 0 了,那么一定是出现了下面六种情况中的一种,既发生了重排。
每一列,从上到下代表代码执行的顺序。
运行程序,经过漫长的等待,得到了如下的输出:
可以看到,在执行了五十多万次以后,我们终于捕捉到了一次重排序。发生这种情况的几率很低,所以你就算没有用 volatile 大概率不会有问题,但我们在今后还是要合理的使用 volatile。
内存可见性
聊完指令重排,接下来聊聊内存可见。这次我们直接上代码:
private static boolean flag = true;private static void justRun() { System.out.println("Thread One Start"); while (flag) {} System.out.println("Thread One End");}public static void main(String[] args) throws InterruptedException { new Thread(() -> justRun(), "Thread One").start(); TimeUnit.SECONDS.sleep(1); flag = false;}
代码很简单,主线程内开启一个子线程,子线程中一个 while 循环,当 flag 为 false 时,结束循环。flag 初始值为 true,一秒钟后,被主线程设置为 false。
按照上面这个逻辑,子线程应该会在程序启动一秒后停止。然而,当你运行程序后会发现,这个程序就像吃了炫迈一样,根本停不下来。
这说明主线程对 flag 的修改,子线程并没有感知到。我们修改一下程序:
private static volatile boolean flag = true;
为 flag 加上 volatile 修饰符,再次运行,你会发现程序运行后,很快(大概一秒钟)就停止了。这是为啥?是炫迈的药劲儿过了吗?
哈哈,当然不是。为了更好的性能,线程都有自己的缓存(CPU 中的高速缓存),我们称之为工作内存或者本地内存。还有一块公共内存,我们叫它主从吧。它们的结构大致如下图所示:
主存中定义了一个 flag 变量,每个线程读取它的时候,为了更好的性能会在线程本地缓存一份它的副本。读取的时候也是优先读取本地副本的值。当 flag 被 volatile 修饰后,每次被修改,都会让其他线程中的副本失效,从而必须去主存中读取最新的值。所以,在使用了 volatile 后,子线程能够立即感知到 flag 的变化,从而停止。
上图简化了线程(CPU)的缓存结构,其完整结构如下图所示:
现代 CPU 共有三级缓存,分别为:L1、L2 和 L3。CPU 中的每个核心都有自己的 L1 和 L2,而一颗 CPU 中的多个核心会共享 L3。
总结
Volatile 的意思是,易变的,动荡不定的,反复无常的。volatile 的作用就是告诉 JVM,被我修饰的变量它非常善变,你要给我盯好了,一旦有风吹草动要立马通知大家;另外,你不要自作聪明的调整它的位置(为了性能重排序),它可是说翻脸就翻脸的主儿。
最后,留一个小问题:内存可见性的那个程序中,就算 flag 没有被 volatile 修饰,线程顶多不是第一时间读到 flag 的修改,但也不应该一直读不到呀,这是为啥?这太反直觉了!
边栏推荐
猜你喜欢
《.NET物联网从零开始》系列
智能家居控制系统的功能和特点
kafka 通过 jdbc 从oracle抓取数据
B43 - 基于STM32单片机的自动视力检测仪
Axure实现表格带滚动条
What is control board custom development?
110+ public professional datasets summarized
面试中老生常谈的MySQL问答集锦夯实基础
Account opening requirements and exemptions for special futures such as crude oil
AlphaControls 控件 TsPanel TsGroupBox 块与组的结合
随机推荐
Axure实现表格带滚动条
Account opening requirements and exemptions for special futures such as crude oil
Lagrange插值公式matlab实现
【教程3】疯壳·ARM功能手机-整板资源介绍
HR to get the entry date RP_GET_HIRE_DATE
JMeter笔记6 | JMeter录制(配置代理)
一键生成 API 文档的妙招
面试官:Redis 大 key 要如何处理?
ceph2
电子产品硬件开发中存在的问题
生产者-消费者线程模型学习
B48 - 基于51单片机的学生管理门禁系统设计
kubernetes之helm简介、安装、配置
LeetCode 413.等差数列划分
字符设备的注册
央企施工企业数字化转型的灵魂是什么
谭中意:你知道 “开源女王” 是谁吗?
我不写单元测试,被批了
PGSQL backup tool, which is better?
110+ public professional datasets summarized