volatile的可見性、防止指令重排序以及不能保證原子性的解決方式

前言

volatile的使用與線程安全關係密切,主要作用是使變量在多個線程間可見,另外也有防止指令重排的作用。

比如主內存中有變量a=0,線程1設置a=10,線程2再操作a的時候,是以a=10的基礎上進行操作,否則會影響邏輯!


volatile的可見性

要了解volatile的可見性,首先得了解java內存模型:

java內存模型

Java內存模型由Java虛擬機規範定義,用來屏蔽各個平臺的硬件差異。簡單來說:

1. 所有變量儲存在主內存。

2. 每條線程擁有自己的工作內存,其中保存了主內存中線程使用到的變量的副本。

3. 線程不能直接讀寫主內存中的變量,所有操作均在工作內存中完成。

線程,主內存,工作內存的交互關係如下圖所示

volatile的可見性、防止指令重排序以及不能保證原子性的解決方式


如下列代碼所示,rt啓動之後修改isRunning的值爲false,此時while循環不會停止,因爲run方法裏得不到改變之後的isRunning。

解決:使用volatile修飾isRunning,這樣當isRunning的值改變之後,會立即刷新到主內存裏,工作內存也能立即獲取到新的值

public class RunThread extends Thread {
 private boolean isRunning = true;
 private void setRunning(boolean isRunning){
   this.isRunning = isRunning;
 }
 
 public void run () {
   System.out.println("進入run方法");
   while(isRunning == true){
     //...
   }
   System.out.println("線程停止");
 }
 public static void main(String[] args) {
   RunThread rt = new RunThread();
   rt.start();
   try {
     Thread.sleep(3000);
     rt.setRunning(false);
     System.out.println("isRunning的值已經被設置成false");
     Thread.sleep(1000);
     System.out.println(rt.isRunning);
   } catch (InterruptedException e) {
     // TODO Auto-generated catch block
     e.printStackTrace();
   }
 }
}


volatile能防止指令重排

如下列代碼所示,這是單例模式的雙檢鎖寫法

public class SingletonTest {
   private volatile static SingletonTest instance = null;
   private SingletonTest() { }
   public static SingletonTest getInstance() {
       if(instance == null) {
           synchronized (SingletonTest.class){
               if(instance == null) {
                   instance = new SingletonTest();  //非原子操作
               }
           }
       }
       return instance;
   }
}

我們看到instance用了volatile修飾,由於 instance = new SingletonTest();可分解爲:

1.memory =allocate(); //分配對象的內存空間
2.ctorInstance(memory); //初始化對象
3.instance =memory; //設置instance指向剛分配的內存地址

操作2依賴1,但是操作3不依賴2,所以有可能出現1,3,2的順序,當出現這種順序的時候,雖然instance不爲空,但是對象也有可能沒有正確初始化,會出錯。

而使用volatile修飾instance之後,不會出現亂序的行爲!


volatile不保證原子性以及解決方式

1.什麼是原子性?

下列語句中,哪些是原子性操作?

x = 10;         //語句1
y = x;          //語句2
x++;            //語句3
x = x + 1;      //語句4

語句1 是直接將數值 10 賦值給 x,也就是說線程執行這個語句的會直接將數值 10 寫入到工作內存中;

語句2 實際上包含兩個操作,它先要去讀取 x 的值,再將 x 的值寫入工作內存。雖然,讀取 x 的值以及 將 x 的值寫入工作內存這兩個操作都是原子性操作,但是合起來就不是原子性操作了;

同樣的,x++ 和 x = x+1 包括3個操作:讀取 x 的值,進行加 1 操作,寫入新的值。

只有 語句1 的操作具備原子性。也就是說,只有簡單的讀取、賦值(而且必須是將數字賦值給某個變量,變量之間的相互賦值不是原子操作)纔是原子操作!

2.舉例

如下列代碼所示,使用volatile修飾的變量count,利用10個線程分別對count進行++操作,而根據上面的表述,++操作不是原子操作!每個線程加1000個數,打印的結果中一定有一個是10000纔是對的,但是實際上並不是這樣!因爲volatile不保證原子性!

public class VolatileNoAtomic extends Thread{
 private static volatile int count;
//  private static AtomicInteger count = new AtomicInteger(0);
 private static void addCount(){
   for (int i = 0; i < 1000; i++) {
     count ++;
//      count.incrementAndGet();
   }
   System.out.println(count);
 }
 public void run(){
   addCount();
 }
 
 public static void main(String[] args) {
   VolatileNoAtomic[] arr = new VolatileNoAtomic[10];
   for (int i = 0; i < arr.length; i++) {
     arr[i] = new VolatileNoAtomic();
   }
   
   for (int i = 0; i < arr.length; i++) {
     arr[i].start();
   }
 }
}

解決:

方法1:使用原子類Atomic類的系列對象,這樣既不會阻塞,又能保證原子性!

方法2:使用synchronized修飾addCount方法,這樣做的話,線程同步之後會有阻塞,運行時間加長,而且volatile將會失效,不建議這麼改

方法3:使用Lock加鎖,當然,跟方法2一樣的有阻塞


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章