谈到volatile关键字,大多数开发者都有必定了解,能够说是开发者很是熟悉,深刻以后又很是陌生的一个关键字。至关于轻量的synchronized,也叫轻量级锁,与synchronized相比性能上开销较少,同时又具有了可见性、有序性以及部分原子性,是Java并发需中很是重要的一个关键字。这篇文章咱们将从volatile底层原理上来深刻剖析他是怎么保证可见性、有序性以及部分原子性的,同时也会总结一些volatile关键字的典型应用场景。java
所谓原子性,就是说一个操做是一个完整的总体,在其余线程看来这个操做要么未开始,要么已完成,不会看到中间的操做过程,跟事务有点类似。编程
那为何说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++是一个复合操做,他能够简单分为:app
var = increaseI; //步骤1:将increaseI的值加载到寄存器var var = var + 1;//步骤2:将寄存器var的值增长1 increaseI = var;//步骤3:将寄存器var的值写入increaseI
volatile只能保证第一步和第三部单个操做的原子性,并不能保证整个自增和自减过程的原子性,也就是说volatile修饰的increaseI++并非原子操做。下图也能够说明这个问题:ide
关于可见性,在前面的《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的可见性。优化
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变量后便不会再出现这种情况。
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
在DoubleCheck的汇编代码中咱们看到加了volatile关键字后汇编代码中多了一行lock指令,那么这个指令表明什么意思呢?
lock指令有两个功能: