Java併發編程基礎之volatile

  首先簡單介紹一下volatile的應用,volatile做爲Java多線程中輕量級的同步措施,保證了多線程環境中「共享變量」的可見性。這裏的可見性簡單而言能夠理解爲當一個線程修改了一個共享變量的時候,另外的線程可以讀到這個修改的值。下面就是volatile的具體定義和實現原理。上一篇Java內存模型html

1、volatile的定義和實現原理

一、Java併發模型採用的方式

  a)線程通訊的機制主要有兩種:共享內存和消息傳遞。java

  ①共享內存:線程之間共享程序的公共狀態,經過寫-讀共享內存中的公共狀態來進行隱式通訊;程序員

  ②消息傳遞:線程之間沒有公共狀態,線程之間 必須經過發送消息來顯式通訊。編程

  b)同步:用於控制不一樣線程之間操做發生相對順序。在緩存

  共享內存模型中,同步是顯式的進行的,須要顯示的指定某個方法或者代碼塊在線程執行期間互斥進行。多線程

  消息傳遞模型中,因爲消息的發送一定在消息的接受以前,因此同步是隱式的進行的。併發

  c)Java併發採用的是共享內存模型,線程之間通訊老是隱式的進行,並且這個通訊是對程序員透明的。那麼咱們須要瞭解的是這個隱式通訊的底層工做機制。app

二、volatile的定義

Java編程語言中容許線程訪問共享變量,爲了確保共享變量可以被準確和一致性的更新,線程應該確保經過排它鎖單獨得到這個變量。

三、volatile的底層實現原理

  a)在編寫多線程程序中,使用volatile修飾的共享變量在進行寫操做的時候,編譯器生成的彙編代碼中會多出一條lock指令,這條lock指令的做用:jvm

①將當前處理器緩存行中的數據寫回到系統內存
②這個寫回內存的操做會使得其餘CPU裏緩存了該內存地址的數據無效

  b)參考下面的這張圖理解編程語言

2、volatile的內存語義

一、volatile的特性

  a)首先咱們來看對單個變量的讀/寫的實現(單個變量的狀況能夠看作是對同一個鎖對這個變量的讀/寫進行了同步),看下面的例子

 1 package cn.jvm.test;
 2 
 3 public class TestVolatile1 {
 4 
 5     volatile long var1 = 0L;
 6     
 7     public void set(long l) {
 8         // TODO Auto-generated method stub
 9         var1 = l;
10     }
11     
12     public void getAndIncrement() {
13         // TODO Auto-generated method stub
14         var1 ++; //注意++操做
15     }
16     
17     public long get() {
18         return var1;
19     }
20 }

  上面的set和get操做在語義上和使用synchronized修飾後同樣,即下面的這種寫法

 1 package cn.jvm.test;
 2 
 3 public class TestVolatile1 {
 4 
 5     volatile long var1 = 0L;
 6     
 7     public synchronized void set(long l) {
 8         // TODO Auto-generated method stub
 9         var1 = l;
10     }
11     
12     public synchronized long get() {
13         return var1;
14     }
15 }

  b)可是在上面的用例中,咱們使用的var1++操做,總體上沒有原子性,因此若是使用多線程方粉getAndIncrement方法的話,會致使讀出的數據和主存中不一致的狀況。

  c)volatile變量的特性

①可見性:對一個volatile變量的讀操做,老是可以看到對這個volatile變量最後的寫入
②原子性:對任意單個volatile變量的讀寫具備原子性,可是對於volatile變量的複合型操做並不具有原子性

二、volatile寫-讀創建的happens-before關係

  a)看下面的代碼實例

 1 package cn.jvm.test;
 2 
 3 public class TestVolatile2 {
 4 
 5     int a = 0;
 6     volatile boolean flag = false;
 7     
 8     public void writer() {
 9         a = 1;
10         flag = true;
11     }
12     
13     public void reader() {
14         if(flag) {
15             int i =a;
16             //...其餘操做
17         }
18     }
19 }

  b)在上面的程序中,假設線程A執行write方法,線程B執行reader方法,根據happens-before規則有下面的關係:

程序次序規則:①happens-before②; ③happens-before④

volatile規則:②happens-before③

傳遞性規則:①happens-before④

  因此能夠獲得下面的這個狀態圖

