Java併發程式設計藝術

WhaleFall541發表於2020-08-02

volatile域記憶體知識


如何減少cpu上下文切換

  • 避免使用鎖:無鎖併發程式設計,多執行緒競爭鎖時,會引起上下問文切換,所以多執行緒處理時,可以用一些辦法來避免使用鎖,如將資料的ID按照Hash演算法取模分段,不同的執行緒處理不同段的資料
  • CAS演算法:java的atomic包使用CAS演算法來更新資料,而不需要加鎖
  • 使用最少執行緒:避免建立不需要的執行緒,比如任務很少,但是建立了很多執行緒來處理,這樣會造成大量執行緒都處於等待狀態
  • 協程:在單執行緒裡實現多工的排程,並在單執行緒裡維持多個任務間的切換。

volatile和synchronized

如果volatile變數修飾符使用恰當的話,它比synchronized的使用和執行成本更低,因為它不會引起執行緒上下文的切換和排程。

如果對宣告瞭volatile的變數進行寫操作,JVM就會向處理器傳送一條Lock字首的指令,將這個變數所在快取行的資料寫回到系統記憶體

每個處理器通過嗅探在匯流排上傳播的資料來檢查自己快取的值是不是過期了,當處理器發現自己快取行對應的記憶體地址被修改,就會將當前處理器的快取行設定成無效狀態,當處理器對這個資料進行修改操作的時候,會重新從系統記憶體中把資料讀到處理器快取裡。

volatile實現原則

  • Lock字首指令會引起處理器快取回寫到記憶體。Lock字首指令導致在執行指令期間,聲言處理器的LOCK#訊號。在多處理器環境中,LOCK#訊號確保在聲言該訊號期間,處理器可以獨佔任何共享記憶體。但是,在最近的處理器裡,LOCK#訊號一般不鎖匯流排,而是鎖快取,畢竟鎖匯流排開銷的比較大。
  • 一個處理器的快取回寫到記憶體會導致其他處理器的快取無效。IA-32處理器和Intel 64處理器使用MESI(修改、獨佔、共享、無效)控制協議去維護內部快取和其他處理器快取的一致性

jdk 7追加位元組優化效能

  • 將共享變數追加到64位元組。一些處理器不支援部分填充快取行,如果佇列頭節點和尾節點都不足64位元組的話,處理器會將他們讀到同一個快取記憶體行中,在多處理器下每個處理器都會快取同樣的頭、尾節點,當一個處理器試圖修改頭節點時,會將整個快取行鎖定,那麼在快取一致性機制的作用下,會導致其他處理器不能訪問自己快取記憶體中的尾節點,而佇列的入隊和出隊操作則需要不停修改頭節點和尾節點,所以在多處理器的情況下將會嚴重影響到佇列的入隊和出隊效率。Doug lea使用追加到64位元組的方式來填滿高速緩衝區的快取行,避免頭節點和尾節點載入到同一個快取行,使頭、尾節點在修改時不會互相鎖定。

  • 偏向鎖:當一個執行緒訪問同步塊並獲取鎖時,會在物件頭和棧幀中的鎖記錄裡儲存鎖偏向的執行緒ID,以後該執行緒在進入和退出同步塊時不需要進行CAS操作來加鎖和解鎖,只需簡單地測試一下物件頭的Mark Word
    裡是否儲存著指向當前執行緒的偏向鎖。如果測試成功,表示執行緒已經獲得了鎖。如果測試失敗,則需要再測試一下Mark Word中偏向鎖的標識是否設定成1(表示當前是偏向鎖):如果沒有設定,則使用CAS競爭鎖;如果設定了,則嘗試使用CAS將物件頭的偏向鎖指向當前執行緒。

  • 輕量級鎖:執行緒在執行同步塊之前,JVM會先在當前執行緒的棧楨中建立用於儲存鎖記錄的空間,並將物件頭中的Mark Word複製到鎖記錄中,官方稱為Displaced Mark Word。然後執行緒嘗試使用CAS將物件頭中的Mark
    Word替換為指向鎖記錄的指標。如果成功,當前執行緒獲得鎖,如果失敗,表示其他執行緒競爭鎖,當前執行緒便嘗試使用自旋來獲取鎖。

cpu如何保證原子性

匯流排鎖:線鎖就是使用處理器提供的一個LOCK #訊號,當一個處理器在匯流排上輸出此訊號時,其他處理器的請求將被阻塞住,那麼該處理器可以獨佔共享記憶體。

快取鎖:指記憶體區域如果被快取在處理器的快取行中,並且在Lock操作期間被鎖定,那麼當它執行鎖操作回寫到記憶體時,處理器不在匯流排上聲言LOCK #訊號,而是修改內部的記憶體地址,並允許它的快取一致性機制來保證操作的原子性,因為快取一致性機制會阻止同時修改由兩個以上處理器快取的記憶體區域資料,當其他處理器回寫已被鎖定的快取行的資料時,會使快取行無效。

兩種情況不會使用快取鎖

  • 第一種情況:當操作的資料不能被快取在處理器內部,或操作的資料跨多個快取行(cache line)時,則處理器會呼叫匯流排鎖定。
  • 第二種情況:有些處理器不支援快取鎖定。對於Intel 486和Pentium處理器,就算鎖定的記憶體區域在處理器的快取行中也會呼叫匯流排鎖定。

CAS 原子操作的問題

ABA問題:但是如果一個值原來是A,變成了B,又變成了A,那麼使用CAS進行檢查時會發現它的值沒有發生變化,但是實際上卻變化了。ABA問題的解決思路就是使用版本號。在變數前面追加上版本號,每次變數更新的時候把版本號加1,那麼A→B→A就會變成1A→2B→3A。

  • 解決辦法:從Java 1.5開始,JDK的Atomic包裡提供了一個類AtomicStampedReference來解決ABA問題。這個類的compareAndSet
    方法的作用是首先檢查當前引用是否等於預期引用,並且檢查當前標誌是否等於預期標誌,如果全部相等,則以原子方式將該引用和該標誌的值設定為給定的更新值。

