Java併發程式設計基礎之volatile
首先簡單介紹一下volatile的應用,volatile作為Java多執行緒中輕量級的同步措施,保證了多執行緒環境中“共享變數”的可見性。這裡的可見性簡單而言可以理解為當一個執行緒修改了一個共享變數的時候,另外的執行緒能夠讀到這個修改的值。下面就是volatile的具體定義和實現原理。上一篇Java記憶體模型
一、volatile的定義和實現原理
1、Java併發模型採用的方式
a)執行緒通訊的機制主要有兩種:共享記憶體和訊息傳遞。
①共享記憶體:執行緒之間共享程式的公共狀態,通過寫-讀共享記憶體中的公共狀態來進行隱式通訊;
②訊息傳遞:執行緒之間沒有公共狀態,執行緒之間 必須通過傳送訊息來顯式通訊。
b)同步:用於控制不同執行緒之間操作發生相對順序。在
共享記憶體模型中,同步是顯式的進行的,需要顯示的指定某個方法或者程式碼塊線上程執行期間互斥進行。
訊息傳遞模型中,由於訊息的傳送必定在訊息的接受之前,所以同步是隱式的進行的。
c)Java併發採用的是共享記憶體模型,執行緒之間通訊總是隱式的進行,而且這個通訊是對程式設計師透明的。那麼我們需要了解的是這個隱式通訊的底層工作機制。
2、volatile的定義
Java程式語言中允許執行緒訪問共享變數,為了確保共享變數能夠被準確和一致性的更新,執行緒應該確保通過排它鎖單獨獲得這個變數。
3、volatile的底層實現原理
a)在編寫多執行緒程式中,使用volatile修飾的共享變數在進行寫操作的時候,編譯器生成的彙編程式碼中會多出一條lock指令,這條lock指令的作用:
①將當前處理器快取行中的資料寫回到系統記憶體 ②這個寫回記憶體的操作會使得其他CPU裡快取了該記憶體地址的資料無效
b)參考下面的這張圖理解
二、volatile的記憶體語義
1、volatile的特性
a)首先我們來看對單個變數的讀/寫的實現(單個變數的情況可以看做是對同一個鎖對這個變數的讀/寫進行了同步),看下面的例子
1 package cn.jvm.test; 2 3 public class TestVolatile1 { 4 5volatile long var1 = 0L; 6 7public void set(long l) { 8// TODO Auto-generated method stub 9var1 = l; 10} 11 12public void getAndIncrement() { 13// TODO Auto-generated method stub 14var1 ++; //注意++操作 15} 16 17public long get() { 18return var1; 19} 20 }
上面的set和get操作在語義上和使用synchronized修飾後一樣,即下面的這種寫法
1 package cn.jvm.test; 2 3 public class TestVolatile1 { 4 5volatile long var1 = 0L; 6 7public synchronized void set(long l) { 8// TODO Auto-generated method stub 9var1 = l; 10} 11 12public synchronized long get() { 13return var1; 14} 15 }
b)但是在上面的用例中,我們使用的var1++操作,整體上沒有原子性,所以如果使用多執行緒方粉getAndIncrement方法的話,會導致讀出的資料和主存中不一致的情況。
c)volatile變數的特性
①可見性:對一個volatile變數的讀操作,總是能夠看到對這個volatile變數最後的寫入 ②原子性:對任意單個volatile變數的讀寫具有原子性,但是對於volatile變數的複合型操作並不具備原子性
2、volatile寫-讀建立的happens-before關係
a)看下面的程式碼例項
1 package cn.jvm.test; 2 3 public class TestVolatile2 { 4 5int a = 0; 6volatile boolean flag = false; 7 8public void writer() { 9a = 1; 10flag = true; 11} 12 13public void reader() { 14if(flag) { 15int i =a; 16//...其他操作 17} 18} 19 }
b)在上面的程式中,假設執行緒A執行write方法,執行緒B執行reader方法,根據happens-before規則有下面的關係:
程式次序規則:①happens-before②; ③happens-before④ volatile規則:②happens-before③ 傳遞性規則:①happens-before④
所以可以得到下面的這個狀態圖
3、volatile的寫/讀記憶體語義
a)下面是volatile的寫/讀記憶體語義
①當寫一個volatile變數時候,JMM會將執行緒對應的本地記憶體中的共享變數值重新整理到主記憶體中 ②當讀一個volatile變數的時候,JMM會將執行緒對應的本地記憶體置為無效,然後從主記憶體中讀取共享變數
b)還是參照上面的程式示例,參考檢視的模型來進行說明
①寫記憶體語義的示意圖:假設執行緒A執行writer方法,執行緒B執行reader方法,初始狀況下執行緒A和B中的變數都是初始狀態
②寫記憶體語義的示意圖:
三、volatile記憶體語義的實現
我們上面說到的基本上從巨集觀上而言都是說明了volatile保證記憶體可見性問題,volatile的另一個語義就是禁止指令重排序的優化。下面說一下volatile禁止指令重排序的實現細節
1、volatile重排序規則
①當第二個操作是volatile寫的時候,不管第一個操作是什麼,都不能進行指令重排序。這個規則確保volatile寫之前的操作都不會被重排序到volatile寫之後。 也是為了保證volatile寫對其他執行緒可見 ②當第一個操作為volatile讀的時候,不管第二個操作是什麼,都不能進行重排序。確保volatile讀之後的操作不會被重排序到volatile讀之前 ③當第一個操作是volatile寫,第二個操作是volatile讀的時候,不能進行重排序
如下所示,上面的是下表中的總結。
2、記憶體屏障
編譯器在生成位元組碼的時候,會在指令序列中插入記憶體屏障來禁止對特定型別的處理器重排序。下面是集中策略,後面會說明這幾種情況
①在每個volatile寫操作之前插入StoreStore屏障 ②在每個volatile寫操作之後插入StoreLoad屏障 ③在每個volatile讀操作之後插入LoadLoad屏障 ④在每個volatile讀操作之後插入LoadStore屏障
3、記憶體屏障示例
a)volatile寫插入記憶體屏障之後的指令序列圖
b)volatile讀插入記憶體屏障後的指令序列圖
四、volatile與死迴圈問題
1、先看下面的示例程式碼,觀察執行結果,當共享變數isRunning 沒有被宣告為volatile的時候,main執行緒會在2秒之後將共享變數isRunning 置為false並且輸出修改資訊,這樣新建的執行緒應該結束執行,但是實際上並沒有,控制檯中會一直保持執行的狀態,並且不會列印執行緒結束執行;如下所示
1 package cn.jvm.test; 2 3 class ThreadDemo extends Thread { 4privateboolean isRunning = true; 5@Override 6public void run() { 7System.out.println(Thread.currentThread().getName() + " 開始執行"); 8while(isRunning) { 9 10} 11System.out.println(Thread.currentThread().getName() + " 結束執行"); 12} 13public boolean isRunning() { 14return isRunning; 15} 16public void SetIsRunning(boolean isRunning) { 17this.isRunning = isRunning; 18} 19 } 20 21 public class TestVolatile4 { 22public static void main(String[] args) { 23ThreadDemo td = new ThreadDemo(); 24td.start(); 25try { 26Thread.sleep(2000); 27td.SetIsRunning(false); 28System.out.println(Thread.currentThread().getName() + " 執行緒將共享變數值修改為false"); 29} catch (Exception e) { 30// TODO: handle exception 31e.printStackTrace(); 32} 33} 34 }
2、分析出現上面結果的原因
在啟動執行緒ThreadDemo之後,變數isRunning被存在公共堆疊以及執行緒的私有堆疊中,後//續中執行緒一直在私有堆疊中取出isRunning的值,雖然main執行緒執行SetIsRunning方法修改了 isRunning的值,但是這個值並沒有被Thread-//0執行緒所知,就像上面說的Thread-0取得值一直都是私有堆疊中的,所以不會知道isRunning被修改,也就不會退出迴圈
3、按照上面的原因分析一下執行的時候的工作記憶體和主記憶體的情況,按照下面的分析我們很容易得出結論
上面的問題就是因為工作記憶體(私有堆疊)和主記憶體(公共堆疊)中的值不同步。 而按照我們上面說到的volatile使得單個變數保證執行緒可見性,就可以對程式修改保證共享變數在main執行緒中的修改對Thread-0執行緒可見(結合volatile的實現原理)
4、修改之後的結果
1 package cn.jvm.test; 2 3 class ThreadDemo extends Thread { 4private volatile boolean isRunning = true; 5@Override 6public void run() { 7System.out.println(Thread.currentThread().getName() + " 開始執行"); 8while(isRunning) { 9 10} 11System.out.println(Thread.currentThread().getName() + " 結束執行"); 12} 13public boolean isRunning() { 14return isRunning; 15} 16public void SetIsRunning(boolean isRunning) { 17this.isRunning = isRunning; 18} 19 } 20 21 public class TestVolatile4 { 22public static void main(String[] args) { 23ThreadDemo td = new ThreadDemo(); 24td.start(); 25try { 26Thread.sleep(2000); 27td.SetIsRunning(false); 28System.out.println(Thread.currentThread().getName() + " 執行緒將共享變數值修改為false"); 29} catch (Exception e) { 30// TODO: handle exception 31e.printStackTrace(); 32} 33} 34 } 將isRunning修改為volatile
五、volatile對於複合操作非原子性問題
1、volatile能保證對單個變數在多執行緒之間的可見性問題,但是對於單個變數的複合操作不能保證原子性,如下程式碼示例,執行結果為 ,當然這個結果是隨機的,但是不能保證執行結果是100000
在沒有使用同步操作之前,雖然count變數是volatile的,但是由於count++操作是個複合操作 ①從記憶體中取出count的值 ②計算count的值 ③將count的值寫到記憶體中 這個複合操作由於volatile不能保證原子性,所以就會出現錯誤
1 package cn.jvm.test; 2 3 import java.util.ArrayList; 4 import java.util.List; 5 6 public class TestVolatile5 { 7volatile int count = 0; 8/*synchronized*/ void m(){ 9for(int i = 0; i < 10000; i++){ 10count++; 11} 12} 13 14public static void main(String[] args) { 15final TestVolatile5 t = new TestVolatile5(); 16List<Thread> threads = new ArrayList<>(); 17for(int i = 0; i < 10; i++){ 18threads.add(new Thread(new Runnable() { 19@Override 20public void run() { 21t.m(); 22} 23})); 24} 25for(Thread thread : threads){ 26thread.start(); 27} 28for(Thread thread : threads){ 29try { 30thread.join(); 31} catch (InterruptedException e) { 32// TODO Auto-generated catch block 33e.printStackTrace(); 34} 35} 36System.out.println(t.count); 37} 38 }
2、下面按照JVM的記憶體工作來分析一下,即當前一個執行緒在計算count變數的時候,另一個執行緒已經修改了count變數的值,這樣就必然會出現錯誤。所以對於這種複合操作就需要使用原子類或者使用synchronized來保證原子性(保證同步)
3、修改後的synchronized和使用原子類如下所示
1 package cn.jvm.test; 2 3 import java.util.ArrayList; 4 import java.util.List; 5 6 public class TestVolatile5 { 7int count = 0; 8synchronized void m(){ 9for(int i = 0; i < 10000; i++){ 10count++; 11} 12} 13 14public static void main(String[] args) { 15final TestVolatile5 t = new TestVolatile5(); 16List<Thread> threads = new ArrayList<>(); 17for(int i = 0; i < 10; i++){ 18threads.add(new Thread(new Runnable() { 19@Override 20public void run() { 21t.m(); 22} 23})); 24} 25for(Thread thread : threads){ 26thread.start(); 27} 28for(Thread thread : threads){ 29try { 30thread.join(); 31} catch (InterruptedException e) { 32// TODO Auto-generated catch block 33e.printStackTrace(); 34} 35} 36System.out.println(t.count); 37} 38 } 使用synchronized
1 package cn.jvm.test; 2 3 import java.util.ArrayList; 4 import java.util.List; 5 import java.util.concurrent.atomic.AtomicInteger; 6 7 public class TestVolatile5 { 8AtomicInteger count = new AtomicInteger(0); 9void m(){ 10for(int i = 0; i < 10000; i++){ 11count.getAndIncrement(); 12} 13} 14 15public static void main(String[] args) { 16final TestVolatile5 t = new TestVolatile5(); 17List<Thread> threads = new ArrayList<>(); 18for(int i = 0; i < 10; i++){ 19threads.add(new Thread(new Runnable() { 20@Override 21public void run() { 22t.m(); 23} 24})); 25} 26for(Thread thread : threads){ 27thread.start(); 28} 29for(Thread thread : threads){ 30try { 31thread.join(); 32} catch (InterruptedException e) { 33// TODO Auto-generated catch block 34e.printStackTrace(); 35} 36} 37System.out.println(t.count); 38} 39 } 使用原子型別