因为我说:volatile 是轻量级的 synchronized,面试官让我回去等通知!
因为我说:volatile 是轻量级的 synchronized,面试官让我回去等通知!
volatile 是并发编程的重要组成部分,也是面试常被问到的问题之一。不要向小强那样,因为一句:volatile 是轻量级的 synchronized,而与期望已久的大厂失之交臂。
volatile 有两大特性:保证内存的可见性和禁止指令重排序。那什么是可见性和指令重排呢?接下来我们一起来看。
内存可见性
要了解内存可见性先要从 Java 内存模型(JMM)说起,在 Java 中所有的共享变量都在主内存中,每个线程都有自己的工作内存,为了提高线程的运行速度,每个线程的工作内存都会把主内存中的共享变量拷贝一份进行缓存,以此来提高运行效率,内存布局如下图所示:

但这样就会产生一个新的问题,如果某个线程修改了共享变量的值,其他线程不知道此值被修改了,就会发生两个线程值不一致的情况,我们用代码来演示一下这个问题。
public class VolatileExample {
// 可见性参数
private static boolean flag = false;
public static void main(String[] args) {
new Thread(() -> {
try {
// 暂停 0.5s 执行
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
flag = true;
System.out.println("flag 被修改成 true");
}).start();
// 一直循环检测 flag=true
while (true) {
if (flag) {
System.out.println("检测到 flag 变为 true");
break;
}
}
}
}
以上程序的执行结果如下:
flag 被修改成 true
我们会发现永远等不到 检测到 flag 变为 true 的结果,这是因为非主线程更改了 flag=true,但主线程一直不知道此值发生了改变,这就是内存不可见的问题。
内存的可见性是指线程修改了变量的值之后,其他线程能立即知道此值发生了改变。
我们可以使用 volatile 来修饰 flag,就可以保证内存的可见性,代码如下:
public class VolatileExample {
// 可见性参数
private static volatile boolean flag = false;
public static void main(String[] args) {
new Thread(() -> {
try {
// 暂停 0.5s 执行
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
flag = true;
System.out.println("flag 被修改成 true");
}).start();
// 一直循环检测 flag=true
while (true) {
if (flag) {
System.out.println("检测到 flag 变为 true");
break;
}
}
}
}
以上程序的执行结果如下:
检测到 flag 变为 true
flag 被修改成 true
指令重排
指令重排是指在执行程序时,编译器和处理器常常会对指令进行重排序,已到达提高程序性能的目的。
比如小强要去图书馆还上次借的书,随便再借一本新书,而此时室友小王也想让小强帮他还一本书,未发生指令重排的做法是,小强先把自己的事情办完,再去办室友的事,这样显然比较浪费时间,还有一种做法是,他先把自己的书和小王的书一起还掉,再给自己借一本新书,这就是指令重排的意义。
但指令重排不能保证指令执行的顺序,这就会造成新的问题,如下代码所示:
public class VolatileExample {
// 指令重排参数
private static int a = 0, b = 0;
private static int x = 0, y = 0;
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < Integer.MAX_VALUE; i++) {
Thread t1 = new Thread(() -> {
// 有可能发生指令重排,先 x=b 再 a=1
a = 1;
x = b;
});
Thread t2 = new Thread(() -> {
// 有可能发生指令重排,先 y=a 再 b=1
b = 1;
y = a;
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("第 " + i + "次,x=" + x + " | y=" + y);
if (x == 0 && y == 0) {
// 发生了指令重排
break;
}
// 初始化变量
a = 0;
b = 0;
x = 0;
y = 0;
}
}
}
以上程序执行结果如下所示:

可以看出执行到 48526 次时发生了指令重排,y 就变成了非正确值 0,显然这不是我们想要的结果,这个时候就可以使用 volatile 来禁止指令重排。
以上我们通过代码的方式演示了指令重排和内存可见性的问题,接下来我们用代码来演示一下 volatile 同步方式的问题。
volatile 非同步方式
首先,我们使用 volatile 修饰一个整数变量,再启动两个线程分别执行同样次数的 ++ 和 -- 操作,最后发现执行的结果竟然不是 0,代码如下:
public class VolatileExample {
public static volatile int count = 0; // 计数器
public static final int size = 100000; // 循环测试次数
public static void main(String[] args) {
// ++ 方式
Thread thread = new Thread(() -> {
for (int i = 1; i <= size; i++) {
count++;
}
});
thread.start();
// -- 方式
for (int i = 1; i <= size; i++) {
count--;
}
// 等所有线程执行完成
while (thread.isAlive()) {}
System.out.println(count); // 打印结果
}
}
以上程序执行结果如下:
1065
可以看出,执行结果并不是我们期望的结果 0,我们把以上代码使用 synchronized 改造一下:
public class VolatileExample {
public static int count = 0; // 计数器
public static final int size = 100000; // 循环测试次数
public static void main(String[] args) {
// ++ 方式
Thread thread = new Thread(() -> {
for (int i = 1; i <= size; i++) {
synchronized (VolatileExample.class) {
count++;
}
}
});
thread.start();
// -- 方式
for (int i = 1; i <= size; i++) {
synchronized (VolatileExample.class) {
count--;
}
}
// 等所有线程执行完成
while (thread.isAlive()) {}
System.out.println(count); // 打印结果
}
}
这次执行的结果变成了我们期望的值 0。
这说明 volatile 只是轻量级的线程可见方式,并不是轻量级的同步方式,所以并不能说 volatile 是轻量级的 synchronized,终于知道为什么面试官让我回去等通知了。
volatile 使用场景
既然 volatile 只能保证线程操作的可见方式,那它有什么用呢?
volatile 在多读多写的情况下虽然一定会有问题,但如果是一写多读的话使用 volatile 就不会有任何问题。volatile 一写多读的经典使用示例就是 CopyOnWriteArrayList,CopyOnWriteArrayList 在操作的时候会把全部数据复制出来对写操作加锁,修改完之后再使用 setArray 方法把此数组赋值为更新后的值,使用 volatile 可以使读线程很快的告知到数组被修改,不会进行指令重排,操作完成后就可以对其他线程可见了,核心源码如下:
public class CopyOnWriteArrayList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
private transient volatile Object[] array;
final void setArray(Object[] a) {
array = a;
}
//...... 忽略其他代码
}
总结
本文我们通过代码的方式演示了 volatile 的两大特性,内存可见性和禁止指令重排,使用 ++ 和 -- 的方式演示了 volatile 并非轻量级的同步方式,以及 volatile 一写多读的经典使用案例 CopyOnWriteArrayList。
更多 Java 原创文章,请关注我微信公众号 「Java中文社群」
因为我说:volatile 是轻量级的 synchronized,面试官让我回去等通知!的更多相关文章
- java多线程关键字volatile、lock、synchronized
--------------------- 本文来自 旭日Follow_24 的CSDN 博客 ,全文地址请点击:https://blog.csdn.net/xuri24/article/detail ...
- 面试官没想到一个Volatile,我都能跟他扯半小时
点赞再看,养成习惯,微信搜索[三太子敖丙]关注这个互联网苟且偷生的工具人. 本文 GitHub https://github.com/JavaFamily 已收录,有一线大厂面试完整考点.资料以及我的 ...
- 最后一面挂在volatile关键字上,面试官:重新学学Java吧!
最后一面挂在volatile关键字上,面试官:重新学学Java吧! 为什么会有volatile关键字? volatile: 易变的; 无定性的; 无常性的; 可能急剧波动的; 不稳定的; 易恶化的; ...
- 反制面试官 | 14张原理图 | 再也不怕被问 volatile!
反制面试官 | 14张原理图 | 再也不怕被问 volatile! 悟空 爱学习的程序猿,自主开发了Java学习平台.PMP刷题小程序.目前主修Java.多线程.SpringBoot.SpringCl ...
- 面试官最爱的 volatile 关键字,这些问题你都搞懂了没?
前言 volatile相关的知识点,在面试过程中,属于基础问题,是必须要掌握的知识点,如果回答不上来会严重扣分的哦. volatile关键字基本介绍 volatile可以看成是synchronized ...
- 面试官:说一下Synchronized底层实现,锁升级的具体过程?
面试官:说一下Synchronized底层实现,锁升级的具体过程? 这是我去年7,8月份面试的时候被问的一个面试题,说实话被问到这个问题还是很意外的,感觉这个东西没啥用啊,直到后面被问了一波new O ...
- 深度剖析Java的volatile实现原理,再也不怕面试官问了
上篇文章我们讲了synchronized的用法和实现原理,我们总爱说synchronized是重量级锁,volatile是轻量级锁.为什么volatile是轻量级锁,体现在哪些方面?以及volatil ...
- 面试官最爱的volatile关键字
在Java相关的岗位面试中,很多面试官都喜欢考察面试者对Java并发的了解程度,而以volatile关键字作为一个小的切入点,往往可以一问到底,把Java内存模型(JMM),Java并发编程的一些特性 ...
- Java面试官最爱问的volatile关键字
在Java的面试当中,面试官最爱问的就是volatile关键字相关的问题.经过多次面试之后,你是否思考过,为什么他们那么爱问volatile关键字相关的问题?而对于你,如果作为面试官,是否也会考虑采用 ...
随机推荐
- markdown直接粘贴截图
通过代码方式 cmd markdown粘贴截图 https://www.jianshu.com/p/ae048b5090f8
- SHELL小练习
1.SHELL编程服务器IP修改脚本 脚本实现动态IP修改: 脚本实现静态IP修改: 实现IP地址输入判断正确性: IP地址修改成功判断&回滚: 2.SHELL编程Tomcat多实例管理脚本( ...
- Linux下文件 ~/.bashrc 和 ~/.bash_profile 和 /etc/bashrc 和 /etc/profile 的区别 | 用户登录后加载配置文件的顺序
转自 https://blog.csdn.net/secondjanuary/article/details/9206151 文件说明: /ect/profile 此文件为系统的每个用户设置环境信息, ...
- Java IO: 管道
原文链接 作者: Jakob Jenkov 译者: 李璟(jlee381344197@gmail.com) Java IO中的管道为运行在同一个JVM中的两个线程提供了通信的能力.所以管道也可以作为 ...
- 吴裕雄--天生自然HTML学习笔记:HTML 简介
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title> ...
- UnitTest测试框架-操作步骤
一.UnitTest 1. TestCase 说明:测试用例 1.新建类并集成unittest.TestCase 2. TestSuite 说明:测试套件(多条用例) 方法: 1. 实例化 suite ...
- 通过virt-manager给Windowsxp系统配置virtio驱动
在虚拟机的detail上添加一个硬件设备. 下载virtio.iso文件,我使用的版本126,具体的virtio驱动放到了10.2上的itest的目录中,使用可以找. 在配置中,添加virtio硬盘. ...
- Flask admin Flask login 整合模板
项目地址: https://github.com/WES6/supflask Flask admin 官方文档: https://flask-admin.readthedocs.io/en/lates ...
- MySQL安装和常用命令
一.安装MySQL groupadd mysqluseradd -r -g mysql mysqlgroups mysqlfind / -name mysql | xargs rm -rfwget h ...
- Nginx+PHP配置错误,日志:[error] 24324#0: *31 FastCGI sent in stderr: "Primary script unknown" while reading response header from upstream
一.问题现象 1.安装nginx.php.php-fpm后,浏览器访问php报错,“File not found”: 二.问题排查 1.检查nginx.php-fpm服务是否正常启动,均正常启动: 2 ...