迴圈時間長開銷大問題。自旋CAS如果長時間不成功,會給CPU帶來非常大的執行開銷。

只能保證一個共享變數的原子操作。還有一個取巧的辦法,就是把多個共享變數合併成一個共享變數來操作。比如,有兩個共享變數i=2,j=a,合併一下ij=2a,然後用CAS來操作ij。

使用鎖機制實現原子操作鎖機制保證了只有獲得鎖的執行緒才能夠操作鎖定的記憶體區域。JVM內部實現了很多種鎖機制,有偏向鎖、輕量級鎖和互斥鎖。有意思的是除了偏向鎖,JVM實現鎖的方式都用了迴圈CAS,即當一個執行緒想進入同步塊的時候使用迴圈CAS的方式來獲取鎖,當它退出同步塊的時候使用迴圈CAS釋放鎖。

以何種機制來交換資訊


指令重排序

在執行程式時,為了提高效能,編譯器和處理器常常會對指令做重排序。重排序分3種型別

  1. 編譯器優化的重排序。編譯器在不改變單執行緒程式語義的前提下,可以重新安排語句的執行順序。
  2. 指令級並行重排序。現代處理器採用了指令級並行技術(Instruction-Level Parallelism,ILP)來將多條指令重疊執行。如果不存在資料依賴性,處理器可以改變語句對應機器指令的執行順序。
  3. 記憶體系統的重排序。由於處理器使用快取和讀/寫緩衝區,這使得載入和儲存操作看上去可能是在亂序執行。

1屬於編譯器重排序,2和3屬於處理器重排序;

對於編譯器,JMM的編譯器重排序規則會禁止特定型別的編譯器重排序(不是所有的編譯器重排序都要禁止)。對於處理器重排序,JMM的處理器重排序規則會要求Java編譯器在生成指令序列時,插入特定型別的記憶體屏障(Memory Barriers,Intel稱之為Memory Fence)指令,通過記憶體屏障指令來禁止特定型別的處理器重排序。

併發程式設計模型分類

通過以批處理的方式重新整理寫緩衝區,以及合併寫緩衝區中對同一記憶體地址的多次寫,減少對記憶體匯流排的佔用。雖然寫緩衝區有這麼多好處,但每個處理器上的寫緩衝區,僅僅對它所在的處理器可見。這個特性會對記憶體操作的執行順序產生重要的影響:處理器對記憶體的讀/寫操作的執行順序,不一定與記憶體實際發生的讀/寫操作順序一致!

sparc-TSO和X86擁有相對較強的處理器記憶體模型,它們僅允許對寫-讀操作做重排序

StoreLoad Barriers是一個“全能型”的屏障,它同時具有其他3個屏障的效果。
執行該屏障開銷會很昂貴,因為當前處理器通常要把寫緩衝區中的資料全部重新整理到記憶體中(Buffer Fully Flush)。

happens-before

java使用新的JSR-133記憶體模型。在JMM中如果一個操作執行的結果需要對另一個操作可見,那麼這兩個操作之間必須要在happens-before關係。
與程式設計師密切相關的happens-before規則如下。

  • 程式順序規則:一個執行緒中的每個操作,happens-before於該執行緒中的任意後續操作。

  • 監視器鎖規則:對一個鎖的解鎖,happens-before於隨後對這個鎖的加鎖。

  • volatile變數規則:對一個volatile域的寫,happens- before於任意後續對這個volatile域的讀。

  • 傳遞性:如果A happens-before B,且B happens-before C,那麼Ahappens-before C。

happens-before理解

順序一致性模型

一個執行緒中的所有操作必須按照程式的順序來執行。

(不管程式是否同步)所有執行緒都只能看到一個單一的操作執行順序。在順序一致性記憶體模型中,每個操作都必須原子執行且立刻對所有執行緒可見。

當多個執行緒併發執行時,圖中的開關裝置能把所有執行緒的所有記憶體讀/寫操作序列化(即在順序一致性模型中,所有操作之間具有全序關係)。

CPU匯流排事務

匯流排事務包括讀事務(Read Transaction)和寫事務(Write Transaction)。讀事務從記憶體傳送資料到處理器,寫事務從處理器傳送資料到記憶體,每個事務會讀/寫記憶體中一個或多個物理上連續的字。
在一個處理器執行匯流排事務期間,匯流排會禁止其他的處理器和I/O裝置執行記憶體的讀/寫。

當JVM在這種處理器上執行時,可能會把一個64位long/double型變數的寫操作拆分為兩個32位的寫操作來執行。這兩個32位的寫操作可能會被分配到不同的匯流排事務中執行,此時對這個64位變數的寫操作將不具有原子性。

從JSR -133記憶體模型開始(即從JDK5開始),僅僅只允許把一個64位long/double型變數的寫操作拆分為兩個32位的寫操作來執行,任意的讀操作在JSR-133中都必須具有原子性(即任意讀操作必須要在單個讀事務中執行)。

volatile特點

可見性。對一個volatile變數的讀,總是能看到(任意執行緒)對這個volatile變數最後的寫入。

原子性:對任意單個volatile變數的讀/寫具有原子性,但類似於volatile++這種複合操作不具有原子性。

每一個箭頭連結的兩個節點,代表了一個happens-before關係。黑色箭頭表示程式順序規則;橙色箭頭表示volatile規則;藍色箭頭表示組合這些規則後提供的happens-before保證。

A執行緒寫一個volatile變數後,B執行緒讀同一個volatile變數。A執行緒在寫volatile變數之前所有可見的共享變數(即寫之前的值都寫入到JMM中),在B執行緒讀同一個volatile變數後,將立即變得對B執行緒可見。


