volatile的適用場景.md

一般多線程問題涉及到兩個特性:原子性和可見性。
關鍵字synchronized舉例,如果把代碼塊聲明爲 synchronized,有兩個重要後果,通常是指該代碼具有 原子性(atomicity)和 可見性(visibility)。
原子性意味着個時刻,只有一個線程能夠執行一段代碼,這段代碼通過一個monitor object保護。從而防止多個線程在更新共享狀態時相互衝突。
可見性則更爲微妙,它必須確保釋放鎖之前對共享數據做出的更改對於隨後獲得該鎖的另一個線程是可見的。 —— 如果沒有同步機制提供的這種可見性保證,線程看到的共享變量可能是修改前的值或不一致的值,這將引發許多嚴重問題

volatile的使用條件

volatile具有可見性,但是不具備原子性。可見性的意思是,當一個線程修改 一個共享變量時,另一個線程能讀到這個修改的值。 如果volatile變量修飾符使用恰當的話,他會比synchronized的使用執行成本更低,因爲它不會線程的上下文切換和調度。

在有限的一些情形下使用 volatile 變量替代鎖。要使 volatile 變量提供理想的線程安全,必須同時滿足下面兩個條件:

  • 對變量的寫操作不依賴於當前值。
  • 該變量沒有包含在具有其他變量的不變式中。(不變式表示兩個變量與同一個變量做大小比較操作,如start<value,value<end,那麼start<end就是value的不變式)

概括來說,可以被寫入 volatile 變量的這些有效值獨立於任何程序的狀態,包括變量的當前狀態。

第一個條件限制的場景:使 volatile 變量不能用作線程安全計數器。雖然增量操作(x++)看上去類似一個單獨操作,實際上它是一個由(讀取-修改-寫入)操作序列組成的組合操作,必須以原子方式執行,而 volatile 不能提供必須的原子特性。實現正確的操作需要使x 的值在操作期間保持不變,而 volatile 變量無法實現這點。
第二個條件限制的場景:volatile變量不能用於約束條件中。下面是一個非線程安全的數值範圍類。它包含了一個不變式 —— 下界總是小於或等於上界。

@NotThreadSafe 
public class NumberRange {
    private volatile int lower, upper;
 
    public int getLower() { return lower; }
    public int getUpper() { return upper; }
 
    public void setLower(int value) { 
        if (value > upper) 
            throw new IllegalArgumentException(...);
        lower = value;
    }
 
    public void setUpper(int value) { 
        if (value < lower) 
            throw new IllegalArgumentException(...);
        upper = value;
    }
}

將 lower 和 upper 字段定義爲 volatile 類型不能夠充分實現類的線程安全;而仍然需要使用同步——使 setLower() 和 setUpper() 操作原子化.
否則,如果湊巧兩個線程在同一時間使用不一致的值執行 setLower 和 setUpper 的話,則會使範圍處於不一致的狀態。例如,如果初始狀態是(0, 5),同一時間內,線程 A 調用setLower(4) 並且線程 B 調用setUpper(3),顯然這兩個操作交叉存入的值是不符合條件的,那麼兩個線程都會通過用於保護不變式的檢查,使得最後的範圍值是(4, 3) —— 一個無效值。

2.volatile的適用場景

1.狀態標誌

這種類型的狀態標記的一個公共特性是:通常只有一種狀態轉換;標誌從false 轉換爲true。這種模式可以擴展到來回轉換的狀態標誌。如下面

volatile boolean shutdownRequested;
...
public void shutdown() { 
    shutdownRequested = true; 
}
public void doWork() { 
    while (!shutdownRequested) { 
        // do stuff
    }
}

2.一次性安全發佈(防止 JVM 進行指令重排優化)

最典型的是雙重檢查鎖定的單例模式,如果我們不用volatile修飾的話,可能某個線程拿到的單例對象是還沒有初始化的。
singleton = new Singleton();,這段代碼其實是分爲三步:
分配內存空間。(1)
初始化對象。(2)
將 singleton 對象指向分配的內存地址。(3)
加上 volatile 是爲了讓以上的三步操作順序執行,反之有可能的執行順序爲
1.分配內存空間。(1)
2.將 singleton 對象指向分配的內存地址。(3)
3.初始化對象。(2)
第3步在第2步之前被執行,下一個線程拿到的單例對象是還沒有初始化的,以致於報空指針異常。

//注意volatile!
private volatile static Singleton instace;   
  
public static Singleton getInstance(){   
    //第一次null檢查     
    if(instance == null){            
        synchronized(Singleton.class) { //1     
            //第二次null檢查       
            if(instance == null){ //2  
                instance = new Singleton();//3  
            }  
        }           
    }  
    return instance;        
}

3.開銷較低的“讀-寫鎖”策略

適合於讀操作遠遠超過寫操作場景,可以結合使用內部鎖和 volatile 變量來減少公共代碼路徑的開銷。

{
    private volatile int value;
    //讀操作,沒有synchronized,提高性能
    public int getValue() { 
        return value; 
    } 
 
    //寫操作,必須synchronized。因爲x++不是原子操作
    public synchronized int increment() {
        return value++;
}
}

使用鎖進行所有變化的操作,使用 volatile 進行只讀操作。
其中,鎖一次只允許一個線程訪問值,volatile 允許多個線程執行讀操作.

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