併發中的volatile

1. 概述

因爲線程有本地內存的存在, 一個線程修改的共享變量不會及時的刷新到主內存中, 使得另外一個線程讀取共享變量時讀取到的仍舊是舊值, 就致使了內存可見性問題. 如今volatile就能夠解決這個問題, 爲何能解決內存可見性問題呢? 本文就來揭開volatile的神祕面紗.sql

2. volatile的特性

理解volatile特性的一個好方法就是把對volatile單個變量的讀/寫, 當作是使用同一個鎖對單個變量的讀/寫作了同步.架構

鎖的happens-before規則保證釋放鎖和獲取鎖的兩個線程之間的內存可見性, 這意味着對一個volatile變量的讀, 老是能看到(任意線程)對這個volatile變量最後的寫入.併發

鎖的語義決定了臨界區代碼的執行具備原子性. 這意味着, 即便是64位的long型和double型變量, 只要它是volatile變量, 對該變量的讀/寫就具備原子性. 若是是多個volatile操做或相似於volatile++這種複合操做, 這些操做總體上不具備原子性.app

簡言之, volatile變量自身具備如下特性.分佈式

  • 可見性: 對一個volatile變量的讀, 老是能看到任意線程對這個volatile變量最後的寫入.
  • 原子性: 對任意單個volatile變量的讀/寫具備原子性, volatile變量的複合操做不具備原子性.

3. volatile寫-讀的內存語義

volatile寫的內存語義

當寫一個volatile變量時, JMM會把該線程對應的本地內存中的共享變量值刷新到主內存中.高併發

volatile讀的內存語義

當讀一個volatile變量時, JMM會把線程對應的本地內存中的共享變量值置爲無效, 線程接下來將從主內存中讀取共享變量.性能

volatile內存語義總結

  • 線程A寫一個volatile變量, 實質上是線程A向接下來將要讀這個volatile變量的某個線程發出了(其對共享變量所作修改的)消息.
  • 線程B讀一個volatile變量, 實質上是線程B接收了以前某個線程發出的(在寫這個volatile變量以前對共享變量所作修改的)消息.
  • 線程A寫一個volatile變量, 隨後線程B讀這個volatile變量, 這個過程實質上是線程A經過主內存向線程B發送消息.

4. volatile內存語義的實現

前面提到太重排序分爲編譯器重排序和處理器重排序. 爲了實現volatile語義, JMM會分別限制這兩種類型的重排序類型.學習

JMM針對編譯器制定的volatile重排序規則表

從圖中能夠看出:優化

  • 當第二個操做是volatile寫時, 無論第一個操做是什麼, 都不能重排序. 這個規則確保volatile寫以前的操做不會被編譯器重排序到volatile寫以後.
  • 當第一個操做是volatile讀時, 無論第二個操做是什麼, 都不能重排序. 這個規則確保volatile讀以後的操做不會被編譯器重排序到volatile讀以前.
  • 當第一個操做是volatile寫, 第二個操做是volatile讀時, 不能重排序.

爲了實現volatile的內存語義, 編譯器在生成字節碼時, 會在指令序列中插入內存屏障來禁止特定類型的處理器重排序. 對於編譯器來講, 發現一個最優佈置來最小化插入屏障的總數幾乎不可能. 爲此, JMM採起保守策略. 下面是基於保守策略的JMM內存屏障插入策略.spa

  • 在每一個volatile寫操做的前面插入一個StoreStore屏障.
  • 在每一個volatile寫操做的後面插入一個StoreLoad屏障.
  • 在每一個volatile讀操做的後面插入一個LoadLoad屏障.
  • 在每一個volatile讀操做的後面插入一個LoadStore屏障.

上述內存屏障插入策略很是保守, 但它能夠保證在任意處理器平臺, 任意的程序中都能獲得正確的volatile內存語義.

在實際執行時, 只要不改變volatile寫-讀的內存語義, 編譯器能夠根據具體狀況省略沒必要要的屏障. 舉個例子.

有以下代碼:

public class Demo {
    int a;
    volatile int v1 = 1;
    volatile int v2 = 2;
    void readAndWrite() {
      int i = v1; // 第一個volatile讀
      int j = v2; // 第二個volatile讀
      a = i + j; // 普通寫
      v1 = i + 1; // 第一個volatile寫
      v2 = j * 2; // 第二個 volatile寫
    }
}

針對readAndWrite()方法, 理論上生成字節碼時會以下:

