白话讲述Java中volatile关键字

1、由一段代码引出的问题

  首先咱们先来看这样一段代码:html

 1 public class VolatileThread implements Runnable{
 2 
 3     private boolean flag = true;
 4 
 5     @Override
 6     public void run() {
 7         System.out.println("子线程开始执行...");
 8         while(flag){
 9         }
10         System.out.println("子线程执行结束...");
11     }
12     public void setFlag(boolean flag) {
13         this.flag = flag;
14     }
15 }

 

 1 public class VolatileThreadMain {
 2     public static void main(String[] args) throws InterruptedException {
 3         VolatileThread volatileThread = new VolatileThread();
 4         Thread thread = new Thread(volatileThread);
 5         thread.start();
 6         Thread.sleep(3000);
 7         volatileThread.setFlag(false);
 8         System.out.println("flag改成false");
 9         Thread.sleep(1000);
10         System.out.println(volatileThread.flag);
11     }
12 }

  运行结果:java

 

结果分析:算法

从控制台看出,主线程已经将VolatileThread实例中的flag变量更改成false,按常理来讲while(flag)进行判断的时候,读取flag为false应该中止循环而后打印出“子线程执行结束...”,随后程序结束。可是在这里程序却一直不能中止,说明程序一直在循环之中没出来,也就说while(flag)读取到的flag值一直是true,即便主线程已经将flag改成了false。是什么缘由形成这么奇怪的现象呢?想要弄清楚这一点,咱们有必要先从Java内存模型提及。数组

2、理解Java内存模型

  首先要说明的是Java内存模型(即Java Memory Model,简称JMM)和JVM内存区域划分(程序计数器、Java虚拟机栈、本地方法栈、Java堆、方法区等)是不一样的两个概念,Java内存区域自己是一种抽象的概念,并不真实存在,它描述的是一组规则或规范,经过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。因为JVM运行java程序其实是靠一条条线程来完成的,所以每条线程线程在启动时,JVM都会为其建立一个私有的工做空间,咱们称其为本地工做内存,每条线程的本地工做内存都是相对独立的,其余线程没法访问。其实这很好理解,由于每条线程都有本身的职责,好比主线程负责执行咱们写的代码,GC线程负责垃圾回收等等。试想若是每条线程之间均可以任意的访问其余线程的数据,是否是很是容易引发线程的安全性问题,因此说每条线程都存在这样一个本地工做内存。可是反过来讲,凡事也不能作的太绝对,若是每条线程都彻底独立于其余线程,那么全部线程间就也没法一块儿工做了。说到这里,我想起了《Spring In Action》中的一句话,IoC容器的做用就是下降组件之间的耦合性,经过IoC容器的依赖注入让组件之间产生依赖关系,咱们能够将IoC容器理解成组件之间的一个媒介。说回线程,既然刚才说到每条线程的本地工做内存对其余线程是不见的,可是每条线程还不能和其余线程彻底独立,那么确定就也须要一个相似媒介的东西。这里我形象的把这种情景比喻成相亲,两个互相不认识的男女,它们之间没法进行通讯,可是要想取得联系,就必须经过媒婆来传话,媒婆就是这两个相对隔离的人之间的媒介。由此,就引出了线程间进行通讯的媒介--主内存,主内存是共享数据区域,每条线程均可以访问主内存中的数据。通过上边白话的讲解,下面我用专业术语来介绍一下这两个概念。安全

  • 主内存

  主要存储的是Java实例对象,全部线程建立的实例对象都存放在主内存中,无论该实例对象是成员变量仍是方法中的本地变量(也称局部变量),固然也包括了共享的类信息、常量、静态变量。因为是共享数据区域,多条线程对同一个变量进行访问可能会发现线程安全问题。app

  • 本地工做内存ide

  主要存储当前方法的全部本地变量信息(工做内存中存储着主内存中的变量副本拷贝),每一个线程只能访问本身的工做内存,即线程中的本地变量对其它线程是不可见的,就算是两个线程执行的是同一段代码,它们也会各自在本身的工做内存中建立属于当前线程的本地变量,固然也包括了字节码行号指示器、相关Native方法的信息。注意因为工做内存是每一个线程的私有数据,线程间没法相互访问工做内存,所以存储在工做内存的数据不存在线程安全问题。优化

 

  咱们用下面这幅图来描述各条线程间的本地工做内存呢和主内存之间的关系:this

 

 

 

3、理解线程间的可见性  

  特别须要说明的是,线程是不容许直接操做(主要是指写操做)主内存中的数据的,线程若想操做主内存的数据,必需要先将主内存中的数据读取到本身的本地工做内存中,而后拷贝一个副本,对这个副本进行操做,而后再写回主内存中。另外须要注意的是,线程读取共享数据的时候,也不是每次都从主内从中进行读取,这在操做系统中有一套优化的算法,因为从主内存中读取数据确定要比从本身的工做内存中读取效率低,因此线程前几回会尝试从主内存中进行读取,并保存一份副本到工做内存中,当从主内存中读取屡次后发现老是和工做内存中的副本数据同样时,它以后每次便会优先从选择本地工做内存读取,不肯定什么时候再到主内存中读取。基于上述分析,这种机制会形成一些问题:spa

  • 若是线程A在本地工做内存中对以前读进来的数据进行了更新,而且把线程A的副本更新成了最新值,可是还差最后一步将副本刷新到主内存没完成的时候,此时线程B主内存读取数据,那么此时线程B读取的依然仍是旧的数据(即便线程A确实已经完成了对数据的更新操做),由于线程B是看不见线程A中的数据的。
  • 像上面所说的,若是线程B以前尝试从主内存中读取数据发现老是和副本的一致,那么接下来线程B将会一直读取本身本地工做内存中的副本。即便以后线程A将最新的数据刷新到了主内存,因为线程B一直在读取本身以前读进来的副本,那么主内存中的最新数据线程B依然是看不见的,由于并没人通知它主内存已经更新成了最新值。

