天天看點

Java final關鍵字的記憶體語義以及并發時long、double的特殊規則1 final的記憶體語義2 long、double變量的特殊規則

java中的final關鍵字賦予了對象特殊的記憶體語義,可用于實作線程安全,另外,多線程下在32位的虛拟機中對long、double類型變量的操作可能會有意想不到的表現。

文章目錄

  • 1 final的記憶體語義
    • 1.1 final域重排序規則
    • 1.2 寫 final 域的重排序規則
    • 1.3 讀 final 域的重排序規則
    • 1.4 final域為引用類型
  • 2 long、double變量的特殊規則

1 final的記憶體語義

1.1 final域重排序規則

對于final域,編譯器 和 處理器 要遵守兩個 重排序規則。

  1. 在構造函數内對一個final域的寫入,與随後把這個被構造對象的引用指派給一個引用變量,這兩個操作之間不能重排序。
  2. 初次讀一個包含final域的對象的引用,與随後初次讀這個final域,這兩個操作之間不能重排序。

1.2 寫 final 域的重排序規則

JMM 禁止編譯器把 final 域的寫重排序到構造函數之外。編譯器會在 final 域的寫之後,構造函數 return 之前,插入一個 StoreStore 屏障。就是這個屏障禁止處理器把 final 域的寫重排序到構造函數之外。

  1. final 成員變量必須在聲明的時候初始化或者在構造器中初始化,否則就會報編譯錯誤。
  2. 被 final 修飾的字段在聲明時或者構造器中,一旦初始化完成,那麼在其他線程無須同步就能正确看見 final 字段的值。

1.3 讀 final 域的重排序規則

  1. 初次讀一個包含 final 域的對象的引用,與随後初次讀這個 final 域,這兩個操作之間不能重排序。編譯器會在讀 final 域操作的前面插入一個 LoadLoad 屏障。
  2. 當構造函數結束時,final 類型的值是被保證其他線程通路該對象時,它們的值是可見的。
  3. final 類型的成員變量的值,包括那些用 final 引用指向的 collections 的對象,是讀線程安全而無需使用 synchronized 的。

1.4 final域為引用類型

對于引用類型,寫final域的重排序規則對編譯器和處理器增加了如下限制:在構造函數内對一個final引用的對象的成員域的寫入,與随後在構造函數外把這個被構造對象的引用指派給一個引用變量,這兩個操作之間不能重排序。

2 long、double變量的特殊規則

Java記憶體模型要求lock、unlock、read、load、assign、use、store、write這8個操作都具有原子性,但是對于64位的資料類型(long和double),在模型中特别定義了一條相對寬松的規定:允許虛拟機将沒有被volatile修飾的64位資料的讀寫操作劃分為兩次32位的操作來進行,即允許虛拟機實作選擇可以不保證64位資料類型的load、store、read和write這4個操作的原子性,這點就是所謂的long和double的非原子性協定(Nonatomic Treatment ofdouble andlong Variables)。關于Java記憶體模型,可以看這篇文章:Java記憶體模型與happens-before原則詳解。

對于32位作業系統來說,單次次操作能處理的最長長度為32bit,而long類型8位元組64bit,是以對long的讀寫都要兩條指令才能完成。是以會把一個 64 位 long/ double 型變量的讀 / 寫操作拆分為兩個 32 位的讀 / 寫操作來執行。如果真的這樣,當多個線程共享一個并未聲明為volatile的long或者double類型的變量,并同時對他們進行讀取修改,那麼某些線程可能會讀到一些既非初始值也不是其他線程修改值的代表了“半個變量”的資料。

Java final關鍵字的記憶體語義以及并發時long、double的特殊規則1 final的記憶體語義2 long、double變量的特殊規則

如上圖所示,假設處理器 A 寫一個 long 型變量,同時處理器 B 要讀這個 long 型變量。處理器 A 中 64 位的寫操作被拆分為兩個 32 位的寫操作,且這兩個 32 位的寫操作被配置設定到不同的寫事務中執行。同時處理器 B 中 64 位的讀操作被拆分為兩個 32 位的讀操作,且這兩個 32 位的讀操作被配置設定到同一個的讀事務中執行。當處理器 A 和 B 按上圖的時序來執行時,處理器 B 将看到僅僅被處理器 A“寫了一半“的無效值。

是以需要使用volatile關鍵字來防止此類現象。volatile本身不保證擷取和設定操作的原子性,僅僅保持修改的可見性。但是java的記憶體模型保證聲明為volatile的long和double變量的get和set操作是原子的(存疑)。

Java語言規範文檔:jls-17(https://docs.oracle.com/javase/specs/jls/se8/html/jls-17.html#jls-17.7)中有這樣的描述:

  1. 對于64位的long和double,如果沒有被volatile修飾,那麼對其操作可以不是原子的。在操作的時候,可以分成兩步,每次對32位操作。
  2. 如果使用volatile修飾long和double,那麼其讀寫都是原子操作
  3. 對于64位的引用位址的讀寫,都是原子操作
  4. 在實作JVM時,可以自由選擇是否把讀寫long和double作為原子操作
  5. 推薦JVM實作為原子操作

參考資料:

  1. 《JSR133規範》
  2. 《Java并發程式設計之美》
  3. 《實戰Java高并發程式設計》
  4. 《Java并發程式設計的藝術》
如果有什麼不懂或者需要交流,可以留言。另外希望點贊、收藏、關注,我将不間斷更新各種Java學習部落格!