執行緒A寫一個volatile變數,隨後執行緒B讀這個volatile變數,這個過程實質上是執行緒A通過主記憶體向執行緒B傳送訊息。

volatile重排序規則表

  • 當第二個操作是volatile寫時,不管第一個操作是什麼,都不能重排序。這個規則確保volatile寫之前的操作不會被編譯器重排序到volatile寫之後。
  • 當第一個操作是volatile讀時,不管第二個操作是什麼,都不能重排序。這個規則確保volatile讀之後的操作不會被編譯器重排序到volatile讀之前。
  • 當第一個操作是volatile寫,第二個操作是volatile讀時,不能重排序。

JMM插入記憶體屏障來禁止特定型別的處理器重排序

  • 當第二個操作是volatile寫時,不管第一個操作是什麼,都不能重排序。這個規則確保volatile寫之前的操作不會被編譯器重排序到volatile寫之後。
  • 當第一個操作是volatile讀時,不管第二個操作是什麼,都不能重排序。這個規則確保volatile讀之後的操作不會被編譯器重排序到volatile讀之前。
  • 當第一個操作是volatile寫,第二個操作是volatile讀時,不能重排序。

StoreLoad屏障:一個寫執行緒寫volatile變數,多個讀執行緒讀同一個volatile變數。當讀執行緒的數量大大超過寫執行緒時,選擇在volatile寫之後插入StoreLoad屏障將帶來可觀的執行效率的提升。


ReentrantLock 中公平鎖和非公平鎖記憶體語義

公平鎖和非公平鎖釋放時,最後都要寫一個volatile變數state。

公平鎖獲取時,首先會去讀volatile變數。

非公平鎖獲取時,首先會用CAS更新volatile變數,這個操作同時具有volatile讀和volatile寫的記憶體語義。

concurrent包實現示意圖

final域記憶體知識

final域重排序規則

在建構函式內對一個final域的寫入,與隨後把這個被構造物件的引用賦值給一個引用變數,這兩個操作之間不能重排序。

初次讀一個包含final域的物件的引用,與隨後初次讀這個final域,這兩個操作之間不能重排序

假設一個執行緒A執行writer()方法,隨後另一個執行緒B執行reader()方法

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

讀到普通變數初始化之前的值

物件的普通域的操作被處理器重排序到讀物件引用之前。讀普通域時,該域還沒有被寫執行緒A寫入,這是一個錯誤的讀取操作。而讀final域的重排序規則會把讀物件final域的操作“限定”在讀物件引用之後,此時該final域已經被A執行緒初始化過了,這是一個正確的讀取操作。

被final修飾的型別為引用型別

在建構函式內對一個final引用的物件的成員域的寫入,與隨後在建構函式外把這個被構造物件的引用賦值給一個引用變數,這兩個操作之間不能重排序。

  • 1是對final域的寫入,2是對這個final域引用的物件的成員域的寫入,3是把被構造的物件的引用賦值給某個引用變數。這裡除了前面提到的1不能和3重排序外,2和3也不能重排序。

  • JMM可以確保讀執行緒C至少能看到寫執行緒A在建構函式中對final引用物件的成員域的寫入。即C至少能看到陣列下標0的值為1。而寫執行緒B對陣列元素的寫入,讀執行緒C可能看得到,也可能看不到。JMM不保證執行緒B的寫入對讀執行緒C
    可見,因為寫執行緒B和讀執行緒C之間存在資料競爭,此時的執行結果不可預知。

  • 如果想要確保讀執行緒C看到寫執行緒B對陣列元素的寫入,寫執行緒B和讀執行緒C之間需要使用同步原語(lock或volatile)來確保記憶體可見性。

為什麼final引用不能從建構函式內溢位

在引用變數為任意執行緒可見之前,該引用變數指向的物件的final域已經在建構函式中被正確初始化過了

在建構函式內部,不能讓這個被構造物件的引用為其他執行緒所見,也就是物件引用不能在建構函式中“逸出”。

執行read()方法的執行緒仍然可能無法看到final域被初始化後的值,因為這裡的操作1和操作2之間可能被重排序。


final語義在處理器中的實現

寫final域的重排序規則會要求編譯器在final域的寫之後,建構函式return之前插入一個StoreStore障屏。讀final域的重排序規則要求編譯器在讀final域的操作前面插入一個LoadLoad屏障。由於X86處理器不會對寫-寫操作做重排序,所以在X86處理器中,寫final域需要的StoreStore障屏會被省略掉。同樣,由於X86處理器不會對存在間接依賴關係的操作做重排序,所以在X86處理器中,讀final域需要的LoadLoad屏障也會被省略掉。也就是說,在X86處理器中,final域的讀/寫不會插入任何記憶體屏障!(在x86處理器中僅有StoreLoad屏障)

JMM相關內容

在x86架構下僅有StoreLoad屏障

詳情請見

JMM記憶體模型設計原則

對於會改變程式執行結果的重排序,JMM要求編譯器和處理器必須禁止這種重排序。

對於不會改變程式執行結果的重排序,JMM對編譯器和處理器不做要求(JMM允許這種重排序)。

