Java并发(3)- 聊聊Volatile
引言
谈到volatile关键字,大多数开发者都有一定了解,可以说是开发者非常熟悉,深入之后又非常陌生的一个关键字。相当于轻量的synchronized,也叫轻量级锁,与synchronized相比性能上开销较少,同时又具备了可见性、有序性以及部分原子性,是Java并发需中非常重要的一个关键字。这篇文章我们将从volatile底层原理上来深入剖析他是怎么保证可见性、有序性以及部分原子性的,同时也会总结一些volatile关键字的典型应用场景。
volatile的“部分”原子性
所谓原子性,就是说一个操作是一个完整的整体,在其他线程看来这个操作要么未开始,要么已完成,不会看到中间的操作过程,跟事务有点相似。
那为什么说volatile只具有“部分”原子性,因为从本质上来说volatile是不具备原子性的,他修饰的只是单个变量,大部分情况下单个变量的读取和赋值本身就具有原子性,但有一个例外,就是32位Java虚拟机下的long/double型变量操作。
在32位Java虚拟机下,long/double型变量的读写操作会分为两部分,先读写高32位,在读写低32位,或者相反,这样如果没有将变量声明为volatile变量,在多线程读写时就有可能导致结果不可预知,因为对单个long/double型变量的读写并不是一个整体,也就是不具备原子性,只有使用volatile修饰之后,对单个long/double型变量的读写才具备了原子性的特点。在64位Java虚拟机下,long/double型变量读写本身就具有原子性,如果只是为了简单的读写就不需要使用volatile修饰。
需要明白的是volatile仅仅只保证变量的读和写是原子性操作,并不能保证对变量的复合操作也是原子性的,这是需要注意的地方,最为经典的场景就是对单个变量进行自增和自减。
private volatile static int increaseI = 0;
public static void main(String[] args) {
for (int i = 0; i < 100000; i++) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
increaseI++;
}
}, String.valueOf(i));
thread.start();
}
while(Thread.activeCount()>1)
Thread.yield();
System.out.println(increaseI);
}
如果大家经过测试,会发现很多时候,打印出来的结果不是100000。这就是因为volatile修饰的变量只能保证变量的读写是原子性的,而increaseI++是一个复合操作,他可以简单分为:
var = increaseI; //步骤1:将increaseI的值加载到寄存器var
var = var + 1;//步骤2:将寄存器var的值增加1
increaseI = var;//步骤3:将寄存器var的值写入increaseI
volatile只能保证第一步和第三部单个操作的原子性,并不能保证整个自增和自减过程的原子性,也就是说volatile修饰的increaseI++并不是原子操作。下图也可以说明这个问题:

