萬字長文!一文完全搞懂Java,阿里Java面試必問

在執行程序的過程當中,爲了提升性能,編譯器和處理器一般會對指令進行重排序。重排序主要分爲三類java

  • 編譯器優化的重排序:編譯器在不改變單線程語義的狀況下,會對執行語句進行從新排序。git

  • 指令集重排序:現代操做系統中的處理器都是並行的,若是執行語句之間不存在數據依賴性,處理器能夠改變語句的執行順序程序員

  • 內存重排序:因爲處理器會使用讀/寫緩衝區,出於性能的緣由,內存會對讀/寫進行重排序編程

也就是說,要想併發程序正確地執行,必需要保證原子性、可見性以及有序性。只要有一個沒有被保證,就有可能會致使程序運行不正確。數組

volatile 的實現原理緩存

=================================================================================安全

上面聊了這麼多,你可能都要忘了這篇文章的故事主角了吧?主角永遠存在於咱們心中 …mybatis

其實上面聊的這些,都是在爲 volatile 作鋪墊。架構

在併發編程中,最須要處理的就是線程之間的通訊和線程間的同步問題,上面的可見性、原子性、有序性也是這兩個問題帶來的。併發

可見性

而 volatile 就是爲了解決這些問題而存在的。Java 語言規範對 volatile 下列定義:Java 語言爲了確保可以安全的訪問共享變量,提供了 volatile 這個關鍵字,volatile 是一種輕量級同步機制,它並不會對共享變量進行加鎖,但在某些狀況下要比加鎖更加方便,若是一個字段被聲明爲 volatile,Java 線程內存模型可以確保全部線程訪問這個變量的值都是一致的。

一旦共享變量被 volatile 修飾後,就具備了下面兩種含義

  1. 保證了這個字段的可見性,也就是說全部線程都可以"看到"這個變量的值,若是某個 CPU 修改了這個變量的值以後,其餘 CPU 也可以得到通知。

  2. 可以禁止指令的重排序

下面咱們來看一段代碼,這也是咱們編寫併發代碼中常常會使用到的

boolean isStop = false;

while(!isStop){

    ...

}



isStop = true;

在這段代碼中,若是線程一正在執行 while 循環,而線程二把 isStop 改成 true 以後,轉而去作其餘事情,由於線程一併不知道線程二把 isStop 改成 true ,因此線程一就會一直運行下去。

若是 isStop 用 volatile 修飾以後,那麼事情就會變得不同了。

使用 volatile 修飾了 isStop 以後,在線程二把 isStop 改成 true 以後,會強制將其寫入內存,而且會把線程一中 isStop 的值置爲無效(這個值其實是在緩存在 CPU 中的緩存行裏),當線程一繼續執行代碼的時候,會從內存中從新讀取 isStop 的值,此時 isStop 的值就是正確的內存地址的值。

volatile 有下面兩條實現原則,其實這兩條原則咱們在上面介紹的時候已經提過了,一種是總線鎖的方式,咱們後面說總線鎖的方式開銷比較大,因此後面設計人員作了優化,採用了鎖緩存的方式。另一種是 MESI 協議的方式。

  • 在 IA-32 架構軟件開發者的手冊中,有一種 Lock 前綴指令,這種指令可以聲言 LOCK# 信號,在最近的處理器中,LOCK# 信號用於鎖緩存,等到指令執行完畢後,會把緩存的內容寫回內存,這種操做通常又被稱爲緩存鎖定

  • 當緩存寫回內存後,IA-32 和 IA-64 處理器會使用 MESI 協議控制內部緩存和其餘處理器一致。IA-32 和 IA-64 處理器可以嗅探其餘處理器訪問系統內部緩存,當內存值修改後,處理器會從內存中從新讀取內存值進行新的緩存行填充。

因而可知,volatile 可以保證線程的可見性。

那麼 volatile 可以保證原子性嗎?

原子性

咱們仍是以 i = i + 1 這個例子來講明一下,i = i + 1 分爲三個操做

  • 讀取 i 的值

  • 自增 i 的值

  • 把 i 的值寫會內存

咱們知道,volatile 可以保證修改 i 的值對其餘線程可見,因此咱們此時假設線程一執行 i 的讀取操做,此時發生了線程切換,線程二讀取到最新 i 的值是 0 而後線程再次發生切換,線程一把 i 的值改成 1,線程再次切換,由於此時 i 尚未應用到內存,因此線程 i 一樣把 i 的值改成 1 後,線程再次發生切換,線程一把 i 的值寫入內存後,再次發生切換,線程二再次把 i 的值寫會內存,因此此時,雖然內存值改了兩次,可是最後的結果卻不是 2。

那麼 volatile 不能保證原子性,那麼該如何保證原子性呢?