happens-before關係的定義

  1. 如果一個操作happens-before另一個操作,那麼第一個操作的執行結果將對第二個操作可見,而且第一個操作的執行順序排在第二個操作之前。

  2. 兩個操作之間存在happens-before關係,並不意味著Java平臺的具體實現必須要按照happens-before關係指定的順序來執行。如果重排序之後的執行結果,與按happens-before
    關係來執行的結果一致,那麼這種重排序並不非法(也就是說,JMM允許這種重排序)。

  • 上面的1.是JMM對程式設計師的承諾。從程式設計師的角度來說,可以這樣理解happens-before關係:如果A happens-before B,那麼Java記憶體模型將向程式設計師保證——A操作的結果將對B可見,且A的執行順序排在B
    之前。注意,這只是Java記憶體模型向程式設計師做出的保證!
  • 上面的2.是JMM對編譯器和處理器重排序的約束原則。正如前面所言,JMM其實是在遵循一個基本原則:只要不改變程式的執行結果(指的是單執行緒程式和正確同步的多執行緒程式),編譯器和處理器怎麼優化都行。JMM
    這麼做的原因是:程式設計師對於這兩個操作是否真的被重排序並不關心,程式設計師關心的是程式執行時的語義不能被改變(即執行結果不能被改變)。因此,happens-before關係本質上和as-if-serial語義是一回事。

happens-before 和 as-if-serial 異同點

相同點:

  • as-if-serial語義和happens-before這麼做的目的,都是為了在不改變程式執行結果的前提下,儘可能地提高程式執行的並行度。

不同點:

  • as-if-serial語義保證單執行緒內程式的執行結果不被改變,happens-before關係保證正確同步的多執行緒程式的執行結果不被改變。
  • as-if-serial語義給編寫單執行緒程式的程式設計師創造了一個幻境:單執行緒程式是按程式的順序來執行的。happens-before關係給編寫正確同步的多執行緒程式的程式設計師創造了一個幻境:正確同步的多執行緒程式是按happens
    -before指定的順序來執行的。

happens-before規則

  1. 程式順序規則:一個執行緒中的每個操作,happens-before於該執行緒中的任意後續操作。
  2. 監視器鎖規則:對一個鎖的解鎖,happens-before於隨後對這個鎖的加鎖。
  3. volatile變數規則:對一個volatile域的寫,happens-before於任意後續對這個volatile域的讀。
  4. 傳遞性:如果A happens-before B,且B happens-before C,那麼A happens-beforeC。
  5. start()規則:如果執行緒A執行操作ThreadB.start()(啟動執行緒B),那麼A執行緒的ThreadB.start()操作happens-before於執行緒B中的任意操作。
  6. join()規則:如果執行緒A執行操作ThreadB. join()併成功返回,那麼執行緒B中的任意操作happens-before於執行緒A從ThreadB. join()操作成功返回。

  • 1 happens-before 2和3 happens-before 4由程式順序規則產生。由於編譯器和處理器都要遵守as-if-serial語義,也就是說,as-if-serial
    語義保證了程式順序規則。因此,可以把程式順序規則看成是對as-if-serial語義的“封裝”。
  • 2 happens-before 3是由volatile規則產生。前面提到過,對一個volatile變數的讀,總是能看到(任意執行緒)之前對這個volatile變數最後的寫入。因此,volatile
    的這個特性可以保證實現volatile規則。
  • 1 happens-before 4是由傳遞性規則產生的。這裡的傳遞性是由volatile的記憶體屏障插入策略和volatile的編譯器重排序規則共同來保證的。

多執行緒併發初始化物件可能發生指令重排



這裡A2和A3雖然重排序了,但Java記憶體模型的intra-thread semantics將確保A2一定會排在A4前面執行。因此,執行緒A的intra-thread semantics沒有改變,但A2和A3的重排序,將導致執行緒B在B1處判斷出instance不為空,執行緒B接下來將訪問instance引用的物件。此時,執行緒B將會訪問到一個還未初始化的物件。

在知曉了問題發生的根源之後,我們可以想出兩個辦法來實現執行緒安全的延遲初始化。

  • 不允許2和3重排序。
  • 允許2和3重排序,但不允許其他執行緒“看到”這個重排序。

基於volatile的解決方案


這個方案本質上是通過禁止圖3-39中的2和3之間的重排序,來保證執行緒安全的延遲初始化

基於類初始化的解決方案

在執行類的初始化期間,JVM會去獲取一個鎖。這個鎖可以同步多個執行緒對同一個類的初始化。


在首次發生下列任意一種情況時,一個類或介面型別T將被立即初始化

  • T是一個類,而且一個T型別的例項被建立。
  • T是一個類,且T中宣告的一個靜態方法被呼叫。
  • T中宣告的一個靜態欄位被賦值。
  • T中宣告的一個靜態欄位被使用,而且這個欄位不是一個常量欄位。
  • T是一個頂級類(Top Level Class,見Java語言規範的§7.6),而且一個斷言語句巢狀在T內部被執行。

類初始化過程

第1階段:通過在Class物件上同步(即獲取Class物件的初始化鎖),來控制類或介面的初始化。這個獲取鎖的執行緒會一直等待,直到當前執行緒能夠獲取到這個初始化鎖。


第2階段:執行緒A執行類的初始化,同時執行緒B在初始化鎖對應的condition上等待。


第3階段:執行緒A設定state=initialized,然後喚醒在condition中等待的所有執行緒。


第4階段:執行緒B結束類的初始化處理。

執行緒A在第2階段的A1執行類的初始化,並在第3階段的A4釋放初始化鎖;執行緒B在第4階段的B1獲取同一個初始化鎖,並在第4階段的B4之後才開始訪問這個類。根據Java記憶體模型規範的鎖規則,這裡將存在如下的happens-before關係。這個happens-before關係將保證:執行緒A執行類的初始化時的寫入操作(執行類的靜態初始化和初始化類中宣告的靜態欄位),執行緒B一定能看到。
第5階段:執行緒C執行類的初始化的處理。

在第3階段之後,類已經完成了初始化。因此執行緒C在第5階段的類初始化處理過程相對簡單一些(前面的執行緒A和B的類初始化處理過程都經歷了兩次鎖獲取-鎖釋放,而執行緒C的類初始化處理只需要經歷一次鎖獲取-鎖釋放)。執行緒A在第2階段的A1執行類的初始化,並在第3階段的A4釋放鎖;執行緒C在第5階段的C1獲取同一個鎖,並在在第5階段的C4之後才開始訪問這個類。根據Java記憶體模型規範的鎖規則,將存在如下的happens-before關係。