volatile的可见性
关于可见性,在前面的《Java并发(2)- 聊聊happens-before》一文中说过,为了提高操作效率,共享变量的读写都是在线程的本地内存中进行的,当对变量进行更新后,并不会及时将变量的结果刷新回主内存,在多线程环境下,其他线程就不会及时读取到最新的变量值。我们可以从下面的代码来分析这一点。
private static boolean flag = false;
private static void refershFlag() throws InterruptedException {
Thread threadA = new Thread(new Runnable() {
@Override
public void run() {
while (!flag) {
//do something
}
}
});
Thread threadB = new Thread(new Runnable() {
@Override
public void run() {
flag = true;
}
});
DateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
System.out.println("threadA start" + dateFormat.format(new java.util.Date()));
threadA.start();
Thread.sleep(100);
threadB.start();
threadA.join();
System.out.println("threadA end" + dateFormat.format(new java.util.Date()));
}
//threadA start2018/07/25 16:48:41
按正常逻辑来说B线程更新变量flag后,A线程应该马上退出,但实际上很多时候B线程并不会立刻退出,这是因为虚拟机考虑到共享变量没有采用volatile修饰,默认该变量不需要多线程访问,于是做了优化,导致flag共享变量没有及时刷新回主内存,同时其他线程也没有及时去主内存读取的结果。那我们给flag变量加上volatile标示会怎么样呢?
private volatile static boolean flag = false;
//threadA start2018/07/25 16:48:59
//threadA end2018/07/25 16:48:59
可以看到A线程马上退出了,从这点可以看出volatile的可见性。
volatile的有序性
JMM在happens-before规则的基础上保证了单线程和正确同步多线程的有序性,其中就有一条volatile变量规则:对一个volatile变量的写操作happen—before后面对该变量的读操作。
这其中有两点要注意:第一点,针对同一个volatile变量的写、读操作之间才有happens-before关系;第二点,有时间上的先后顺序,必须是写操作happen—before读操作。在《Java并发(2)- 聊聊happens-before》重排序的例子中就很好的说明了volatile禁止重排序的特性。
public class AAndB {
int x = 0;
int y = 0;
int a = 0;
int b = 0;
public void awrite() {
a = 1;
x = b;
}
public void bwrite() {
b = 1;
y = a;
}
}
public class AThread extends Thread{
private AAndB aAndB;
public AThread(AAndB aAndB) {
this.aAndB = aAndB;
}
@Override
public void run() {
super.run();
this.aAndB.awrite();
}
}
public class BThread extends Thread{
private AAndB aAndB;
public BThread(AAndB aAndB) {
this.aAndB = aAndB;
}
@Override
public void run() {
super.run();
this.aAndB.bwrite();
}
}
private static void testReSort() throws InterruptedException {
AAndB aAndB = new AAndB();
for (int i = 0; i < 10000; i++) {
AThread aThread = new AThread(aAndB);
BThread bThread = new BThread(aAndB);
aThread.start();
bThread.start();
aThread.join();
bThread.join();
if (aAndB.x == 0 && aAndB.y == 0) {
System.out.println("resort");
}
aAndB.x = aAndB.y = aAndB.a = aAndB.b = 0;
}
System.out.println("end");
}
当A线程和B线程都出现了重排序可能会打印出resort,但将变量都变为volatile变量后便不会再出现这种状况。
volatile的两个典型使用场景
1 用来标示状态量。
状态量标示就是通过一个boolean类型变量来判断逻辑是否需要执行。就是上面volatile的可见性中的代码:
Thread threadA = new Thread(new Runnable() {
@Override
public void run() {
while (!flag) {
//do something
}
}
});
Thread threadB = new Thread(new Runnable() {
@Override
public void run() {
flag = true;
}
});
如果使用synchronized或者锁写法上将会比较复杂,但如果用volatile来修饰变量就很好的解决了这个问题,保证了状态量的及时刷新回主内存同时其他线程也会强制更新。
2 double-check问题
double-check问题应该是volatile使用最多的场景了。如下代码所示:
public class DoubleCheck {
private volatile static DoubleCheck instance = null;
private DoubleCheck() {
}
public static DoubleCheck getInstance() {
if (null == instance) { //步骤一
synchronized (DoubleCheck.class) {
if (null == instance) { //步骤二
instance = new DoubleCheck(); //步骤三
}
}
}
return instance;
}
public static void main(String[] args) throws InterruptedException {
DoubleCheck doubleCheck = DoubleCheck.getInstance();
}
}
代码中步骤三并不是原子性的,和之前的自增有点类似,可以分为三步:
3.1 为DoubleCheck分配内存地址 alloc memory address
3.2 初始化对象DoubleCheck init DoubleCheck
3.3 将引用地址指向instance instance > memory address
在CPU看来3.2和3.3并不存在依赖关系,是有可能会重排序的,如果将3.2和3.3重排序:

线程2在步骤一时判断instance不为空的情况下,实际上对象并没有初始化,3.2并没有执行。导致接下来使用对象发生错误。此时使用volatile修饰instance变量就可以防止3.2和3.3重排序,这样就保证了多线程访问时代码的正确性。
我们可以查看到汇编代码中在使用volatile关键字后在步骤三中多了lock指令来保证当前执行的有序性:
不使用volatile:

使用volatile