在 JDK 5 的 java.util.concurrent.atomic 包下提供了一些原子操做類,例如 AtomicInteger、AtomicLong、AtomicBoolean,這些操做是原子性操做。它們是利用 CAS 來實現原子性操做的(Compare And Swap),CAS其實是利用處理器提供的 CMPXCHG 指令實現的,而處理器執行 CMPXCHG 指令是一個原子性操做。

詳情能夠參考筆者的這篇文章 一場 Atomic XXX 的魔幻之旅。

那麼 volatile 能不能保證有序性呢?

這裏就須要和你聊一聊 volatile 對有序性的影響了

有序性

上面提到過,重排序分爲編譯器重排序、處理器重排序和內存重排序。咱們說的 volatile 會禁用指令重排序,實際上 volatile 禁用的是編譯器重排序和處理器重排序。

下面是 volatile 禁用重排序的規則

從這個表中能夠看出來,讀寫操做有四種,即不加任何修飾的普通讀寫和使用 volatile 修飾的讀寫。

從這個表中,咱們能夠得出下面這些結論

  • 只要第二個操做(這個操做就指的是代碼執行指令)是 volatile 修飾的寫操做,那麼不管第一個操做是什麼,都不能被重排序。

  • 當第一個操做是 volatile 讀時,無論第二個操做是什麼,都不能進行重排序。

  • 當第一個操做是 volatile 寫以後,第二個操做是 volatile 讀/寫都不能重排序。

爲了實現這種有序性,編譯器會在生成字節碼中,會在指令序列中插入內存屏障來禁止特定類型的處理器重排序。

這裏咱們先來了解一下內存屏障的概念。

內存屏障也叫作柵欄,它是一種底層原語。它使得 CPU 或編譯器在對內存進行操做的時候, 要嚴格按照必定的順序來執行, 也就是說在 memory barrier 以前的指令和 memory barrier 以後的指令不會因爲系統優化等緣由而致使亂序。

內存屏障提供了兩個功能。首先,它們經過確保從另外一個 CPU 來看屏障的兩邊的全部指令都是正確的程序順序;其次它們能夠實現內存數據可見性,確保內存數據會同步到 CPU 緩存子系統。

不一樣計算機體系結構下面的內存屏障也不同,一般須要認真研讀硬件手冊來肯定,因此咱們的主要研究對象是基於 x86 的內存屏障,一般狀況下,硬件爲咱們提供了四種類型的內存屏障。

  • LoadLoad 屏障

它的執行順序是 Load1 ; LoadLoad ;Load2 ,其中的 Load1 和 Load2 都是加載指令。LoadLoad 指令可以確保執行順序是在 Load1 以後,Load2 以前,LoadLoad 指令是一個比較有效的防止看到舊數據的指令。

  • StoreStore 屏障

它的執行順序是 Store1 ;StoreStore ;Store2 ,和上面的 LoadLoad 屏障的執行順序類似,它也可以確保執行順序是在 Store1 以後,Store2 以前。

  • LoadStore 屏障

它的執行順序是 Load1 ; StoreLoad ; Store2 ,保證 Load1 的數據被加載在與這數據相關的 Store2 和以後的 store 指令以前。

  • StoreLoad 屏障

它的執行順序是 Store1 ; StoreLoad ; Load2 ,保證 Store1 的數據被其餘 CPU 看到,在數據被 Load2 和以後的 load 指令加載以前。也就是說,它有效的防止全部 barrier 以前的 stores 與全部 barrier 以後的 load 亂序。

JMM 採起了保守策略來實現內存屏障,JMM 使用的內存屏障以下

下面是一個使用內存屏障的示例

class MemoryBarrierTest {

  int a, b;

  volatile int v, u;

  void f() {

    int i, j;



    i = a;

    j = b;

    i = v;



    j = u;



    a = i;

    b = j;



    v = i;



    u = j;



    i = u;



    j = b;

    a = i;

  }

}

這段代碼雖然比較簡單,可是使用了很多變量,看起來有些亂,咱們反編譯一下來分析一下內存屏障對這段代碼的影響。

從反編譯的代碼咱們是看不到內存屏障的,由於內存屏障是一種硬件層面的指令,單憑字節碼是確定沒法看到的。雖然沒法看到內存屏障的硬件指令,可是 JSR-133 爲咱們說明了哪些字節碼會出現內存屏障。

  • 普通的讀相似 getfield 、getstatic 、 不加 volatile 修飾的數組 load 。

  • 普通的寫相似 putfield 、 putstatic 、 不加 volatile 修飾的數組 store 。

  • volatile 都是能夠被多個線程訪問修飾的 getfield、 getstatic 字段。

  • volatile 寫是能夠被當個線程訪問修飾的 putfield、 putstatic 字段。

這也就是說,只要是普通的讀寫加上了 volatile 關鍵字以後,就是 volatile 讀寫(呃呃呃,我好像說了一句廢話),並無其餘特殊的 volatile 獨有的指令。