通過對比基於volatile的雙重檢查鎖定的方案和基於類初始化的方案,我們會發現基於類初始化的方案的實現程式碼更簡潔。但基於volatile的雙重檢查鎖定的方案有一個額外的優勢:除了可以對靜態欄位實現延遲初始化外,還可以對例項欄位實現延遲初始化。

欄位延遲初始化降低了初始化類或建立例項的開銷,但增加了訪問被延遲初始化的欄位的開銷。在大多數時候,正常的初始化要優於延遲初始化。如果確實需要對例項欄位使用執行緒安全的延遲初始化,請使用上面介紹的基於volatile的延遲初始化的方案;如果確實需要對靜態欄位使用執行緒安全的延遲初始化,請使用上面介紹的基於類初始化的方案。

處理器記憶體模型


記憶體模型劃分

放鬆程式中寫-讀操作的順序,由此產生了Total Store Ordering記憶體模型(簡稱為TSO)。

在上面的基礎上,繼續放鬆程式中寫-寫操作的順序,由此產生了Partial Store Order記憶體模型(簡稱為PSO)。

在前面兩條的基礎上,繼續放鬆程式中讀-寫和讀-讀操作的順序,由此產生了RelaxedMemory Order記憶體模型(簡稱為RMO)和PowerPC記憶體模型。

這裡處理器對讀/寫操作的放鬆,是以兩個操作之間不存在資料依賴性為前提的。

從表3-12中可以看到,所有處理器記憶體模型都允許寫-讀重排序,原因在第1章已經說明過:它們都使用了寫快取區。寫快取區可能導致寫-讀操作重排序。同時,我們可以看到這些處理器記憶體模型都允許更早讀到當前處理器的寫,原因同樣是因為寫快取區。由於寫快取區僅對當前處理器可見,這個特性導致當前處理器可以比其他處理器先看到臨時儲存在自己寫快取區中的寫。表3-12中的各種處理器記憶體模型,從上到下,模型由強變弱。越是追求效能的處理器,記憶體模型設計得會越弱。因為這些處理器希望記憶體模型對它們的束縛越少越好,這樣它們就可以做盡可能多的優化來提高效能。

由於常見的處理器記憶體模型比JMM要弱,Java編譯器在生成位元組碼時,會在執行指令序列的適當位置插入記憶體屏障來限制處理器的重排序。同時,由於各種處理器記憶體模型的強弱不同,為了在不同的處理器平臺向程式設計師展示一個一致的記憶體模型,JMM在不同的處理器中需要插入的記憶體屏障的數量和種類也不相同。

JMM遮蔽了不同處理器記憶體模型的差異,它在不同的處理器平臺之上為Java程式設計師呈現了一個一致的記憶體模型。

各種記憶體模型之間的關係

JMM是一個語言級的記憶體模型,處理器記憶體模型是硬體級的記憶體模型,順序一致性記憶體模型是一個理論參考模型。下面是語言記憶體模型、處理器記憶體模型和順序一致性記憶體模型的強弱對比示意圖,如圖3-49所示。

從圖中可以看出:常見的4種處理器記憶體模型比常用的3中語言記憶體模型要弱,處理器記憶體模型和語言記憶體模型都比順序一致性記憶體模型要弱。同處理器記憶體模型一樣,越是追求執行效能的語言,記憶體模型設計得會越弱。

JMM的記憶體可見性保證

  • 單執行緒程式。單執行緒程式不會出現記憶體可見性問題。編譯器、runtime和處理器會共同確保單執行緒程式的執行結果與該程式在順序一致性模型中的執行結果相同。

  • 正確同步的多執行緒程式。正確同步的多執行緒程式的執行將具有順序一致性(程式的執行結果與該程式在順序一致性記憶體模型中的執行結果相同)。這是JMM關注的重點,JMM通過限制編譯器和處理器的重排序來為程式設計師提供記憶體可見性保證。

  • 未同步/未正確同步的多執行緒程式。JMM為它們提供了最小安全性保障:執行緒執行時讀取到的值,要麼是之前某個執行緒寫入的值,要麼是預設值(0、null、false)。

最小安全性保障與64位資料的非原子性寫並不矛盾。它們是兩個不同的概念,它們“發生”的時間點也不同。

最小安全性“發生”在物件被任意執行緒使用之前。64位資料的非原子性寫“發生”在物件被多個執行緒使用的過程中(寫共享變數)。

64位資料的非原子性寫“發生”在物件被多個執行緒使用的過程中(寫共享變數)。當發生問題時(處理器B看到僅僅被處理器A“寫了一半”的無效值),這裡雖然處理器B讀取到一個被寫了一半的無效值,但這個值仍然是處理器A寫入的,只不過是處理器A還沒有寫完而已。

最小安全性保證執行緒讀取到的值,要麼是之前某個執行緒寫入的值,要麼是預設值(0、null、false)。但最小安全性並不保證執行緒讀取到的值,一定是某個執行緒寫完後的值。最小安全性保證執行緒讀取到的值不會無中生有的冒出來,但並不保證執行緒讀取到的值一定是正確的。

JSR-133對舊記憶體模型的修補

增強volatile的記憶體語義。舊記憶體模型允許volatile變數與普通變數重排序。JSR-133嚴格限制volatile變數與普通變數的重排序,使volatile的寫-讀和鎖的釋放-獲取具有相同的記憶體語義。

增強final的記憶體語義。在舊記憶體模型中,多次讀取同一個final變數的值可能會不相同。為此,JSR-133為final增加了兩個重排序規則。在保證final引用不會從建構函式內逸出的情況下,final具有了初始化安全性。