4、分析引起代码问题的缘由

  上边所描述的这些,总结起来就三个字,可见性。线程的可见性问题,也正是因为java内存模型的机制而引起的,了解了这些,咱们如今回过头了再看最开始的代码,就很是容易理解了:

  

 1 public class VolatileThread implements Runnable{
 2 
 3     private boolean flag = true;
 4     
 5     @Override
 6     public void run() {
 7         System.out.println("子线程开始执行...");
 8         while(flag){
 9         }
10         System.out.println("子线程执行结束...");
11     }
12     public void setFlag(boolean flag) {
13         this.flag = flag;
14     }
15 }
 1 public class VolatileThreadMain {
 2     public static void main(String[] args) throws InterruptedException {
 3         VolatileThread volatileThread = new VolatileThread();
 4         Thread thread = new Thread(volatileThread);
 5         thread.start();
 6         Thread.sleep(3000);
 7         volatileThread.setFlag(false);
 8         System.out.println("flag改成false");
 9         Thread.sleep(1000);
10         System.out.println(volatileThread.flag);
11     }
12 }

  

  咱们知道,成员变量(存在于堆中)是全局共享的变量,所以在VolatileThread类中,flag存在于共享数据区域即主内存。接下来咱们来分析 VolatileThreadMain,第5行当咱们启动自定义的线程thread时,线程执行重写的run方法,进入while循环判断flag时,会先将flag读取进thread本身的本地工做内存并保存一个副本。而后就是不断的判断flag而后执行while循环,注意,我在VolatileThreadMain的第6行加的一个休眠3s,这3s看似不长,可是对于线程thread来讲,它要作的循环次数要数以万计,这么屡次循环判断flag中,flag都没有发生改变,这也就致使了我上面所说的,后边它会优先从本身的副本中读取flag(你们能够自行尝试一下,若是不加休眠,程序是很快就会停下的,就是由于前几回其实线程thread仍是会去主内存中读取数据)。即便后边主线程将主内存中的共享数据flag修改为了false,线程thread也不会从主内存读取了,这也就是形成程序一直中止不了的缘由。

5、解决可见性问题--volatile关键字

  对于上述可见性问题,java给出了解决办法,使用volatile关键字,volatile的功能有两个:保证可见性和禁止指令重排序。

  1.保证可见性

  保证被volatile修饰的共享变量对全部线程总数可见的,也就是当一个线程修改了一个被volatile修饰共享变量的值,新值老是能够被其余线程当即得知。其实它的原理是基于内存屏障(有兴趣能够参考我文末推荐的连接)实现了如下两点:

  • 线程读取共享数据时,每次都必须从主内存中读取,不容许从本地工做内存的副本中读取。
  • 线程更新共享数据时,只要更新完成必须强制刷新到主内存中。 

  经过上述两点保证,就彻底消除了我在(三)中阐述的由可见性引起的一系列问题。所以在咱们的代码中,出现问题的根本缘由是线程thread每次没有从主内存中读取最新的flag值,而是从本地工做内存中的副本中读取,才致使程序一直处于循环状态中停不下来,所以咱们在这里只需将共享变量flag用volatile修饰,即把VolatileThread中的第3行代码改成private volatile boolean flag = true;这样就能保证主线程修改flag后,线程thread会当即得知结果。你们能够本身尝试一下,这里再也不演示。

 

  2.禁止指令重排序

  volatile除了能够保证可见性外,还能够禁止指令的重排序。因为在java内存模型中提供了happens-before原则(想详细了解可见文末连接)来辅助保证程序执行的原子性、可见性以及有序性的问题,使得重排序问题咱们几乎不会遇到。而且鉴于本文重点讨论volatile的可见性问题,这里对指令重拍再也不过多赘述,有兴趣可参看文末连接。

 

6、最后的说明

  不少初学者会分不清volatile和synchronized的区别,不知道分别什么时候使用它们。在这里我想说明,在线程中有三个概念,分别是原子性、可见性、和有序性。volatile主要解决的是线程间的可见性引起的问题,本文上述已经作了详细描述。而synchronized主要解决的是原子性问题,同时也解决了可见性问题。咱们能够认为volatile是synchronized的一个轻量级实现,若是线程间操做的共享数据只存在可见性问题而不存在原子性问题(如本文的例子),咱们用volatile修饰共享变量便可(固然也能够用synchronized修饰,由于synchronized也实现了可见性问题),而尽可能不用比较重量级的synchronized。可是若是共享变量涉及到原子性引起的问题,那咱们就必定要对某些代码进行同步处理了(如synchronized、lock等),这种状况即便使用了volatile照样仍是会引起线程安全问题。

  关于线程安全性问题(侧重讲原子性问题)和synchronized的使用方法,能够参看个人另外一篇文章:http://www.cnblogs.com/rainie-love/p/8531667.html

  最后,给出volatile和synchronized分别能够修饰在哪里:

  1. volatile:只能修饰共享变量(即成员变量),不可修饰局部变量和方法。
  2. synchronized:能够修饰共享变量(即成员变量)、代码块、方法、静态方法。

 

  最后强烈给有一些基础的朋友们推荐一篇超级详细的博文,深刻剖析了JMM内存模型的原理(深刻到操做系统和硬件层面讲解)、线程的三个概念、指令重排序、happens-before原则等等:

  http://blog.csdn.net/javazejian/article/details/72772461

相关文章
相关标签/搜索