三、volatile的寫/讀內存語義

  a)下面是volatile的寫/讀內存語義

①當寫一個volatile變量時候,JMM會將線程對應的本地內存中的共享變量值刷新到主內存中
②當讀一個volatile變量的時候,JMM會將線程對應的本地內存置爲無效,而後從主內存中讀取共享變量

  b)仍是參照上面的程序示例,參考視圖的模型來進行說明

  ①寫內存語義的示意圖:假設線程A執行writer方法,線程B執行reader方法,初始情況下線程A和B中的變量都是初始狀態

   ②寫內存語義的示意圖:

 

3、volatile內存語義的實現

 咱們上面說到的基本上從宏觀上而言都是說明了volatile保證內存可見性問題,volatile的另外一個語義就是禁止指令重排序的優化。下面說一下volatile禁止指令重排序的實現細節

一、volatile重排序規則

①當第二個操做是volatile寫的時候,無論第一個操做是什麼,都不能進行指令重排序。這個規則確保volatile寫以前的操做都不會被重排序到volatile寫以後。
 也是爲了保證volatile寫對其餘線程可見 ②當第一個操做爲volatile讀的時候,無論第二個操做是什麼,都不能進行重排序。確保volatile讀以後的操做不會被重排序到volatile讀以前 ③當第一個操做是volatile寫,第二個操做是volatile讀的時候,不能進行重排序

  以下所示,上面的是下表中的總結。

 

二、內存屏障  

編譯器在生成字節碼的時候,會在指令序列中插入內存屏障來禁止對特定類型的處理器重排序。下面是集中策略,後面會說明這幾種狀況

①在每一個volatile寫操做以前插入StoreStore屏障
②在每一個volatile寫操做以後插入StoreLoad屏障
③在每一個volatile讀操做以後插入LoadLoad屏障
④在每一個volatile讀操做以後插入LoadStore屏障

 

 三、內存屏障示例

  a)volatile寫插入內存屏障以後的指令序列圖

  b)volatile讀插入內存屏障後的指令序列圖

4、volatile與死循環問題

  一、先看下面的示例代碼,觀察運行結果,當共享變量isRunning 沒有被聲明爲volatile的時候,main線程會在2秒以後將共享變量isRunning 置爲false而且輸出修改信息,這樣新建的線程應該結束運行,可是實際上並無,控制檯中會一直保持運行的狀態,而且不會打印線程結束執行;以下所示

 1 package cn.jvm.test;
 2 
 3 class ThreadDemo extends Thread {
 4     private  boolean isRunning = true;
 5     @Override
 6     public void run() {
 7         System.out.println(Thread.currentThread().getName() + " 開始執行");
 8         while(isRunning) {
 9             
10         }
11         System.out.println(Thread.currentThread().getName() + " 結束執行");
12     }
13     public boolean isRunning() {
14         return isRunning;
15     }
16     public void SetIsRunning(boolean isRunning) {
17         this.isRunning = isRunning;
18     }
19 }
20 
21 public class TestVolatile4 {
22     public static void main(String[] args) {
23         ThreadDemo td = new ThreadDemo();
24         td.start();
25         try {
26             Thread.sleep(2000);
27             td.SetIsRunning(false);
28             System.out.println(Thread.currentThread().getName() + " 線程將共享變量值修改成false");
29         } catch (Exception e) {
30             // TODO: handle exception
31             e.printStackTrace();
32         }
33     }
34 }

  二、分析出現上面結果的緣由

在啓動線程ThreadDemo以後,變量isRunning被存在公共堆棧以及線程的私有堆棧中,後//續中線程一直在私有堆棧中取出isRunning的值,雖然main線程執行SetIsRunning方法修改了
isRunning的值,可是這個值並無被Thread-
//0線程所知,就像上面說的Thread-0取得值一直都是私有堆棧中的,因此不會知道isRunning被修改,也就不會退出循環

  三、按照上面的緣由分析一下執行的時候的工做內存和主內存的狀況,按照下面的分析咱們很容易得出結論