java執行緒狀態

執行緒狀態

執行緒狀態之間的變化

Daemon執行緒

Daemon執行緒被用作完成支援性工作,但是在Java虛擬機器退出時Daemon執行緒中的finally塊並不一定會執行。


main執行緒(非Daemon執行緒)在啟動了執行緒DaemonRunner之後隨著main方法執行完畢而終止,而此時Java虛擬機器中已經沒有非Daemon執行緒,虛擬機器需要退出。Java虛擬機器中的所有Daemon執行緒都需要立即終止,因此DaemonRunner立即終止,但是DaemonRunner中的finally塊並沒有執行。

執行緒如何初始化

一個新構造的執行緒物件是由其parent執行緒來進行空間分配的,而child執行緒繼承了parent是否為Daemon、優先順序和載入資源的contextClassLoader以及可繼承的ThreadLocal,同時還會分配一個唯一的ID來標識這個child執行緒。至此,一個能夠執行的執行緒物件就初始化好了,在堆記憶體中等待著執行。

執行緒start()方法的含義是:當前執行緒(即parent執行緒)同步告知Java虛擬機器,只要執行緒規劃器空閒,應立即啟動呼叫start()方法的執行緒。

執行緒中斷 和 中斷異常

中斷好比其他執行緒對該執行緒打了個招呼,其他執行緒通過呼叫該執行緒的interrupt()方法對其進行中斷操作。

執行緒通過檢查自身是否被中斷來進行響應,執行緒通過方法isInterrupted()來進行判斷是否被中斷,也可以呼叫靜態方法Thread.interrupted()對當前執行緒的中斷標識位進行復位。如果該執行緒已經處於終結狀態,即使該執行緒被中斷過,在呼叫該執行緒物件的isInterrupted()時依舊會返回false。

從Java的API中可以看到,許多宣告丟擲InterruptedException的方法(例如Thread.sleep(long millis)方法)這些方法在丟擲InterruptedException之前,Java虛擬機器會先將該執行緒的中斷標識位清除,然後丟擲InterruptedException,此時呼叫isInterrupted()方法將會返回false。

public class Interrupted {
    public static void main(String[] args) throws Exception {
        // sleepThread不停的嘗試睡眠
        Thread sleepThread = new Thread(new SleepRunner(), "SleepThread");
        sleepThread.setDaemon(true);
        // busyThread不停的執行
        Thread busyThread = new Thread(new BusyRunner(), "BusyThread");
        busyThread.setDaemon(true);
        sleepThread.start();
        busyThread.start();
        // 休眠5秒,讓sleepThread和busyThread充分執行
        TimeUnit.SECONDS.sleep(5);
        sleepThread.interrupt();
        busyThread.interrupt();
        System.out.println("SleepThread interrupted is " + sleepThread.isInterrupted());
        System.out.println("BusyThread interrupted is " + busyThread.isInterrupted());
        // 防止sleepThread和busyThread立刻退出
        SleepUtils.second(2);
    }
    static class SleepRunner implements Runnable {
        @Override
        public void run() {
            while (true) {
                SleepUtils.second(10);
            }
        }
    }
    static class BusyRunner implements Runnable {
        @Override
        public void run() {
            while (true) {
            }
        }
    }
}

丟擲InterruptedException的執行緒SleepThread,其中斷標識位被清除了,而一直忙碌運作的執行緒BusyThread,中斷標識位沒有被清除。

synchronized實現細節

本質是對一個物件的監視器(monitor)進行獲取,而這個獲取過程是排他的,也就是同一時刻只能有一個執行緒獲取到由synchronized所保護物件的監視器。

一個執行緒對Object(Object由synchronized保護)的訪問,首先要獲得Object的監視器。如果獲取失敗,執行緒進入同步佇列,執行緒狀態變為BLOCKED。當訪問Object
的前驅(獲得了鎖的執行緒)釋放了鎖,則該釋放操作喚醒阻塞在同步佇列中的執行緒,使其重新嘗試對監視器的獲取。

等待通知

等待/通知機制,是指一個執行緒A呼叫了物件O的wait()方法進入等待狀態,而另一個執行緒B呼叫了物件O的notify()或者notifyAll()方法,執行緒A收到通知後從物件O的wait()方法返回,進而執行後續操作。上述兩個執行緒通過物件O來完成互動,而物件上的wait()和notify/notifyAll()的關係就如同開關訊號一樣,用來完成等待方和通知方之間的互動工作。

public class WaitNotify {
    static boolean flag = true;
    static Object lock = new Object();

    public static void main(String[] args) throws Exception {
        Thread waitThread = new Thread(new Wait(), "WaitThread");
        waitThread.start();
        TimeUnit.SECONDS.sleep(1);
        Thread notifyThread = new Thread(new Notify(), "NotifyThread");
        notifyThread.start();
    }

