白話講述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

相關文章
相關標籤/搜索