volatile在Java記憶體模型(JMM)中,保證共用變數對所有線程可見,但不保證原子性。volatile語義是同步,通過共用變數的方式,完成線程間的通信。 ...
volatile在Java記憶體模型(JMM)中,保證共用變數對所有線程可見,但不保證原子性。volatile語義是同步,通過共用變數的方式,完成線程間的通信。
為什麼需要volatile
Java記憶體模型中抽象、簡化了電腦物理設備,分成工作記憶體和主記憶體,線程有各自的工作記憶體,卻共用主記憶體。如果要把Java記憶體模型與物理設備映射起來的話,L1,L2 Cache可以視為工作記憶體,而L3 Cache視為主記憶體。線程執行指令時,會優先選擇距離 CPU 較近的位置的工作記憶體中使用,而不會從讀寫速度較慢的主記憶體中,我稱之為“就近原則”。當線程指令執行完後,賦值給工作記憶體,如果不回寫到主記憶體,或者通知其他線程,其他線程是無法知曉變數已經修改,仍然會使用曾經緩存在工作記憶體中的變數,這就造成了緩存不一致的問題,Java使用volatile解決這種問題。volatile保證指令賦值完後的變數立即同步回主記憶體中,聲明並通知其他線程當前賦值的變數已經失效,其他線程在下次使用時會放棄工作記憶體中變數,使用主記憶體中的變數。這樣就完成了線程間對於volatile修飾的變數的通信。
可見性
執行引擎只與工作記憶體交互,再有工作記憶體與主記憶體交互。站在執行引擎的角度,與工作記憶體操作完成即表示指令執行完,但是什麼時候工作記憶體會將結果刷新回主記憶體卻不可預測。Java線程間的通信是通過共用記憶體的方式,線程A如果想通知其他所有線程(線程B,線程C)對於變數f的變化情況,需要滿足兩點:
- 將變數回寫到主記憶體中
- 執行引擎讀取時強制從主記憶體中載入
在增加了增加了L1、L2 Cache之後,CPU何時將變數從獨享緩存刷新會共用記憶體,獨享緩存是否從共用記憶體載入變數,時間上都是不可確定的,這就造成了緩存不一致的問題。
可見性的語義是線程對變數更新操作後,其他線程是可以獲知變數的變更情況。
原子性
原子操作是一個或多個不可中斷的操作,要麼一次性完全執行完畢,要麼就不執行,最終狀態不存在有些操作執行完,有些操作沒有執行,在外部看來是不可分割的整體(比如化學中的原子,當然原子也是可以再分割的,不過站在分子層面,原子是最小的不可分割的),原子操作關註的是不被線程調度器中斷的操作。
原子性操作是不會出現線程交替執行的情況,如果出現線程交替,則說明操作被線程調度器中斷。在Java記憶體模型中,原子性保證你獲取的變數要麼是初始值,要麼是被某一個線程寫入的值,而不會是有多個線程同一時間寫入而產生的混亂結果,long或double類型除外(因為變數的前32位可能由一個線程寫入,後32位由另一個不同的線程寫入),不過加上volatile修飾後的long 和double也具有原子性。
註意:volatile關註可見性,而與原子性沒有關係。volatile關註點在於從工作記憶體刷新回主記憶體,而原子操作關註的是否不被打斷。原子和同步目的都是讓不同線程可以安全地訪問共用變數的兩種處理方式,避免造成記憶體一致性錯誤。
我是葛一凡,希望對你有幫助。
參考
- 聊聊併發(一)深入分析Volatile的實現原理
- 聊聊併發(五)——原子操作的實現原理
- Java Language Specification
- Java Volatile 關鍵字詳解
- 從緩存行出發理解volatile變數、偽共用False sharing、disruptor
- Java併發編程:volatile關鍵字解析
- Java 編程要點之併發(Concurrency)詳解
- Java 併發編程(1): Java 記憶體模型(JMM)