    static class Wait implements Runnable {
        public void run() {
            // 加鎖,擁有lock的Monitor
            synchronized (lock) {
                // 當條件不滿足時,繼續wait,同時釋放了lock的鎖
                while (flag) {
                    try {
                        System.out.println(Thread.currentThread()+ " flagistrue.wait
                        @ " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
                        lock.wait();
            } catch (InterruptedException e) {
            }
     }
     // 條件滿足時,完成工作
     System.out.println(Thread.currentThread() + " flag is false. running
     @ " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
              }
          }
      }
      static class Notify implements Runnable {
          public void run() {
              // 加鎖,擁有lock的Monitor
              synchronized (lock) {
                  // 獲取lock的鎖,然後進行通知,通知時不會釋放lock的鎖,
                  // 直到當前執行緒釋放了lock後,WaitThread才能從wait方法中返回
                  System.out.println(Thread.currentThread() + " hold lock. notify @ " +
                  new SimpleDateFormat("HH:mm:ss").format(new Date()));
                  lock.notifyAll();
                  flag = false;
                  SleepUtils.second(5);
              }
              // 再次加鎖
              synchronized (lock) {
                  System.out.println(Thread.currentThread() + " hold lock again. sleep
                  @ " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
                  SleepUtils.second(5);
              }
          }
      }
}

public class SleepUtils {
	public static final void second(long seconds) {
		try {
			TimeUnit.SECONDS.sleep(seconds);
		} catch (InterruptedException e){

		}
	}
}

呼叫wait()、notify()以及notifyAll()時需要注意的細節

  1. 使用wait()、notify()和notifyAll()時需要先對呼叫物件加鎖。
  2. 呼叫wait()方法後,執行緒狀態由RUNNING變為WAITING,並將當前執行緒放置到物件的等待佇列。
  3. notify()或notifyAll()方法呼叫後,等待執行緒依舊不會從wait()返回,需要呼叫notify()或notifAll()的執行緒釋放鎖之後,等待執行緒才有機會從wait()返回。
  4. notify()方法將等待佇列中的一個等待執行緒從等待佇列中移到同步佇列中,而notifyAll()方法則是將等待佇列中所有的執行緒全部移到同步佇列,被移動的執行緒狀態由WAITING變為BLOCKED。
  5. 從wait()方法返回的前提是獲得了呼叫物件的鎖。

WaitThread首先獲取了物件的鎖,然後呼叫物件的wait()方法,從而放棄了鎖並進入了物件的等待佇列WaitQueue中,進入等待狀態。由於WaitThread釋放了物件的鎖,NotifyThread隨後獲取了物件的鎖,並呼叫物件的notify()方法,將WaitThread從WaitQueue移到SynchronizedQueue中,此時WaitThread的狀態變為阻塞狀態。NotifyThread釋放了鎖之後,WaitThread再次獲取到鎖並從wait()方法返回繼續執行。

ThreadLocal 變數使用

連線池案例 連線數增加則總連結數增加,同時為獲取到的比例也在增加

/**
 * 從連線池中獲取、使用和釋放連線的過程,
 * 而客戶端獲取連線的過程被設定為等待超時的模式,
 * 也就是在1000毫秒內如果無法獲取到可用連線,
 * 將會返回給客戶端一個null。設定連線池的大小為10個,
 * 然後通過調節客戶端的執行緒數來模擬無法獲取連線的場景。
 */
public class ConnectionPool {
    private LinkedList<Connection> pool = new LinkedList<Connection>();

    public ConnectionPool(int initialSize) {
        if (initialSize > 0) {
            for (int i = 0; i < initialSize; i++) {
                pool.addLast(ConnectionDriver.createConnection());
            }
        }
    }

    public void releaseConnection(Connection connection) {
        if (connection != null) {
            synchronized (pool) {
                // 連線釋放後需要進行通知,這樣其他消費者能夠感知到連線池中已經歸還了一個連線
                pool.addLast(connection);
                pool.notifyAll();
            }
        }
    }

    // 在mills內無法獲取到連線,將會返回null
    public Connection fetchConnection(long mills) throws InterruptedException {
        synchronized (pool) {
            // 完全超時
            if (mills <= 0) {
                while (pool.isEmpty()) {
                    pool.wait();
                }
                return pool.removeFirst();
            } else {
                long future = System.currentTimeMillis() + mills;
                long remaining = mills;
                while (pool.isEmpty() && remaining > 0) {
                    pool.wait(remaining);
                    remaining = future - System.currentTimeMillis();
                }
                Connection result = null;
                if (!pool.isEmpty()) {
                    result = pool.removeFirst();
                }
                return result;
            }
        }
    }
}
/**
 * 我們通過動態代理構造了一個Connection,該Connection的代理實現僅僅
 * 是在commit()方法呼叫時休眠100毫秒
 */
public class ConnectionDriver {
    static class ConnectionHandler implements InvocationHandler {
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            if (method.getName().equals("commit")) {
                TimeUnit.MILLISECONDS.sleep(100);
            }
            return null;
        }
    }

    // 建立一個Connection的代理,在commit時休眠100毫秒
    public static final Connection createConnection() {
        return (Connection) Proxy.newProxyInstance(ConnectionDriver.class.getClassLoader(),
                new Class<?>[] { Connection.class }, new ConnectionHandler());
    }
}
/**
 * 使用了CountDownLatch來確保ConnectionRunnerThread能夠同時開始執行,
 * 並且在全部結束之後,才使main執行緒從等待狀態中返回。
 * 當前設定的場景是10個執行緒同時執行獲取連線池(10個連線)中的連線,
 * 通過調節執行緒數量來觀察未獲取到連線的情況
 */
public class ConnectionPoolTest {
    static ConnectionPool pool    = new ConnectionPool(10);
    // 保證所有ConnectionRunner能夠同時開始
    static CountDownLatch start    = new CountDownLatch(1);
    // main執行緒將會等待所有ConnectionRunner結束後才能繼續執行
    static CountDownLatch end;

    public static void main(String[] args) throws Exception {
        // 執行緒數量,可以修改執行緒數量進行觀察
        int threadCount = 10;
        end = new CountDownLatch(threadCount);
        int count = 20;
        AtomicInteger got = new AtomicInteger();
        AtomicInteger notGot = new AtomicInteger();
        for (int i = 0; i < threadCount; i++) {
            Thread thread = new Thread(new ConnetionRunner(count, got, notGot),
                    "ConnectionRunnerThread");
            thread.start();
        }
        start.countDown();
        end.await();
        System.out.println("total invoke: " + (threadCount * count));
        System.out.println("got connection: " + got);
        System.out.println("not got connection " + notGot);
    }

    static class ConnetionRunner implements Runnable {
        int        count;
        AtomicInteger    got;
        AtomicInteger    notGot;