根據這段描述,咱們來繼續分析一下上面的字節碼。

a、b 是全局變量,也就是實例變量,不加 volatile 修飾,u、v 是 volatile 修飾的全局變量;i、j 是局部變量。

首先 i = a、j = b 只是把全局變量的值賦給了局部變量,因爲是獲取對象引用的操做,因此是字節碼指令是 getfield 。

從官方手冊就能夠知曉緣由了。

地址在 docs.oracle.com/javase/spec…

由內存屏障的表格可知,第一個操做是普通讀寫的狀況下,只有第二個操做是 volatile 寫纔會設置內存屏障。

繼續向下分析,遇到了 i = v,這個是把 volatile 變量賦值給局部變量,是一種 volatile 讀,一樣的 j = u 也是一種 volatile 讀,因此這兩個操做之間會設置 LoadLoad 屏障。

下面遇到了 a = i ,這是爲全局變量賦值操做,因此其對應的字節碼是 putfield

地址在 docs.oracle.com/javase/spec…

因此在 j = u 和 a = i 之間會增長 LoadStore 屏障。而後 a = i 和 b = j 是兩個普通寫,因此這兩個操做之間不須要有內存屏障。

繼續往下面分析,第一個操做是 b = j ,第二個操做是 v = i 也就是 volatile 寫,因此須要有 StoreStore 屏障;一樣的,v = i 和 u = j 之間也須要有 StoreStore 屏障。

第一個操做是 u = j 和 第二個操做 i = u volatile 讀之間須要 StoreLoad 屏障。

最後一點須要注意下,由於最後兩個操做是普通讀和普通寫,因此最後須要插入兩個內存屏障,防止 volatile 讀和普通讀/寫重排序。

《Java 併發編程藝術》裏面也提到了這個關鍵點。

從上面的分析可知,volatile 實現有序性是經過內存屏障來實現的。

關鍵概念

=======================================================================

在 volatile 實現可見性和有序性的過程當中,有一些關鍵概念,cxuan 這裏從新給讀者朋友們嘮叨下。

  • 緩衝行:英文概念是 cache line,它是緩存中能夠分配的最小存儲單位。由於數據在內存中不是以獨立的項進行存儲的,而是以臨近 64 字節的方式進行存儲。

  • 緩存行填充:cache line fill,當 CPU 把內存的數據載入緩存時,會把臨近的共 64 字節的數據一同放入同一個 Cache line,由於局部性原理:臨近的數據在未來被訪問的可能性大。

  • 緩存命中:cache hit,當 CPU 從內存地址中提取數據進行緩存行填充時,發現提取的位置仍然是上次訪問的位置,此時 CPU 會選擇從緩存中讀取操做數,而不是從內存中取。

  • 寫命中:write hit ,當處理器打算將操做數寫回到內存時,首先會檢查這個緩存的內存地址是否在緩存行中,若是存在一個有效的緩存行,則處理器會將這個操做數寫回到緩存,而不是寫回到內存,這種方式被稱爲寫命中。

  • 內存屏障:memory barriers,是一組硬件指令,是 volatile 實現有序性的基礎。

  • 原子操做:atomic operations,是一組不可中斷的一個或者一組操做。

如何正確的使用 volatile 變量

======================================================================================

上面咱們聊了這麼多 volatile 的原理,下面咱們就來談一談 volatile 的使用問題。

volatile 一般用來和 synchronized 鎖進行比較,雖然它和鎖都具備可見性,可是 volatile 不具備原子性,它不是真正意義上具備線程安全性的一種工具。

從程序代碼簡易性和可伸縮性角度來看,你可能更傾向於使用 volatile 而不是說,由於 volatile 寫起來更方便,而且 volatile 不會像鎖那樣形成線程阻塞,並且若是程序中的讀操做的使用遠遠大於寫操做的話,volatile 相對於鎖還更加具備性能優點。

不少併發專家都推薦遠離 volatile 變量,由於它們相對於鎖更加容易出錯,可是若是你謹慎地聽從一些模式,就可以安全的使用 volatile 變量,這裏有一個 volatile 使用原則

只有在狀態真正獨立於程序內其餘內容時才能使用 volatile。

下面咱們經過幾段代碼來感覺一下這條規則的力量。

1.狀態標誌

一種最簡單使用 volatile 的方式就是將 volatile 做爲狀態標誌來使用。

總結

咱們老是喜歡瞻仰大廠的大神們,但實際上大神也不過凡人,與菜鳥程序員相比,也就多花了幾分心思,若是你再不努力,差距也只會愈來愈大。實際上,做爲程序員,豐富本身的知識儲備,提高本身的知識深度和廣度是頗有必要的。

送你們一份資料,戳這裏免費領取

Mybatis源碼解析

相關文章
相關標籤/搜索