天天看點

Java的volatile到底該如何了解?(上)volatile 的實作次元原子性(Atomicity)可見性(Visibility)

volatile 的實作次元

級别 實作
Java 代碼 volatile int i
ByteCode 位元組碼 ACC_VOLATILE
JVM 虛拟機規範 JVM 記憶體屏障
HotSpot 實作 彙編語言調用
CPU 級别 MESI 原語支援總線鎖

可見性問題

讓一個線程對共享變量的修改,能夠及時的被其他線程看到。

根據JMM中規定的happen before和同步原則:

對某個volatile字段的寫操作happens- before每個後續對該volatile字段的讀操作。

對volatile變量v的寫入,與所有其他線程後續對v的讀同步

要滿足這些條件,是以volatile關鍵字就有這些功能:

禁止緩存;

volatile變量的通路控制符會加個ACC_VOLATILE

對volatile變 量相關的指令不做重排序

volatile 變量可以被看作是一種 "輕量的 synchronized,可算是JVM提供的最輕量級的同步機制。

當一個變量定義為volatile後,可以保證此變量對所有線程的可見性。

原子性(Atomicity)

一次隻允許一個線程持有某鎖,一次隻有一個線程能使用共享資料

由JMM直接保證的原子性變量操作包括read、load、use、assign、store和write六個,大緻可以認為基礎資料類型的通路讀寫是原子性的

如果應用場景需要一個更大範圍的原子性保證,JMM還提供了lock和unlock操作來滿足這種需求,盡管虛拟機未把lock與unlock操作直接開放給使用者使用,但是卻提供了更高層次的位元組碼指令monitorenter和monitorexit來隐匿地使用這兩個操作,這兩個位元組碼指令反映到Java代碼中就是同步塊synchronized關鍵字,是以在synchronized塊之間的操作也具備原子性

可見性(Visibility)

當一個線程修改了線程共享變量的值,其它線程能夠立即得知這個修改。

由于現代可共享記憶體的多處理器架構可能導緻一個線程無法馬上看到另一個線程操作産生的結果。是以 Java 記憶體模型規定了 JVM 的一種最小保證:什麼時候寫入一個變量對其他線程可見。

在現代可共享記憶體的多處理器體系結構中每個處理器都有自己的緩存,并周期性的與主記憶體協調一緻。假設線程 A 寫入一個變量值 V,随後另一個線程 B 讀取變量 V 的值

在下列情況下,線程 B 讀取的值可能不是線程 A 寫入的最新值:

執行線程 A 的處理器把變量 V 緩存到寄存器中。

執行線程 A 的處理器把變量 V 緩存到自己的緩存中,但還沒有同步重新整理到主記憶體中去。

執行線程 B 的處理器的緩存中有變量 V 的舊值。

JMM通過在變量修改後将新值同步回主記憶體,在變量讀取前從主記憶體重新整理變量值這種依賴主記憶體作為傳遞媒介的方式實作可見性,無論是普通變量還是volatile變量都是如此。

普通變量與volatile變量的差別:

volatile保證新值能立即同步到主記憶體,以及每使用前立即從記憶體重新整理。

是以volatile保證了線程操作時變量的可見性,而普通變量則不保證。

除了volatile,Java還有兩個關鍵字能實作可見性:

synchronized

由“對一個變量執行

unlock

前,必須先把此變量同步回主記憶體中(執行

store

write

)”這條規則獲得的

final

被final修飾的字段,在構造器一旦初始化完成,并且構造器沒有把this引用傳遞出去(this引用逃逸是一件危險事情,其它線程可能通過該引用通路到初始化了一半的對象),那在其他線程中就能看見final字段值。

final在該對象的構造器設定對象的字段,當線程看到該對象時,将始終看到該對象的final字段的正确構造版本。

僞代碼示例:

f = new finalDemo();      

讀取到的

f.x

一定最新,

x

為final字段。

若在構造器設定字段後發生讀取,則會看到該final字段配置設定的值,否則它将看到預設值;

public finalDemo() {
    x=1;
    y=x;
};      

y會等于1。

讀取該共享對象的final成員變量之前,先要讀取共享對象。

僞代碼示例:

r = new ReferenceObj(); 
k = r.f;       

這兩個操作不能重排序