        public ConnetionRunner(int count, AtomicInteger got, AtomicInteger notGot) {
            this.count = count;
            this.got = got;
            this.notGot = notGot;
        }

        public void run() {
            try {
                start.await();
            } catch (Exception ex) {
            }
            while (count > 0) {
                try {
                    // 從執行緒池中獲取連線,如果1000ms內無法獲取到,將會返回null
                    // 分別統計連線獲取的數量got和未獲取到的數量notGot
                    Connection connection = pool.fetchConnection(1000);
                    if (connection != null) {
                        try {
                            connection.createStatement();
                            connection.commit();
                        } finally {
                            pool.releaseConnection(connection);
                            got.incrementAndGet();
                        }
                    } else {
                        notGot.incrementAndGet();
                    }
                } catch (Exception ex) {
                } finally {
                    count--;
                }
            }
            end.countDown();
        }
    }
}

執行緒池

public class DefaultThreadPool<Job extends Runnable> implements ThreadPool<Job> {
    // 執行緒池最大限制數
    private static final intMAX_WORKER_NUMBERS = 10;
    // 執行緒池預設的數量
    private static final int    DEFAULT_WORKER_NUMBERS = 5;
    // 執行緒池最小的數量
    private static final int    MIN_WORKER_NUMBERS= 1;
    // 這是一個工作列表,將會向裡面插入工作
    private final LinkedList<Job>    jobs = new LinkedList<Job>();
    // 工作者列表
    private final List<Worker>    workers    = Collections.synchronizedList(new
    ArrayList<Worker>());
    // 工作者執行緒的數量
    private int  workerNum = DEFAULT_WORKER_NUMBERS;
    // 執行緒編號生成
    private AtomicLong    threadNum    = new AtomicLong();

    public DefaultThreadPool() {
    initializeWokers(DEFAULT_WORKER_NUMBERS);
    }

    public DefaultThreadPool(int num) {
        workerNum = num > MAX_WORKER_NUMBERS ? MAX_WORKER_NUMBERS : num < MIN_WORKER_
        NUMBERS ? MIN_WORKER_NUMBERS : num;
        initializeWokers(workerNum);
    }

    public void execute(Job job) {
        if (job != null) {
            // 新增一個工作,然後進行通知
            synchronized (jobs) {
                jobs.addLast(job);
                jobs.notify();
            }
        }
    }

    public void shutdown() {
        for (Worker worker : workers) {
            worker.shutdown();
        }
    }

    public void addWorkers(int num) {
        synchronized (jobs) {
            // 限制新增的Worker數量不能超過最大值
            if (num + this.workerNum > MAX_WORKER_NUMBERS) {
                num = MAX_WORKER_NUMBERS - this.workerNum;
            }
            initializeWokers(num);
            this.workerNum += num;
        }
    }

    public void removeWorker(int num) {
        synchronized (jobs) {
            if (num >= this.workerNum) {
                throw new IllegalArgumentException("beyond workNum");
            }
            // 按照給定的數量停止Worker
            int count = 0;
            while (count < num) {
                Worker worker = workers.get(count)
                if (workers.remove(worker)) {
                worker.shutdown();
                      count++;
                }
            }
            this.workerNum -= count;
        }
    }

    public int getJobSize() {
        return jobs.size();
    }
    // 初始化執行緒工作者
    private void initializeWokers(int num) {
        for (int i = 0; i < num; i++) {
            Worker worker = new Worker();
            workers.add(worker);
            Thread thread = new Thread(worker, "ThreadPool-Worker-" + threadNum.
            incrementAndGet());
            thread.start();
        }
    }

    // 工作者,負責消費任務
    class Worker implements Runnable {
        // 是否工作
        private volatile boolean running= true;
        public void run() {
            while (running) {
                Job job = null;
                synchronized (jobs) {
                    // 如果工作者列表是空的,那麼就wait
                    while (jobs.isEmpty()) {
                        try {
                            jobs.wait();
                        } catch (InterruptedException ex) {
                            // 感知到外部對WorkerThread的中斷操作,返回
                            Thread.currentThread().interrupt();
                            return;
                        }
                     }
                     // 取出一個Job
                     job = jobs.removeFirst();
                }
                if (job != null) {
                    try {
                        job.run();
                    } catch (Exception ex) {
                        // 忽略Job執行中的Exception
                    }
                }
             }
          }

          public void shutdown() {
              running = false;
          }
    }
}

lock鎖

鎖和同步器AQS概念區別

鎖是面向使用者的,它定義了使用者與鎖互動的介面(比如可以允許兩個執行緒並行訪問),隱藏了實現細節;

同步器面向的是鎖的實現者,它簡化了鎖的實現方式,遮蔽了同步狀態管理、執行緒的排隊、等待與喚醒等底層操作。鎖和同步器很好地隔離了使用者和實現者所需關注的領域

因此同步器提供了一個基於CAS的設定尾節點的方法:compareAndSetTail(Node expect, Node update),它需要傳遞當前執行緒“認為”的尾節點和當前節點,只有設定成功後,當前節點才正式與之前的尾節點建立關聯。

同步佇列遵循FIFO,首節點是獲取同步狀態成功的節點,首節點的執行緒在釋放同步狀態時,將會喚醒後繼節點,而後繼節點將會在獲取同步狀態成功時將自己設定為首節點,如下圖所示

設定首節點是通過獲取同步狀態成功的執行緒來完成的,由於只有一個執行緒能夠成功獲取到同步狀態,因此設定頭節點的方法並不需要使用CAS來保證,它只需要將首節點設定成為原首節點的後繼節點並斷開原首節點的next引用即可。

參考資料

  1. 書籍名稱:《java併發程式設計的藝術》 作者:方騰飛 魏鵬 程曉明

歡迎關注微信公眾號哦~ ~

相關文章