volatile背后的原理
在DoubleCheck的汇编代码中我们看到加了volatile关键字后汇编代码中多了一行lock指令,那么这个指令代表什么意思呢?
lock指令有两个功能:
- 对CPU总线和高速缓存加锁,加锁之后执行后面的指令,然后释放锁时将高速缓存中的数据刷新回主内存。
- lock会让其他CPU高速缓存中的缓存行失效,其他CPU读取时必须要从主内存加载最新数据。
简单来说就是lock指令可以实现缓存一致性。通过lock指令的这两个功能,我们就可以很简单的理解当共享变量flag用volatile修饰后,每次更新flag的值都会导致缓存行的数据强制刷新最新值到主内存,volatile变量之前的数据也会被刷新回主内存。同时其他线程必须到主内存读取最新flag的值。这样就实现了共享变量的可见性以及有序性。
参考资料:
《深入理解Java虚拟机》
《Java并发编程的艺术》
Java并发(3)- 聊聊Volatile的更多相关文章
- Java并发编程:volatile关键字解析
Java并发编程:volatile关键字解析 volatile这个关键字可能很多朋友都听说过,或许也都用过.在Java 5之前,它是一个备受争议的关键字,因为在程序中使用它往往会导致出人意料的结果.在 ...
- (转)Java并发编程:volatile关键字解析
转:http://www.cnblogs.com/dolphin0520/p/3920373.html Java并发编程:volatile关键字解析 volatile这个关键字可能很多朋友都听说过,或 ...
- 【死磕Java并发】-----深入分析volatile的实现原理
通过前面一章我们了解了synchronized是一个重量级的锁,虽然JVM对它做了很多优化,而下面介绍的volatile则是轻量级的synchronized.如果一个变量使用volatile,则它 ...
- Java 并发编程:volatile的使用及其原理
Java并发编程系列: Java 并发编程:核心理论 Java并发编程:Synchronized及其实现原理 Java并发编程:Synchronized底层优化(轻量级锁.偏向锁) Java 并发编程 ...
- Java并发编程:volatile关键字解析(转载)
转自https://www.cnblogs.com/dolphin0520/p/3920373.html Java并发编程:volatile关键字解析 Java并发编程:volatile关键字解析 ...
- Java并发编程:volatile关键字解析-转
Java并发编程:volatile关键字解析 转自海子:https://www.cnblogs.com/dayanjing/p/9954562.html volatile这个关键字可能很多朋友都听说过 ...
- Java并发编程:volatile关键字解析(学习总结-海子)
博文地址:Java并发编程:volatile关键字解析
- 6、Java并发编程:volatile关键字解析
Java并发编程:volatile关键字解析 volatile这个关键字可能很多朋友都听说过,或许也都用过.在Java 5之前,它是一个备受争议的关键字,因为在程序中使用它往往会导致出人意料的结果.在 ...
- 转:Java并发编程:volatile关键字解析
Java并发编程:volatile关键字解析 Java并发编程:volatile关键字解析 volatile这个关键字可能很多朋友都听说过,或许也都用过.在Java 5之前,它是一个备受争议的关键字, ...
- [转载]Java并发编程:volatile关键字解析
Java并发编程:volatile关键字解析 volatile这个关键字可能很多朋友都听说过,或许也都用过.在Java 5之前,它是一个备受争议的关键字,因为在程序中使用它往往会导致出人意料的结果.在 ...
随机推荐
- 【Ecshop】商品数据采集扩展
一个自用的Ecshop商品数据采集程序 ->到此下载
- [Azure Storage]使用Java上传文件到Storage并生成SAS签名
Azure官网提供了比较详细的文档,您可以参考:https://azure.microsoft.com/en-us/documentation/articles/storage-java-how-to ...
- 生成heap dump
在查看内存泄露以及对内存问题中,要dump出当前内存堆存储快照,便于分析.有几种方法可以做,简介如下 一.intellij IDEA 由于我用的是intellij IDEA,所以没有介绍Eclipse ...
- Android 布局跟着NAVIGATION_BAR 重新布局
要想让自己的布局跟着NAVIGATION_BAR 的变化重新布局,就不要设置 SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION. 这个FLAG. 这个flag 设置之后,你的 ...
- 开启虚拟机所报的错误:VMware Workstation cannot connect to the virtual machine. Make sure you have rights to run the program, access all directories the program uses, and access all directories for temporary fil
当我们开启虚拟机时出现错误: VMware Workstation cannot connect to the virtual machine. Make sure you have rights t ...
- 10.bootstrap分页,点击哪个分页号,哪个分页号就active
1.分页,点击哪个分页号,哪个分页号就active <nav> <ul class="pagination"> <li><a href=& ...
- laravel5.5容器
目录 1. 比较典型的例子就是 cache 缓存 2. 容器顾名思义,其实就是完成存取过程 2.1 绑定过程 简单绑定 绑定单例 绑定实例 绑定初始数据 2.2 解析过程 容器主要是为了实现控制反转, ...
- laravel5.5中间件
目录 1. 中间件知识 1. artisan 命令 2. 文件内容 3. 前置中间件和后置中间件 4. 使用中间件 2. 控制器中间件 1. 中间件知识 1. artisan 命令 php artis ...
- 通过学习制作长微博工具来了解水印的制作,及EditText中的内容在图片中换行显示
长微博工具非常有用,140字的要求可能阻止你写更多的内容,于是长微博工具应运而生,虽然网上有很多长微博工具,但是我都不是很满意,所以自己想做一个,通过做这个长微博工具,我学习到了很多东西,有两个难点, ...
- Linux常用命令及工具记录(持续更新)
一.命令 convmv 作用:文件名的编码转换 安装:sudo apt-get install convmv 使用:convmv * -f gbk -t utf8 --notest c ...