上面的問題就是由於工做內存(私有堆棧)和主內存(公共堆棧)中的值不一樣步。
而按照咱們上面說到的volatile使得單個變量保證線程可見性,就能夠對程序修改保證共享變量在main線程中的修改對Thread-0線程可見(結合volatile的實現原理)

  四、修改以後的結果

 1 package cn.jvm.test;
 2 
 3 class ThreadDemo extends Thread {
 4     private volatile boolean isRunning = true;
 5     @Override
 6     public void run() {
 7         System.out.println(Thread.currentThread().getName() + " 開始執行");
 8         while(isRunning) {
 9             
10         }
11         System.out.println(Thread.currentThread().getName() + " 結束執行");
12     }
13     public boolean isRunning() {
14         return isRunning;
15     }
16     public void SetIsRunning(boolean isRunning) {
17         this.isRunning = isRunning;
18     }
19 }
20 
21 public class TestVolatile4 {
22     public static void main(String[] args) {
23         ThreadDemo td = new ThreadDemo();
24         td.start();
25         try {
26             Thread.sleep(2000);
27             td.SetIsRunning(false);
28             System.out.println(Thread.currentThread().getName() + " 線程將共享變量值修改成false");
29         } catch (Exception e) {
30             // TODO: handle exception
31             e.printStackTrace();
32         }
33     }
34 }
將isRunning修改成volatile

 

5、volatile對於複合操做非原子性問題

  一、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 {
 7     volatile int count = 0;
 8     /*synchronized*/ void m(){
 9         for(int i = 0; i < 10000; i++){
10             count++;
11         }
12     }
13 
14     public static void main(String[] args) {
15         final TestVolatile5 t = new TestVolatile5();
16         List<Thread> threads = new ArrayList<>();
17         for(int i = 0; i < 10; i++){
18             threads.add(new Thread(new Runnable() {
19                 @Override
20                 public void run() {
21                     t.m();
22                 }
23             }));
24         }
25         for(Thread thread : threads){
26             thread.start();
27         }
28         for(Thread thread : threads){
29             try {
30                 thread.join();
31             } catch (InterruptedException e) {
32                 // TODO Auto-generated catch block
33                 e.printStackTrace();
34             }
35         }
36         System.out.println(t.count);
37     }
38 }

  二、下面按照JVM的內存工做來分析一下,即當前一個線程在計算count變量的時候,另外一個線程已經修改了count變量的值,這樣就必然會出現錯誤。因此對於這種複合操做就須要使用原子類或者使用synchronized來保證原子性(保證同步)

  三、修改後的synchronized和使用原子類以下所示

 1 package cn.jvm.test;
 2 
 3 import java.util.ArrayList;
 4 import java.util.List;
 5 
 6 public class TestVolatile5 {
 7     int count = 0;
 8     synchronized void m(){
 9         for(int i = 0; i < 10000; i++){
10             count++;
11         }
12     }
13 
14     public static void main(String[] args) {
15         final TestVolatile5 t = new TestVolatile5();
16         List<Thread> threads = new ArrayList<>();
17         for(int i = 0; i < 10; i++){
18             threads.add(new Thread(new Runnable() {
19                 @Override
20                 public void run() {
21                     t.m();
22                 }
23             }));
24         }
25         for(Thread thread : threads){
26             thread.start();
27         }
28         for(Thread thread : threads){
29             try {
30                 thread.join();
31             } catch (InterruptedException e) {
32                 // TODO Auto-generated catch block
33                 e.printStackTrace();
34             }
35         }
36         System.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 {
 8     AtomicInteger count = new AtomicInteger(0);
 9     void m(){
10         for(int i = 0; i < 10000; i++){
11             count.getAndIncrement();
12         }
13     }
14 
15     public static void main(String[] args) {
16         final TestVolatile5 t = new TestVolatile5();
17         List<Thread> threads = new ArrayList<>();
18         for(int i = 0; i < 10; i++){
19             threads.add(new Thread(new Runnable() {
20                 @Override
21                 public void run() {
22                     t.m();
23                 }
24             }));
25         }
26         for(Thread thread : threads){
27             thread.start();
28         }
29         for(Thread thread : threads){
30             try {
31                 thread.join();
32             } catch (InterruptedException e) {
33                 // TODO Auto-generated catch block
34                 e.printStackTrace();
35             }
36         }
37         System.out.println(t.count);
38     }
39 }
使用原子類型

參考自《Java併發編程的藝術》 《Java多線程編程核心技術》

相關文章
相關標籤/搜索