通常static final是不可修改的字段,然而System.in、System.out和System.err 都是static final字段,遺留原因,必須允許通過set方法改變,我們将這些字段稱為寫保護,以差別于普通final字段:

Java的volatile到底該如何了解?(上)volatile 的實作次元原子性(Atomicity)可見性(Visibility)
Java的volatile到底該如何了解?(上)volatile 的實作次元原子性(Atomicity)可見性(Visibility)
Java的volatile到底該如何了解?(上)volatile 的實作次元原子性(Atomicity)可見性(Visibility)
Java的volatile到底該如何了解?(上)volatile 的實作次元原子性(Atomicity)可見性(Visibility)

必須確定釋放鎖之前對共享資料做出的更改,對于随後獲得該鎖的另一個線程可見,對域中的值做指派和傳回的操作通常是原子性的,但遞增/減并不是。

volatile對所有線程立即可見,對volatile變量所有的寫操作都能立即傳回到其它線程,即volatile變量在各個線程中是一緻的,但并非基于volatile變量的運算在并發下是安全的。

volatile變量在各線程的工作記憶體中不存在一緻性問題(在各個線程的工作記憶體中volatile變量可存在不一緻,但由于每次使用前都要先重新整理,執行引擎看不到不一緻的情況,是以可認為不存在一緻性問題),但Java裡的運算并非原子操作,導緻volatile變量的運算在并發下一樣不安全:

public class Atomicity {
    int i;
    
    void f(){
        i++;
    }
    
    void g(){
        i += 3;
    }
}      

編譯後檔案

void f();
        0  aload_0 [this]
        1  dup
        2  getfield concurrency.Atomicity.i : int [17]
        5  iconst_1
        6  iadd
        7  putfield concurrency.Atomicity.i : int [17]
 // Method descriptor #8 ()V
 // Stack: 3, Locals: 1
 void g();
        0  aload_0 [this]
        1  dup
        2  getfield concurrency.Atomicity.i : int [17]
        5  iconst_3
        6  iadd
        7  putfield concurrency.Atomicity.i : int [17]
}      

每個操作都産生了一個 get 和 put ,之間還有一些其它指令。是以在擷取和修改之間,另一個線程可能會修改這個域。是以,這些操作不是原子性的。

再看下面這個例子是否符合上面的描述:

public class AtomicityTest implements Runnable {
      private int i = 0;
      
      public int getValue() {
          return i;
      }

      private synchronized void evenIncrement() {
          i++;
          i++;
      }

      public void run() {
        while(true)
          evenIncrement();
      }

      public static void main(String[] args) {
        ExecutorService exec = Executors.newCachedThreadPool();
        AtomicityTest at = new AtomicityTest();
        exec.execute(at);
        while(true) {
          int val = at.getValue();
          if(val % 2 != 0) {
            System.out.println(val);
            System.exit(0);
          }
        }
      }
}

output:
1      

該程式将找到奇數值并終止。盡管return i原子性,但缺少同步使得其數值可以在處于不穩定的中間狀态時被讀取。由于 i 不是 volatile,存在可見性問題

getValue() 和 evenIncrement() 必須synchronized。

對于基本類型的讀/寫操作被認為是安全的原子性操作

但當對象處于不穩定狀态時,仍舊很有可能使用原子性操作來通路他們

最明智的做法是遵循同步的規則

volatile 變量隻保證可見性

在不符合以下條件規則的運算場景中,仍需加鎖(使用synchronized或JUC原子類)保證原子性:

運算結果不依賴變量的目前值,或者能確定隻有單一的線程修改變量的值

變量不需要與其它的狀态變量共同參與不可變類限制

基本上,若一個域可能會被多個任務同時通路or這些任務中至少有一個是寫任務,那就該将此域設為volatile

當一個域定義為 volatile 後,将具備

1.保證此變量對所有的線程的可見性,當一個線程修改了這個變量的值,volatile 保證了新值能立即同步到主記憶體,其它線程每次使用前立即從主記憶體重新整理

但普通變量做不到這點,普通變量的值線上程間傳遞均需要通過主記憶體來完成

2.禁止指令重排序。有volatile修飾的變量,指派後多執行了一個“load addl $0x0, (%esp)”操作,這個操作相當于一個記憶體屏障(指令重排序時不能把後面的指令重排序到記憶體屏障之前的位置)

這些操作的目的是用線程中的局部變量維護對該域的精确同步