我理解的Java併發基礎(二):happens-before、可見性與原子性

重排序是指編譯器和處理器爲了優化程序性能而對指令序列進行從新排序的一種手段。
重排序分3種類型。java

  1. 編譯器優化的重排序。編譯器在不改變單線程寓意的前提下,從新安排語句的執行順序。
  2. 指令級並行的重排序。cpu將多條指令重疊執行。若是不存在數據依賴性,處理器能夠改變語句對應的機器指令的執行順序。
  3. 內存系統的重排序。因爲處理器使用緩存和讀/寫緩衝區,這使得加載和存儲操做看上去多是在亂序執行。 重排序既然是優化,在單線程下是不會影響宏觀上的代碼執行順序的。可是在多線程併發的狀況下就不能保障了。由於宏觀上的一行代碼,對cpu來講對應不少個指令行。

這些重排序可能會致使線程程序出現內存可見性問題。
  比方,一個線程執行兩行代碼:a = 1; flag = true; 而另一個線程執行if(flag){ a = 2 }。若是前者線程發生重排序,併發的時候後者線程就可能發生線程安全問題。(原本前者線程執行到a=1的時候flag仍是false呢,結果因爲重排序先執行了,就致使後者線程進入了if(){}中)程序員

  • 第1點由編譯器引發,JMM的重排序規則會禁止特定類型的編譯器重排序。
  • 針對第2點,JMM要求編譯後的指令對要求禁止重排序的地方插入特性類型的內存屏障(memory barriers/fence)來禁止特性類型的處理器重排序。
  • 針對第3點,JMM提出了內存一致性模型

順序一致性內存模型是一個被計算機科學家理想化了的理論參考模型,它爲程序員提供了極強的內存可見性保證。
順序一致性內存模型有兩大特性。數據庫

  1. 一個線程中的全部操做必須按照程序的順序來執行。
  2. (無論程序是否同步)全部線程都只能看到一個單一的操做執行順序。在順序一致性內存模型中,每一個操做都必須原子執行且馬上對全部線程可見。

  在概念上,順序一致性模型有一個單一的全局內存,這個內存經過一個左右擺動的開關能夠鏈接到任意一個線程,同時每個線程必須按照程序的順序來執行內存讀/寫操做,在任意時間點最多隻能有一個線程能夠鏈接到內存。當多個線程併發執行時,全部線程的全部內存讀/寫操做按照調度執行串行化編程

須要重點理解的happens-before
  爲了不java程序員理解複雜的跟cpu指令相關的內存屏障來保證重排序規則,java使用了happens-before的概念來闡述操做之間的內存可見性。在JMM中,若是一個操做執行的結果須要對另外一個操做可見,那麼兩個操做之間必需要存在happens-before的關係。這兩個操做能夠在同一個線程內,也能夠在不一樣線程內。緩存

happens-before規則:安全

  1. 程序順序規則:在一個單獨的線程中,按照程序代碼的執行順序,先執行的操做happens—before後執行的操做。
  2. 管理鎖定規則:一個unlock操做happens—before後面對同一個鎖的lock操做。
  3. volatile變量規則:對一個volatile變量的寫操做happens—before後面對該變量的讀操做。
  4. 傳遞性:若是A happens-before B 、B happens-before C 那麼A happens-before C 。
  5. start()規則:線程A內執行ThreadB.start(),那麼 A線程的ThreadB.start()操做happens-before於線程B中的任意操做。
  6. join()規則: 若是線程A執行操做ThreadB.join()併成功返回, 那麼線程B中的任意操做happens-before於線程A從ThreadB.join()操做成功返回。

怎麼理解呢?
  把A happens-before B 當作 A的發生B必定是知道的。(不是說多線程中A必定要在B以前發生)多線程

什麼是可見性
  各個線程雖然有本身的緩存,但各個線程在使用同一個變量進行運算以前以及運算完成以後,該變量在各個線程中的數據是一致的。併發

什麼是原子性
  原子性其實就是告訴cpu不能中斷,直到執行完一段指令集以後才能切換。cpu執行完時間片後的任意一個原子指令集以後,都有可能被調度器切換到去執行其餘線程。屬於執行的最小單元。相似於數據庫事務的原子性。
  數據在主內存與線程工做內存的交互,Java虛擬機規範定義了8種原子操做:鎖定(lock)、解鎖(unlock)、讀取(read)、載入(load)、使用(use)、賦值(assign)、存儲(store)、寫入(write)。
  讀取(read)、載入(load)、使用(use)、賦值(assign)、存儲(store)、寫入(write)這6中操做是最基本的原子性操做。 若是想要實現更多操做組成的原子性操做,可使用關鍵字lock和unlock或者synchronized。app

cpu原子性的實現方式:性能

  1. 總線鎖。要操做的內存區域與cpu之間的通道被鎖住,其餘處理器不能操做該區域。效率低。
  2. 緩存鎖。若是要操做的數據在CPU的高速緩存中。使用 緩存一致性 來保證各處理器緩存的一致。效率高。

java原子性的實現方式:

  1. 循環CAS。好比AtomicXxx類的加減和賦值操做。
  2. 鎖機制

循環CAS方式的特色,在低爭搶的場景下,操做效率高。缺點也很明顯:

  1. ABA的問題,即便引入版本比較實現比較複雜;
  2. 循環時間長的話開銷大,白白浪費了cpu資源。
  3. 只能保障一個共享變量的原子操做。

voliatile關鍵字只保證變量的可見性,禁止指令重排序優化,不是原子性的。

實現可見性的關鍵字有:voliatile、synchronized(lock)、final
保證有序性的關鍵字有:voliatile、synchronized(lock)

參考資料:

  • 《Java併發編程的藝術》
  • 《深刻理解Java虛擬機:JVM高級特性與最佳實踐》
  • 以上內容爲筆者平常瑣屑積累,已無從考究引用。若是有,請站內信提示。
相關文章
相關標籤/搜索