天天看點

ConcurrentProgramming:volatile/構造方法溢出/禁止重排序一、64位寫入的原子性(Half Write)二、 重排序:DCL問題三、 volatile實作原理四、JSR-133對volatile語義的增強

一、64位寫入的原子性(Half Write)

如,對于一個long型變量的指派和取值操作而言,在多線程場景下,線程A調用set(100),線程B調用get(),在某些場景下,傳回值可能不是100。

public class MyClass {
    
  private long a = 0;
    
  // 線程A調用set(100)
  public void set(long a) {
    this.a = a;
  }
 
  // 線程B調用get(),傳回值一定是100嗎?
  public long get() {
    return this.a;
  }
    
}
           

因為JVM的規範并沒有要求64位的long或者double的寫入是原子的。在32位的機器上,一個64位變量的寫入可能被拆分成兩個32位的寫操作來執行。這樣一來,讀取的線程就可能讀到“一半的值”。解決辦法也很簡單,在long前面加上volatile關鍵字。

二、 重排序:DCL問題

單例模式的線程安全的寫法不止一種,常用寫法為DCL(Double Checking Locking),如下所示:

public class Singleton {
    
  private static Singleton instance;
    
  public static Singleton getInstance() {
      
    if (instance == null) {
        
      synchronized(Singleton.class) {
          
        if (instance == null) {
          // 此處代碼有問題
          instance = new Singleton();
       }
          
     }
        
   }
      
    return instance;
 }
    
}
           

上述的 instance = new Singleton(); 代碼有問題:其底層會分為三個操作:

  1. 配置設定一塊記憶體。
  2. 在記憶體上初始化成員變量。
  3. 把instance引用指向記憶體。
對象的構造并不是“原子的”

在這三個操作中,操作2和操作3可能重排序,即先把instance指向記憶體,再初始化成員變量,因為二者并沒有先後的依賴關系。此時,另外一個線程可能拿到一個未完全初始化的對象。這時,直接通路裡面的成員變量,就可能出錯。這就是典型的“構造方法溢出”問題。

解決辦法也很簡單,就是為instance變量加上volatile修飾。

volatile的三重功效:
  • 64位寫入的原子性
  • 記憶體可見性
  • 禁止重排序

三、 volatile實作原理

由于不同的CPU架構的緩存體系不一樣,重排序的政策不一樣,所提供的記憶體屏障指令也就有差異。

這裡隻探讨為了實作volatile關鍵字的語義的一種參考做法:

  1. 在volatile寫操作的前面插入一個StoreStore屏障。保證volatile寫操作不會和之前的寫操作重排序。
  2. 在volatile寫操作的後面插入一個StoreLoad屏障。保證volatile寫操作不會和之後的讀操作重排序。
  3. 在volatile讀操作的後面插入一個LoadLoad屏障+LoadStore屏障。保證volatile讀操作不會和之後的讀操作、寫操作重排序。

具體到x86平台上,其實不會有LoadLoad、LoadStore和StoreStore重排序,隻有StoreLoad一種重排序(記憶體屏障),也就是隻需要在volatile寫操作後面加上StoreLoad屏障。

四、JSR-133對volatile語義的增強

在JSR -133之前的舊記憶體模型中,一個64位long/ double型變量的讀/ 寫操作可以被拆分為兩個32位的讀/寫操作來執行。從JSR -133記憶體模型開始 (即從JDK5開始),僅僅隻允許把一個64位long/ double型變量的寫操作拆分為兩個32位的寫操作來執行,任意的讀操作在JSR -133中都必須具有原子性(即 任意讀操作必須要在單個讀事務中執行)。

這也正展現了Java對happen-before規則的嚴格遵守。

繼續閱讀