Java并发编程之volatile的理解

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java并发编程之volatile的理解相关的知识,希望对你有一定的参考价值。

 Java并发编程之volatile关键字的理解 

        Java中每个线程都有自己的工作内存,类比于处理器的缓存,线程的工作内存中保存了被该线程使用到的变量的主内存的拷贝。线程读写变量都是直接在自己的工作内存中进行的,而何时刷新数据(指将修改的结果更新到主存或者把主存的变量读取覆盖掉工作内存中的值)是不确定的。

 

       volatile关键字是修饰字段的关键字,貌似是JDK1.5之后才有的,在多线程编程中,很大的几率会用到这个关键字,volatile修饰变量后该变量有这么一种效果:线程每一次读该变量都是直接从主存(JVM的主存)中读,而不是从线程的工作内存中;每一次写该变量都会同时写到主存中,而不仅仅是线程的工作内存中。因此一开头说的"何时刷新数据是不确定的"只适用于非volatile变量。

 

JVM对volatile变量有两个保证:

  1. 可见性。这个上面也大概解释了,就是某个线程改变了值,另一个线程立马就能读到改变后的值,容易理解。

  2. Happens-Before。有两点要说明:

  • 线程在写volatile变量时,若对一个普通变量的写在对该volatile变量的写之前,那么对该普通变量的写也将会被写到主存,而不仅仅是工作内存;线程在读volatile变量时,若对一个普通变量的读在对该volatile变量的读之后,那么对该普通变量的读将会先和主存同步,再读取,而不是直接从工作内存中读。例如  下载

Java代码 

  1. Thread A:  

  2. object.nonVolatileVar = 1;  // stepA1  

  3. object.volatileVar = object.volatileVar + 1// stepA2  

  4.   

  5. Thread B:  

  6. int volatileVar = object.volatileVar; // stepB1  

  7. int nonVolatile = object.nonVolatileVar; // stepB2  

 线程A执行到stepA2,当要把volatileVar的新值写到主存时,nonVolatileVar的新值也会被刷到主存中;线程B执行到stepB1时,当要从主存中读object.volatileVar时,object.nonVolatileVar也会被一起读进工作内存,因此当线程 B执行到StepB2时,是可以拿到nonVolatileVar 的最新值的。这种特性其实蛮有用的:当一个线程有多个volatile变量时,可以根据这个特性减少volatile变量(通过变量的读、写顺序),可以达到和多个volatile变量同样的效果。

 

  • 对volatile变量的读写指令是不会被JVM重排序的。读/写之前或之后的其他指令可以重排序,但对volatile变量的读/写指令和其它指令的相对顺序是不会改变的。例如  下载

Java代码 

  1. object.nonVolatile1 = 123;  //instruction1  

  2. object.nonVolatile2 = 456;  //instruction2  

  3. object.nonVolatile3 = 789// //instruction3  

  4.   

  5. object.volatile     = true//a volatile variable, //instruction4  

  6.   

  7. int value1 = sharedObject.nonVolatile4; //instruction5  

  8. int value2 = sharedObject.nonVolatile5;  //instruction6  

  9. int value3 = sharedObject.nonVolatile6;   //instruction7  

 由于JVM发现instruction1、instruction2、instruction3没有前后作用关系,因此jvm有可能会重排序这三条指令,instruction456也是如此,但是中间有个volatile变量的读。因此instruction123是不会被重排序到instruction4后面去的,同样instruction456也不会重排序到instruction4前面去的,他们的相对顺序不会变。

 

一个很常见的用volatile的例子就是单例模式(某种线程安全的写法):  下载

 

Java代码 

  1. public class Singleton {  

  2.       

  3.     private volatile static Singleton instance;  

  4.       

  5.     public static Singleton getInstance() {  

  6.         if(instance == null) { //step1  

  7.             synchronized (Singleton.class) { //step2  

  8.                 if(instance == null) { //step3  

  9.                     instance = new Singleton(); // step4  

  10.                 }  

  11.             }  

  12.         }  

  13.         return instance;  

  14.     }  

  15.     private Singleton(){}  

  16. }  

  这里的isntance如果不用volatile修饰,那么这个单例就是非多线程安全的,知道synchronized有可见性保证的人可能会问:为什么这里用了synchronized还需要用volatile修饰?确实,这里两者都保证了可见性,但是这里用volatile不是因为可见性的原因,而是因为指令重排序的原因:首先要知道一点的就是new一个对象时有三步(伪码):  下载

 

Java代码 

  1. memory = allocate();   //1:分配对象的内存空间  

  2. ctorInstance(memory);  //2:初始化对象  

  3. instance = memory;     //3:设置instance指向刚分配的内存地址  

 而这三条指令肯定都是同一个线程执行,根据intra-thread semantic(intra-thread semantics保证重排序不会改变单线程内的程序执行结果),这三条指令是可以重排序成下面这样的:

Java代码 

  1. memory = allocate();   //1:分配对象的内存空间  

  2. instance = memory;     //3:设置instance指向刚分配的内存地址  

  3. ctorInstance(memory);  //2:初始化对象  

 那么上面的单例就有问题了。假设不幸上述重排序发生了,那么初始化对象的线程正好设置了instance = memory(即instance已经不为null了)但是instance还没被初始化时,另一个线程跑到step1,发现instance不为null,然后直接把instance拿去用了,后面自然就会出现各种问题,因为对象根本还没被初始化。用了volatile修饰后,上面所说的重排序就被禁止了。 

 

java.util.concurrent包下用到volatile的地方数不胜数,比如java.util.concurrent.FutureTask<T>中就有使用到volatile的happens-before原则:: 下载

技术分享
 


技术分享

 可以看到有两个变量state, callable都需要保证其可见性, 但是这里只用volatile修饰其中一个,而通过写的顺序来保证不被volatile修饰的那个变量的可见性。

 

书上看到的:“除了volatile外,synchronized和final也能实现可见性。synchronized的可见性:在离开synchronized代码块前,必须先把变量同步到主存中。final的可见性:被final修饰的字段在构造器中一旦完成初始化,并且构造器没有把this引用传递出去,那在其他线程中就能看到这个值”。


以上是关于Java并发编程之volatile的理解的主要内容,如果未能解决你的问题,请参考以下文章

音视频开发之旅(52) - Java并发编程 之内存模型与volatile

深入理解Java并发编程之核心原理概念

深入理解Java并发编程之核心原理概念

Java并发编程之volatile的应用

java并发编程--深入理解volatile关键字

Java并发编程之volatile变量