引言

谈到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指令有两个功能:

  1. 对CPU总线和高速缓存加锁,加锁之后执行后面的指令,然后释放锁时将高速缓存中的数据刷新回主内存。
  2. lock会让其他CPU高速缓存中的缓存行失效,其他CPU读取时必须要从主内存加载最新数据。

    简单来说就是lock指令可以实现缓存一致性。通过lock指令的这两个功能,我们就可以很简单的理解当共享变量flag用volatile修饰后,每次更新flag的值都会导致缓存行的数据强制刷新最新值到主内存,volatile变量之前的数据也会被刷新回主内存。同时其他线程必须到主内存读取最新flag的值。这样就实现了共享变量的可见性以及有序性。




    参考资料:

    《深入理解Java虚拟机》

    《Java并发编程的艺术》

Java并发(3)- 聊聊Volatile的更多相关文章

  1. Java并发编程:volatile关键字解析

    Java并发编程:volatile关键字解析 volatile这个关键字可能很多朋友都听说过,或许也都用过.在Java 5之前,它是一个备受争议的关键字,因为在程序中使用它往往会导致出人意料的结果.在 ...

  2. (转)Java并发编程:volatile关键字解析

    转:http://www.cnblogs.com/dolphin0520/p/3920373.html Java并发编程:volatile关键字解析 volatile这个关键字可能很多朋友都听说过,或 ...

  3. 【死磕Java并发】-----深入分析volatile的实现原理

      通过前面一章我们了解了synchronized是一个重量级的锁,虽然JVM对它做了很多优化,而下面介绍的volatile则是轻量级的synchronized.如果一个变量使用volatile,则它 ...

  4. Java 并发编程:volatile的使用及其原理

    Java并发编程系列: Java 并发编程:核心理论 Java并发编程:Synchronized及其实现原理 Java并发编程:Synchronized底层优化(轻量级锁.偏向锁) Java 并发编程 ...

  5. Java并发编程:volatile关键字解析(转载)

    转自https://www.cnblogs.com/dolphin0520/p/3920373.html Java并发编程:volatile关键字解析   Java并发编程:volatile关键字解析 ...

  6. Java并发编程:volatile关键字解析-转

    Java并发编程:volatile关键字解析 转自海子:https://www.cnblogs.com/dayanjing/p/9954562.html volatile这个关键字可能很多朋友都听说过 ...

  7. Java并发编程:volatile关键字解析(学习总结-海子)

    博文地址:Java并发编程:volatile关键字解析

  8. 6、Java并发编程:volatile关键字解析

    Java并发编程:volatile关键字解析 volatile这个关键字可能很多朋友都听说过,或许也都用过.在Java 5之前,它是一个备受争议的关键字,因为在程序中使用它往往会导致出人意料的结果.在 ...

  9. 转:Java并发编程:volatile关键字解析

    Java并发编程:volatile关键字解析 Java并发编程:volatile关键字解析 volatile这个关键字可能很多朋友都听说过,或许也都用过.在Java 5之前,它是一个备受争议的关键字, ...

  10. [转载]Java并发编程:volatile关键字解析

    Java并发编程:volatile关键字解析 volatile这个关键字可能很多朋友都听说过,或许也都用过.在Java 5之前,它是一个备受争议的关键字,因为在程序中使用它往往会导致出人意料的结果.在 ...

随机推荐

  1. 内置函数系列之 map

    map(映射函数)语法: map(函数,可迭代对象) 可以对可迭代对象中的每一个元素,分别执行函数里的操作 # 1.计算每个元素的平方 lst = [1,2,3,4,5] lst_new = map( ...

  2. python实现排序之冒泡排序

    冒泡排序:是将一串无需的数字,排列成有序的.通过相邻的两个数作比较,大的往后移,经过反复的比较,最后得出一串有序的数列. 那么用代码该如何实现? 其实这个问题的思路就是判断每相邻的两个数,进行大小比较 ...

  3. Redis ----------String的操作

    set    key   value 设置key对应的值为String类型的value mset    key   value 一次设置多个 key对应的值 mget    key   value 一 ...

  4. 基于THINKPHP+layui+Ajax无刷新实现图片上传预览

    <fieldset class="layui-elem-field" style="width:500px;margin:50px 0 0 300px;" ...

  5. 安装python虚拟运行环境,linux下轻松切换python2和python3

    一.查询系统采用的python版本 $ python --version Python 3.7.3 系统采用的python版本为3.7.3 以下查询py3和py2的目录: $ which python ...

  6. 430. Flatten a Multilevel Doubly Linked List

    /* // Definition for a Node. class Node { public: int val = NULL; Node* prev = NULL; Node* next = NU ...

  7. Codeforces Round #458C DP

    C. Travelling Salesman and Special Numbers time limit per test 1 second memory limit per test 256 me ...

  8. 購買管理(MM)

    ■購買管理■ [購買伝票]EKKO: ヘッダ EKPO: 明細 EKET: 納入日程行 EKPA: 取引先機能 EKKN: 勘定設定 EKBE: 後続伝票 EKBEH: 削除済み後続伝票履歴 [請求書 ...

  9. TouTiao开源项目 分析笔记13 最后一个订阅号的实现主页面

    1.实现订阅号的基础类 1.1.本地订阅号的Bean类==>MediaChannelBean public class MediaChannelBean implements Parcelabl ...

  10. Android log 引发的血案

    今天调试代码,我打印了一个东西: Log.d("WelcomeActivity", res.str); 结果总是代码执行不到这一行的下一行,程序也没有挂掉.后来,我自己去想各种可能 ...