Java中的final關鍵字的內存語義以及 long、double變量的特殊規則

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

1 Final的內存語義

1.1 final域重排序規則

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

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

1.2 寫 final 域的重排序規則

  1. JMM 禁止編譯器把 final 域的寫重排序到構造函數之外。編譯器會在 final 域的寫之後,構造函數 return 之前,插入一個 StoreStore 屏障。就是這個屏障禁止處理器把 final 域的寫重排序到構造函數之外。
    a) final 成員變量必須在聲明的時候初始化或者在構造器中初始化,否則就會報編譯錯誤。
    b) 被 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類型的變量,並同時對他們進行讀取修改,那麼某些線程可能會讀到一些既非初始值也不是其他線程修改值的代表了“半個變量”的數據。如下圖:
在這裏插入圖片描述
  如上圖所示,假設處理器 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實現爲原子操作

參考
《JSR133規範》
《Java併發編程之美》
《實戰Java高併發程序設計》
《Java併發編程的藝術》

如果有什麼不懂或者需要交流,可以留言。另外希望點贊、收藏、關注,我將不間斷更新各種Java學習博客!

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