int i = v1; // volatile讀後面插入LoadLoad和LoadStore屏障
LoadLoad; // 確保v1的裝載先於後續裝載指令
LoadStore; // 確保v1的加載先於後續存儲指令

int j = v2; // volatile讀後面插入LoadLoad和LoadStore屏障
LoadLoad; // 確保v2的裝載先於後續裝載指令
LoadStore; // 確保v2的加載先於後續存儲指令

a = i + j; // 普通讀寫無屏障

StoreStore; // 確保以前的存儲指令要先於v1的存儲
v1 = i + 1; // volatile寫前加StoreStore屏障, 後加StoreLoad屏障
StoreLoad; // 確保v1的存儲要先於後續的裝載指令

StoreStore;  // 確保以前的存儲指令要先於v1的存儲
v2 = j + 1; // volatile寫前加StoreStore屏障, 後加StoreLoad屏障
StoreLoad; // 確保v1的存儲要先於後續的裝載指令

因爲不一樣的處理器有不一樣的"鬆緊度"的處理器內存模型, 內存屏障的插入還能夠根據具體的處理器內存模型繼續優化, 以X86處理器爲例, 處理最後的StoreLoad屏障外, 其它的屏障都會被省略. X86處理器僅會對寫-讀操做作重排序, 不會對讀-讀, 讀-寫和寫-寫操做作重排序. 所以X86處理器會省略掉這3中操做類型對應的內存屏障. 因此在X86處理器中, JMM僅需在volatile寫後面插入一個StoreLoad屏障便可實現volatile寫-讀的內存語義.歡迎工做一到五年的Java工程師朋友們加入Java架構交流圈:874811168 圈內提供免費的Java架構學習資料(裏面有高可用、高併發、高性能及分佈式、Jvm性能調優、Spring源碼,MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多個知識點的架構資料)合理利用本身每一分每一秒的時間來學習提高本身,不要再用"沒有時間「來掩飾本身思想上的懶惰!趁年輕,使勁拼,給將來的本身一個交代!

下面是X86處理器優化以後的內存屏障

int i = v1; // volatile讀後面插入LoadLoad和LoadStore屏障
// LoadLoad; // 確保v1的裝載先於後續裝載指令
// LoadStore; // 確保v1的加載先於後續存儲指令

int j = v2; // volatile讀後面插入LoadLoad和LoadStore屏障
// LoadLoad; // 確保v2的裝載先於後續裝載指令
// LoadStore; // 確保v2的加載先於後續存儲指令

a = i + j; // 普通讀寫無屏障

// StoreStore; // 確保以前的存儲指令要先於v1的存儲
v1 = i + 1; // volatile寫前加StoreStore屏障, 後加StoreLoad屏障
StoreLoad; // 確保v1的存儲要先於後續的裝載指令

// StoreStore;  // 確保以前的存儲指令要先於v1的存儲
v2 = j + 1; // volatile寫前加StoreStore屏障, 後加StoreLoad屏障
StoreLoad; // 確保v1的存儲要先於後續的裝載指令

5. JSR-133爲何要加強volatile的內存語義

JSR-133也就是在JDK1.5中加入的.

在JSR-133以前的舊Java內存模型中, 雖然不容許volatile變量之間重排序, 但舊的Java內存模型容許volatile變量與普通變量重排序.

在舊的內存模型中, 當1和2之間沒有數據依賴關係時, 1和2之間就可能被重排序(3和4相似). 其結果就是: 讀線程B執行4時, 不必定能看到寫線程A在執行1時對共享變量的修改.

所以, 在舊的內存模型中, volatile的寫-讀沒有鎖的釋放-獲所具備的內存語義. 爲了提供一種比鎖更輕量級的線程之間通訊的機制, JSR-133專家組決定加強volatile的內存語義: 嚴格限制編譯器和處理器對volatile變量與普通變量的重排序, 確保volatile的寫-讀和鎖的釋放-獲取具備相同的內存語義. 從編譯器重排序規則和處理器內存屏障插入策略來看, 只要volatile變量與普通變量之間的重排序可能會破壞volatile的內存語義, 這種重排序就會被編譯器重排序規則和處理器內存屏障插入策略禁止.

6. 總結

volatile能保證內存可見性正是經過內存屏障來實現的, 而且不一樣的編譯器對內存屏障的支持不一樣, 可是因爲大多數處理器都使用了寫緩衝區, 因此大多數處理器都支持StoreLoad屏障.

------------------------------------------- 不要由於知識簡單就忽略, 不積跬步無以致千里.

相關文章
相關標籤/搜索