從一個簡單的Java單例示例談談併發

Martin Zhao發表於2016-05-10

一個簡單的單例示例

單例模式可能是大家經常接觸和使用的一個設計模式,你可能會這麼寫

public class UnsafeLazyInitiallization {
    private static UnsafeLazyInitiallization instance;
    private UnsafeLazyInitiallization() {
    }
    public static UnsafeLazyInitiallization getInstance(){
        if(instance==null){   //1:A執行緒執行
            instance=new UnsafeLazyInitiallization();  //2:B執行緒執行
        }
        return instance;
    }
}

上面程式碼大家應該都知道,所謂的執行緒不安全的懶漢單例寫法。在UnsafeLazyInitiallization類中,假設A執行緒執行程式碼1的同時,B執行緒執行程式碼2,此時,執行緒A可能看到instance引用的物件還沒有初始化。

你可能會說,執行緒不安全,我可以對getInstance()方法做同步處理保證安全啊,比如下面這樣的寫法

 public class SafeLazyInitiallization {
        private static SafeLazyInitiallization instance;
        private SafeLazyInitiallization() {
        }
        public synchronized static SafeLazyInitiallization getInstance(){
            if(instance==null){
                instance=new SafeLazyInitiallization();
            }
            return instance;
        }
    }

這樣的寫法是保證了執行緒安全,但是由於getInstance()方法做了同步處理,synchronized將導致效能開銷。如getInstance()方法被多個執行緒頻繁呼叫,將會導致程式執行效能的下降。反之,如果getInstance()方法不會被多個執行緒頻繁的呼叫,那麼這個方案將能夠提供令人滿意的效能。

那麼,有沒有更優雅的方案呢?前人的智慧是偉大的,在早期的JVM中,synchronized存在巨大的效能開銷,因此,人們想出了一個“聰明”的技巧——雙重檢查鎖定。人們通過雙重檢查鎖定來降低同步的開銷。下面來讓我們看看

public class DoubleCheckedLocking { //1
    private static DoubleCheckedLocking instance; //2
    private DoubleCheckedLocking() {
    }
    public static DoubleCheckedLocking getInstance() { //3
        if (instance == null) { //4:第一次檢查
            synchronized (DoubleCheckedLocking.class) { //5:加鎖
                if (instance == null) //6:第二次檢查
                    instance = new DoubleCheckedLocking(); //7:問題的根源出在這裡
            } //8
        } //9
        return instance; //10
    } //11
}

如上面程式碼所示,如果第一次檢查instance不為null,那麼就不需要執行下面的加鎖和初始化操作。因此,可以大幅降低synchronized帶來的效能開銷。雙重檢查鎖定看起來似乎很完美,但這是一個錯誤的優化!為什麼呢?線上程執行到第4行,程式碼讀取到instance不為null時,instance引用的物件有可能還沒有完成初始化。在第7行建立了一個物件,這行程式碼可以分解為如下的3行虛擬碼

memory=allocate(); //1:分配物件的記憶體空間
ctorInstance(memory); //2:初始化物件
instance=memory; //3:設定instance指向剛分配的記憶體地址

上面3行程式碼中的2和3之間,可能會被重排序(在一些JIT編譯器上,這種重排序是真實發生的,如果不瞭解重排序,後文JMM會詳細解釋)。2和3之間重排序之後的執行時序如下

memory=allocate(); //1:分配物件的記憶體空間
instance=memory; //3:設定instance指向剛分配的記憶體地址,注意此時物件還沒有被初始化
ctorInstance(memory); //2:初始化物件

回到示例程式碼第7行,如果發生重排序,另一個併發執行的執行緒B就有可能在第4行判斷instance不為null。執行緒B接下來將訪問instance所引用的物件,但此時這個物件可能還沒有被A執行緒初始化。在知曉問題發生的根源之後,我們可以想出兩個辦法解決

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

下面就介紹這兩個解決方案的具體實現

基於Volatile的解決方案

對於前面的基於雙重檢查鎖定的方案,只需要做一點小的修改,就可以實現執行緒安全的延遲初始化。請看下面的示例程式碼

public class SafeDoubleCheckedLocking {
    private volatile static SafeDoubleCheckedLocking instance;

    private SafeDoubleCheckedLocking() {
    }

    public static SafeDoubleCheckedLocking getInstance() {

        if (instance == null) {

            synchronized (SafeDoubleCheckedLocking.class) {

                if (instance == null)

                    instance = new SafeDoubleCheckedLocking();//instance為volatile,現在沒問題了

            }

        }

        return instance;

    }

}

當宣告物件的引用為volatile後,前面虛擬碼談到的2和3之間的重排序,在多執行緒環境中將會被禁止。

基於類初始化的解決方案

JVM在類的初始化階段(即在Class被載入後,且被執行緒使用之前),會執行類的初始化。在執行類的初始化期間,JVM會去獲取多個執行緒對同一個類的初始化。基於這個特性,實現的示例程式碼如下

public class InstanceFactory {

    private InstanceFactory() {
    }

    private static class InstanceHolder {

        public static InstanceFactory instance = new InstanceFactory();

    }

    public static InstanceFactory getInstance() {

        return InstanceHolder.instance; //這裡將導致InstanceHolder類被初始化

    }

}

這個方案的本質是允許前面虛擬碼談到的2和3重排序,但不允許其他執行緒“看到”這個重排序。在InstanceFactory示例程式碼中,首次執行getInstance()方法的執行緒將導致InstanceHolder類被初始化。由於Java語言是多執行緒的,多個執行緒可能在同一時間嘗試去初始化同一個類或介面(比如這裡多個執行緒可能會在同一時刻呼叫getInstance()方法來初始化IInstanceHolder類)。Java語言規定,對於每一個類和介面C,都有一個唯一的初始化鎖LC與之對應。從C到LC的對映,由JVM的具體實現去自由實現。JVM在類初始化期間會獲取這個初始化鎖,並且每個執行緒至少獲取一次鎖來確保這個類已經被初始化過了。

JMM

也許你還存在疑問,前面談的重排序是什麼鬼?為什麼volatile在某方面就能禁止重排序?現在引出本文的另一個話題JMM(Java Memory Model——Java記憶體模型)。什麼是JMM呢?JMM是一個抽象概念,它並不存在。Java虛擬機器規範中試圖定義一種Java記憶體模型(JMM)來遮蔽掉各種硬體和作業系統的記憶體訪問差異,以實現讓Java程式在各種平臺下都能達到一致的記憶體訪問效果。在此之前,主流程式語言(如C/C++等)直接使用物理硬體和作業系統的記憶體模型,因此,會由於不同平臺的記憶體模型的差異,有可能導致程式在一套平臺上併發完全正常,而在另一套平臺上併發訪問卻經常出錯,因此在某些場景就必須針對不同的平臺來編寫程式。

Java執行緒之間的通訊由JMM來控制,JMM決定一個執行緒共享變數的寫入何時對另一個執行緒可見。JMM保證如果程式是正確同步的,那麼程式的執行將具有順序一致性。從抽象的角度看,JMM定義了執行緒和主記憶體之間的抽象關係:執行緒之間的共享變數(例項域、靜態域和資料元素)儲存在主記憶體(Main Memory)中,每個執行緒都有一個私有的本地記憶體(Local Memory),本地記憶體中儲存了該執行緒以讀/寫共享變數的副本(區域性變數、方法定義引數和異常處理引數是不會線上程之間共享,它們儲存線上程的本地記憶體中)。從物理角度上看,主記憶體僅僅是虛擬機器記憶體的一部分,與物理硬體的主記憶體名字一樣,兩者可以互相類比;而本地記憶體,可與處理器快取記憶體類比。Java記憶體模型的抽象示意圖如圖所示

enter image description here

這裡先介紹幾個基礎概念:8種操作指令、記憶體屏障、順序一致性模型、as-if-serial、happens-before 、資料依賴性、 重排序。

8種操作指令

關於主記憶體與本地記憶體之間具體的互動協議,即一個變數如何從主記憶體拷貝到本地記憶體、如何從本地記憶體同步回主記憶體之類的實現細節,JMM中定義了以下8種操作來完成,虛擬機器實現時必須保證下面提及的每種操作都是原子的、不可再分的(對於double和long型別的遍歷來說,load、store、read和write操作在某些平臺上允許有例外):

  • lock(鎖定):作用於主記憶體的變數,它把一個變數標識為一條執行緒獨立的狀態。
  • unlock(解鎖):作用於主記憶體的變數,它把一個處於鎖定狀態的變數釋放出來,釋放後的變數才可以被其他執行緒鎖定。
  • read(讀取):作用於主記憶體的變數,它把一個變數的值從主記憶體傳輸到執行緒的本地記憶體中,以便隨後的load動作使用。
  • load(載入):作用於本地記憶體的變數,它把read操作從主記憶體中得到變數值放入本地記憶體的變數副本中。
  • use(使用):作用於本地記憶體的變數,它把本地記憶體中一個變數的值傳遞給執行引擎,每當虛擬機器遇到一個需要使用到變數的值的位元組碼指令時將會執行這個操作。
  • assign(賦值):作用於本地記憶體的變數,它把一個從執行引擎接收到的值賦給本地記憶體的變數,每當虛擬機器遇到一個給變數賦值的位元組碼指令時執行這個操作。
  • store(儲存):作用於本地記憶體的變數,它把本地記憶體中的一個變數的值傳送到主記憶體中,以便隨後的write操作使用。
  • write(寫入):作用於主記憶體的變數,它把store操作從本地記憶體中提到的變數的值放入到主記憶體的變數中。

如果要把一個變數從主記憶體模型複製到本地記憶體,那就要順序的執行read和load操作,如果要把變數從本地記憶體同步回主記憶體,就要順序的執行store和write操作。注意,Java記憶體模型只要求上述兩個操作必須按順序執行,而沒有保證是連續執行。也就是說read與load之間、store與write之間是可插入其他指令的,如對主記憶體中的變數a、b進行訪問時,一種可能出現的順序是read a read b、load b、load a。

記憶體屏障

記憶體屏障是一組處理器指令(前面的8個操作指令),用於實現對記憶體操作的順序限制。包括LoadLoad, LoadStore, StoreLoad, StoreStore共4種記憶體屏障。記憶體屏障存在的意義是什麼呢?它是在Java編譯器生成指令序列的適當位置插入記憶體屏障指令來禁止特定型別的處理器重排序,從而讓程式按我們預想的流程去執行,記憶體屏障是與相應的記憶體重排序相對應的。JMM把記憶體屏障指令分為4類

enter image description here

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

資料依賴性

如果兩個操作訪問同一個變數,且這兩個操作中有一個為寫操作,此時這兩個操作之間就存在資料依賴性。資料依賴性分3種型別:寫後讀、寫後寫、讀後寫。這3種情況,只要重排序兩個操作的執行順序,程式的執行結果就會被改變。編譯器和處理器可能對操作進行重排序。而它們進行重排序時,會遵守資料依賴性,不會改變資料依賴關係的兩個操作的執行順序。

這裡所說的資料依賴性僅針對單個處理器中執行的指令序列和單個執行緒中執行的操作,不同處理器之間和不同執行緒之間的資料依賴性不被編譯器和處理器考慮。

順序一致性記憶體模型

順序一致性記憶體模型是一個理論參考模型,在設計的時候,處理器的記憶體模型和程式語言的記憶體模型都會以順序一致性記憶體模型作為參照。它有兩個特性:

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

從順序一致性模型中,我們可以知道程式所有操作完全按照程式的順序序列執行。而在JMM中,臨界區內的程式碼可以重排序(但JMM不允許臨界區內的程式碼“逸出”到臨界區外,那樣就破壞監視器的語義)。JMM會在退出臨界區和進入臨界區這兩個關鍵時間點做一些特別處理,使得執行緒在這兩個時間點具有與順序一致性模型相同的記憶體檢視。雖然執行緒A在臨界區內做了重排序,但由於監視器互斥執行的特性,這裡的執行緒B根本無法“觀察”到執行緒A在臨界區內的重排序。這種重排序既提高了執行效率,又沒有改變程式的執行結果。像前面單例示例的類初始化解決方案就是採用了這個思想。

as-if-serial

as-if-serial的意思是不管怎麼重排序,(單執行緒)程式的執行結果不能改變。編譯器、runtime和處理器都必須遵守as-if-serial語義。為了遵守as-if-serial語義,編譯器和處理器不會對存在資料依賴關係的操作做重排序。

as-if-serial語義把單執行緒程式保護了起來,遵守as-if-serial語義的編譯器、runtime和處理器共同為編寫單執行緒程式的程式設計師建立了一個幻覺:單執行緒程式是按程式的順序來執行的。as-if-serial語義使單執行緒程式設計師無需擔心重排序會干擾他們,也無需擔心記憶體可見性問題。

happens-before

happens-before是JMM最核心的概念。從JDK5開始,Java使用新的JSR-133記憶體模型,JSR-133 使用happens-before的概念闡述操作之間的記憶體可見性,如果一個操作執行的結果需要對另一個操作可見,那麼這兩個操作之間必須存在happens-before關係。

happens-before規則如下:

  • 程式次序法則:執行緒中的每個動作 A 都 happens-before 於該執行緒中的每一個動作 B,其中,在程式中,所有的動作 B 都出現在動作 A 之後。(注:此法則只是要求遵循 as-if-serial語義)
  • 監視器鎖法則:對一個監視器鎖的解鎖 happens-before 於每一個後續對同一監視器鎖的加鎖。(顯式鎖的加鎖和解鎖有著與內建鎖,即監視器鎖相同的儲存語意。)
  • volatile變數法則:對 volatile 域的寫入操作 happens-before 於每一個後續對同一域的讀操作。(原子變數的讀寫操作有著與 volatile 變數相同的語意。)(volatile變數具有可見性和讀寫原子性。)
  • 執行緒啟動法則:在一個執行緒裡,對 Thread.start 的呼叫會 happens-before 於每一個啟動執行緒中的動作。 執行緒終止法則:執行緒中的任何動作都 happens-before 於其他執行緒檢測到這個執行緒已終結,或者從 Thread.join 方法呼叫中成功返回,或者 Thread.isAlive 方法返回false。
  • 中斷法則法則:一個執行緒呼叫另一個執行緒的 interrupt 方法 happens-before 於被中斷執行緒發現中斷(通過丟擲InterruptedException, 或者呼叫 isInterrupted 方法和 interrupted 方法)。
  • 終結法則:一個物件的建構函式的結束 happens-before 於這個物件 finalizer 開始。
  • 傳遞性:如果 A happens-before 於 B,且 B happens-before 於 C,則 A happens-before 於 C。

happens-before與JMM的關係如下圖所示

enter image description here

as-if-serial語義和happens-before本質上一樣,參考順序一致性記憶體模型的理論,在不改變程式執行結果的前提下,給編譯器和處理器以最大的自由度,提高並行度。

重排序

終於談到我們反覆提及的重排序了,重排序是指編譯器和處理器為了優化程式效能而對指令序列進行重新排序的一種手段。重排序分3種型別。

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

從Java原始碼到最終實際執行的指令序列,會分別經歷下面3種重排序

enter image description here

上述的1屬於編譯器重排序,2和3屬於處理器重排序。這些重排序可能會導致多執行緒程式出現記憶體可見性問題。對於編譯器,JMM的編譯器重排序規則會禁止特定型別的編譯器重排序(不是所有的編譯器重排序都要禁止)。對於處理器重排序,JMM的處理器重排序規則會要求Java編譯器在生成指令序列時,插入特定型別的記憶體屏障指令,通過記憶體屏障指令來禁止特定型別的處理器重排序。

JMM屬於語言級的記憶體模型,它確保在不同的編譯器和不同的處理器平臺之上,通過禁止特定型別的編譯器重排序和處理器重排序,為程式設計師提供一致的記憶體可見性保證。

從JMM設計者的角度來說,在設計JMM時,需要考慮兩個關鍵因素:

  • 程式設計師對記憶體模型的使用。程式設計師希望記憶體模型易於理解,易於程式設計。程式設計師希望基於一個強記憶體模型(程式儘可能的順序執行)來編寫程式碼。
  • 編譯器和處理器對記憶體模型的實現。編譯器和處理器希望記憶體模型對它們的束縛越少越好,這樣它們就可以做盡可能多的優化(對程式重排序,做盡可能多的併發)來提高效能。編譯器和處理器希望實現一個弱記憶體模型。

JMM設計就需要在這兩者之間作出協調。JMM對程式採取了不同的策略:

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

介紹完了這幾個基本概念,我們不難推斷出JMM是圍繞著在併發過程中如何處理原子性、可見性和有序性這三個特徵來建立的:

  • 原子性:由Java記憶體模型來直接保證的原子性操作就是我們前面介紹的8個原子操作指令,其中lock(lock指令實際在處理器上原子操作體現對匯流排加鎖或對快取加鎖)和unlock指令操作JVM並未直接開放給使用者使用,但是卻提供了更高層次的位元組碼指令monitorenter和monitorexit來隱式使用這兩個操作,這兩個位元組碼指令反映到Java程式碼中就是同步塊——synchronize關鍵字,因此在synchronized塊之間的操作也具備原子性。除了synchronize,在Java中另一個實現原子操作的重要方式是自旋CAS,它是利用處理器提供的cmpxchg指令實現的。至於自旋CAS後面J.U.C中會詳細介紹,它和volatile是整個J.U.C底層實現的核心。
  • 可見性:可見性是指一個執行緒修改了共享變數的值,其他執行緒能夠立即得知這個修改。而我們上文談的happens-before原則禁止某些處理器和編譯器的重排序,來保證了JMM的可見性。而體現在程式上,實現可見性的關鍵字包含了volatile、synchronize和final。
  • 有序性:談到有序性就涉及到前面說的重排序和順序一致性記憶體模型。我們也都知道了as-if-serial是針對單執行緒程式有序的,即使存在重排序,但是最終程式結果還是不變的,而多執行緒程式的有序性則體現在JMM通過插入記憶體屏障指令,禁止了特定型別處理器的重排序。通過前面8個操作指令和happens-before原則介紹,也不難推斷出,volatile和synchronized兩個關鍵字來保證執行緒之間的有序性,volatile本身就包含了禁止指令重排序的語義,而synchronized則是由監視器法則獲得。

J.U.C

談完了JMM,那麼Java相關類庫是如何實現的呢?這裡就談談J.U.C( java.util.concurrent),先來張J.U.C的思維導圖

enter image description here

不難看出,J.U.C由atomic、locks、tools、collections、Executor這五部分組成。它們的實現基於volatile的讀寫和CAS所具有的volatile讀和寫。AQS(AbstractQueuedSynchronizer,佇列同步器)、非阻塞資料結構和原子變數類,這些J.U.C中的基礎類都是使用了這種模式實現的,而J.U.C中的高層類又依賴於這些基礎類來實現的。從整體上看,J.U.C的實現示意圖如下

enter image description here

也許你對volatile和CAS的底層實現原理不是很瞭解,這裡先這裡先簡單介紹下它們的底層實現

volatile

Java語言規範第三版對volatile的定義為:Java程式語言允許執行緒訪問共享變數,為了確保共享變數能被準確和一致性的更新,執行緒應該確保通過排他鎖單獨獲得這個變數。如果一個欄位被宣告為volatile,Java記憶體模型確保這個所有執行緒看到這個值的變數是一致的。而volatile是如何來保證可見性的呢?如果對宣告瞭volatile的變數進行寫操作,JVM就會向處理器傳送一條Lock字首的指令,將這個變數所在快取行的資料寫回到系統記憶體(Lock指令會在聲言該訊號期間鎖匯流排/快取,這樣就獨佔了系統記憶體)。但是,就算是寫回到記憶體,如果其他處理器快取的值還是舊的,再執行計算操作就會有問題。所以,在多處理器下,為了保證各個處理器的快取是一致的,就會實現快取一致性協議,每個處理器通過嗅探在匯流排(注意處理器不直接跟系統記憶體互動,而是通過匯流排)上傳播的資料來檢查自己快取的值是不是過期了,當處理器發現直接快取行對應的記憶體地址被修改,就會將當前處理器的快取行設定成無效狀態,當處理器對這個資料進行修改操作的時候,會重新從系統記憶體中把資料讀到處理器快取裡。

CAS

CAS其實應用挺廣泛的,我們常常聽到的悲觀鎖樂觀鎖的概念,樂觀鎖(無鎖)指的就是CAS。這裡只是簡單說下在併發的應用,所謂的樂觀併發策略,通俗的說,就是先進性操作,如果沒有其他執行緒爭用共享資料,那操作就成功了,如果共享資料有爭用,產生了衝突,那就採取其他的補償措施(最常見的補償措施就是不斷重試,治到成功為止,這裡其實也就是自旋CAS的概念),這種樂觀的併發策略的許多實現都不需要把執行緒掛起,因此這種操作也被稱為非阻塞同步。而CAS這種樂觀併發策略操作和衝突檢測這兩個步驟具備的原子性,是靠什麼保證的呢?硬體,硬體保證了一個從語義上看起來需要多次操作的行為只通過一條處理器指令就能完成。

也許你會存在疑問,為什麼這種無鎖的方案一般會比直接加鎖效率更高呢?這裡其實涉及到執行緒的實現和執行緒的狀態轉換。實現執行緒主要有三種方式:使用核心執行緒實現、使用使用者執行緒實現和使用使用者執行緒加輕量級程式混合實現。而Java的執行緒實現則依賴於平臺使用的執行緒模型。至於狀態轉換,Java定義了6種執行緒狀態,在任意一個時間點,一個執行緒只能有且只有其中的一種狀態,這6種狀態分別是:新建、執行、無限期等待、限期等待、阻塞、結束。 Java的執行緒是對映到作業系統的原生執行緒之上的,如果要阻塞或喚醒一個執行緒,都需要作業系統來幫忙完成,這就需要從使用者態轉換到核心態中,因此狀態轉換需要耗費很多的處理器時間。對於簡單的同步塊(被synchronized修飾的方法),狀態轉換消耗的時間可能比使用者程式碼執行的時間還要長。所以出現了這種優化方案,在作業系統阻塞執行緒之間引入一段自旋過程或一直自旋直到成功為止。避免頻繁的切入到核心態之中。

但是這種方案其實也並不完美,在這裡就說下CAS實現原子操作的三大問題

  • ABA問題。因為CAS需要在操作值的時候,檢查值有沒有變化,如果沒有發生變化則更新,但是如果一個值原來是A,變成了B,又變成了A,那麼使用CAS進行檢查時會發現它的值沒有變化,但是實際上發生變化了。ABA解決的思路是使用版本號。在變數前面追加上版本號,每次變數更新的時候把版本號加1。JDK的Atomic包裡提供了一個類AtomicStampedReference來解決ABA問題。不過目前來說這個類比較“雞肋”,大部分情況下ABA問題不會影響程式併發的正確性,如果需要解決ABA問題,改用原來的互斥同步可能會比原子類更高效。
  • 迴圈時間長開銷大。自旋CAS如果長時間不成功,會給CPU帶來非常大的執行開銷。所以說如果是長時間佔用鎖執行的程式,這種方案並不適用於此。
  • 只能保證一個共享變數的原子操作。當對一個共享變數執行操作時,我們可以使用自旋CAS來保證原子性,但是對多個共享變數的操作時,自旋CAS就無法保證操作的原子性,這個時候可以用鎖。

談完了這兩個概念,下面我們就來逐個分析這五部分的具體原始碼實現

atomic

atomic包的原子操作類提供了一種簡單、效能高效、執行緒安全操作一個變數的方式。atomic包裡一共13個類,屬於4種型別的原子更新方式,分別是原子更新基本型別、原子更新陣列、原子更新引用、原子更新屬性。atomic包裡的類基本使用Unsafe實現的包裝類。

下面通過一個簡單的CAS方式實現計數器(一個執行緒安全的計數器方法safeCount和一個非執行緒安全的計數器方法count)的示例來說下

public class CASTest {

    public static void main(String[] args){
        final Counter cas=new Counter();
        List ts=new ArrayList(600);
        long start=System.currentTimeMillis();
        for(int j=0;j<100;j++){
            Thread t=new Thread(new Runnable() {
                @Override
                public void run() {
                    for(int i=0;i<10000;i++){
                        cas.count();
                        cas.safeCount();
                    }
                }
            });
            ts.add(t);
        }
        for(Thread t:ts){
            t.start();
        }

        for(Thread t:ts){
            try {
                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println(cas.i);
        System.out.println(cas.atomicI.get());
        System.out.println(System.currentTimeMillis()-start);
    }

}

public class Counter {
    public AtomicInteger atomicI=new AtomicInteger(0);
    public int i=0;

    /**
    * 使用CAS實現執行緒安全計數器
    */
    public void safeCount(){
        for(;;){
            int i=atomicI.get();
            boolean suc=atomicI.compareAndSet(i,++i);
            if(suc){
                break;
            }
        }
    }

    /**
    * 非執行緒安全計數器
    */
    public void count(){
        i++;
    }

}

safeCount()方法的程式碼塊其實是getandIncrement()方法的實現,原始碼for迴圈體第一步優先取得atomicI裡儲存的數值,第二步對atomicI的當前數值進行加1操作,關鍵的第三步呼叫compareAndSet()方法來進行原子更新操作,該方法先檢查當前數值是否等於current,等於意味著atomicI的值沒有被其他執行緒修改過,則將atomicI的當前數值更新成next的值,如果不等compareAndSet()方法會返回false,程式則進入for迴圈重新進行compareAndSet()方法操作進行不斷嘗試直到成功為止。在這裡我們跟蹤下compareAndSet()方法如下

public final boolean compareAndSet(int expect, int update) {
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}

從上面原始碼我們發現是使用Unsafe實現的,其實atomic裡的類基本都是使用Unsafe實現的。我們再回到這個本地方法呼叫,這個本地方法在openjdk中依次呼叫c++程式碼為unsafe.cpp、atomic.app和atomic_windows_x86.inline.hpp。關於本地方法實現的原始碼這裡就不貼出來了,其實大體上是程式會根據當前處理器的型別來決定是否為cmpxchg指令新增lock字首。如果程式是在多處理器上執行,就為cmpxchg指令加上lock字首(Lock Cmpxchg)。反之,如果程式是在單處理器上執行,就省略lock字首(單處理器自身就會維護單處理器內的順序一致性,不需要lock字首提供的記憶體屏障效果)。

locks

鎖是用來控制多個執行緒訪問共享資源的形式,Java SE 5之後,J.U.C中新增了locks來實現鎖功能,它提供了與synchronized關鍵字類似的同步功能。只是在使用時需要顯示的獲取和釋放鎖。雖然它缺少了隱式獲取和釋放鎖的便捷性,但是卻擁有了鎖獲取和釋放的可操作性、可中斷的獲取鎖及超時獲取鎖等多種synchronized關鍵字不具備的同步特性。

locks在這我們只介紹下核心的AQS(AbstractQueuedSynchronizer,佇列同步器),AQS是用來構建鎖或者其他同步元件的基礎框架,它使用一個用volatile修飾的int成員變數表示同步狀態。通過內建的FIFO佇列來完成資源獲取執行緒的排隊工作。同步器的主要使用方式是繼承,子類通過繼承同步器並實現它的抽象方法來管理同步狀態,在抽象方法的實現過程免不了要對同步狀態進行更改,這時候就會使用到AQS提供的3個方法:getState()、setState()和compareAndSetState()來進行操作,這是因為它們能夠保證狀態的改變是原子性的。為什麼這麼設計呢?因為鎖是面向使用者的,它定義了使用者與鎖互動的介面,隱藏了實現細節,而AQS面向的是鎖的實現者,它簡化了鎖的實現方式,遮蔽了同步狀態管理、執行緒的排隊、等待與喚醒等底層操作。鎖和AQS很好的隔離了使用者和實現者鎖關注的領域。

現在我們就自定義一個獨佔鎖來詳細解釋下AQS的實現機制

public class Mutex implements Lock {
    private static class Sync extends AbstractQueuedSynchronizer {
        private static final long serialVersionUID = -4387327721959839431L;

        protected boolean isHeldExclusively() {
            return getState() == 1;
        }

        public boolean tryAcquire(int acquires) {
            assert acquires == 1; // Otherwise unused
            if (compareAndSetState(0, 1)) {
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }

        protected boolean tryRelease(int releases) {
            assert releases == 1; // Otherwise unused
            if (getState() == 0)
                throw new IllegalMonitorStateException();
            setExclusiveOwnerThread(null);
            setState(0);
            return true;
        }

        Condition newCondition() {
            return new ConditionObject();
        }
    }

    private final Sync sync = new Sync();

    public void lock() {
        sync.acquire(1);
    }

    public boolean tryLock() {
        return sync.tryAcquire(1);
    }

    public void unlock() {
        sync.release(1);
    }

    public Condition newCondition() {
        return sync.newCondition();
    }

    public boolean isLocked() {
        return sync.isHeldExclusively();
    }

    public boolean hasQueuedThreads() {
        return sync.hasQueuedThreads();
    }

    public void lockInterruptibly() throws InterruptedException {
        sync.acquireInterruptibly(1);
    }

    public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
        return sync.tryAcquireNanos(1, unit.toNanos(timeout));
    }
}

實現自定義元件的時候,我們可以看到,AQS可重寫的方法是tryAcquire()——獨佔式獲取同步狀態、tryRelease()——獨佔式釋放同步狀態、tryAcquireShared()——共享式獲取同步狀態、tryReleaseShared ()——共享式釋放同步狀態、isHeldExclusively()——是否被當前執行緒所獨佔。這個示例中,獨佔鎖Mutex是一個自定義同步元件,它在同一時刻只允許一個執行緒佔有鎖。Mutex中定義了一個靜態內部類,該內部類繼承了同步器並實現了獨佔式獲取和釋放同步狀態。在tryAcquire()中,如果經過CAS設定成功(同步狀態設定為1),則表示獲取了同步狀態,而在tryRelease()中,只是將同步狀態重置為0。接著我們對比一下重入鎖(ReentrantLock)的原始碼實現

public class ReentrantLock implements Lock, java.io.Serializable {
    private static final long serialVersionUID = 7373984872572414699L;
    /** Synchronizer providing all implementation mechanics */
    private final Sync sync;

    /**
     * Base of synchronization control for this lock. Subclassed
     * into fair and nonfair versions below. Uses AQS state to
     * represent the number of holds on the lock.
     */
    abstract static class Sync extends AbstractQueuedSynchronizer {
        private static final long serialVersionUID = -5179523762034025860L;

        /**
         * Performs {@link Lock#lock}. The main reason for subclassing
         * is to allow fast path for nonfair version.
         */
        abstract void lock();

        /**
         * Performs non-fair tryLock.  tryAcquire is
         * implemented in subclasses, but both need nonfair
         * try for trylock method.
         */
        final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }

        protected final boolean tryRelease(int releases) {
            int c = getState() - releases;
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            setState(c);
            return free;
        }

        protected final boolean isHeldExclusively() {
            // While we must in general read state before owner,
            // we don't need to do so to check if current thread is owner
            return getExclusiveOwnerThread() == Thread.currentThread();
        }

        final ConditionObject newCondition() {
            return new ConditionObject();
        }

        // Methods relayed from outer class

        final Thread getOwner() {
            return getState() == 0 ? null : getExclusiveOwnerThread();
        }

        final int getHoldCount() {
            return isHeldExclusively() ? getState() : 0;
        }

        final boolean isLocked() {
            return getState() != 0;
        }

        /**
         * Reconstitutes this lock instance from a stream.
         * @param s the stream
         */
        private void readObject(java.io.ObjectInputStream s)
            throws java.io.IOException, ClassNotFoundException {
            s.defaultReadObject();
            setState(0); // reset to unlocked state
        }
    }

    /**
     * Sync object for non-fair locks
     */
    static final class NonfairSync extends Sync {
    //todo sth...
    }
    //todo sth...
}

重入鎖分公平鎖和不公平鎖,預設使用的是不公平鎖,在這我們看到實現重入鎖大體上跟我們剛才自定義的獨佔鎖差不多,但是有什麼區別呢?我們看看重入鎖nonfairTryAcquire()方法實現:首先獲取同步狀態(預設是0),如果是0的話,CAS設定同步狀態,非0的話則判斷當前執行緒是否已佔有鎖,如果是的話,則偏向更新同步狀態。從這裡我們不難推斷出重入鎖的概念,同一個執行緒可以多次獲得同一把鎖,在釋放的時候也必須釋放相同次數的鎖。通過對比相信大家對自定義一個鎖有了一個初步的概念,也許你存在疑問我們重寫的這幾個方法在AQS哪地方用呢?現在我們來繼續往下跟蹤,我們深入跟蹤下剛才自定義獨佔鎖lock()方法裡面acquire()的實現

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

這個方法在AQS類裡面,看到裡面的tryAcquire(arg)大家也就明白了,tryAcquire(arg)方法獲取同步狀態,後面acquireQueued(addWaiter(Node.EXCLUSIVE), arg)方法就是說的節點構造、加入同步佇列及在同步佇列中自旋等待的AQS沒暴露給我們的相關操作。大體的流程就是首先呼叫自定義同步器實現的tryAcquire()方法,該方法保證執行緒安全的獲取同步狀態,如果獲取同步狀態失敗,則構造同步節點(獨佔式Node.EXCLUSIVE,同一時刻只能有一個執行緒成功獲取同步狀態)並通過addWaiter()方法將該節點加入到同步佇列的尾部,最後呼叫acquireQueued()方法,使得該節點以“死迴圈”的方式獲取同步狀態。如果獲取不到則阻塞節點中的執行緒,而被阻塞執行緒的喚醒主要靠前驅節點的出隊或阻塞執行緒被中斷來實現。也許你還是不明白剛才所說的,那麼我們繼續跟蹤下addWaiter()方法的實現

private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode);
    // Try the fast path of enq; backup to full enq on failure
    Node pred = tail;
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    enq(node);
    return node;
}

private Node enq(final Node node) {
    for (;;) {
        Node t = tail;
        if (t == null) { // Must initialize
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

上面的程式碼通過使用compareAndSetTail()方法來確保節點能夠被執行緒安全新增。在enq()方法中,同步器通過“死迴圈”來確保節點的正確新增,在”死迴圈“中只有通過CAS將節點設定成為尾節點之後,當前執行緒才能夠從該方法返回,否則,當前執行緒不斷地嘗試重試設定。

在節點進入同步佇列之後,發生了什麼呢?現在我們繼續跟蹤下acquireQueued()方法

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

從上面的程式碼我們不難看出,節點進入同步佇列之後,就進入了一個自旋的過程,每個節點(或者說每個執行緒)都在自省的觀察,當條件滿足時(自己的前驅節點是頭節點就進行CAS設定同步狀態)就獲得同步狀態,然後就可以從自旋的過程中退出,否則依舊在這個自旋的過程中。

collections

從前面的思維導圖我們可以看到併發容器包括連結串列、佇列、HashMap等.它們都是執行緒安全的。

  • ConcurrentHashMap : 一個高效的執行緒安全的HashMap。
  • CopyOnWriteArrayList : 在讀多寫少的場景中,效能非常好,遠遠高於vector。
  • ConcurrentLinkedQueue : 高效併發佇列,使用連結串列實現,可以看成執行緒安全的LinkedList。
  • BlockingQueue : 一個介面,JDK內部通過連結串列,陣列等方式實現了這個介面,表示阻塞佇列,非常適合用作資料共享 。
  • ConcurrentSkipListMap : 跳錶的實現,這是一個Map,使用跳錶資料結構進行快速查詢 。

另外Collections工具類可以幫助我們將任意集合包裝成執行緒安全的集合。在這裡重點說下ConcurrentHashMap和BlockingQueue這兩個併發容器。

我們都知道HashMap執行緒不安全的,而我們可以通過Collections.synchronizedMap(new HashMap<>())來包裝一個執行緒安全的HashMap或者使用執行緒安全的HashTable,但是它們的效率都不是很好,這時候我們就有了ConcurrentHashMap。為什麼ConcurrentHashMap高效且執行緒安全呢?其實它使用了鎖分段技術來提高了併發的訪問率。假如容器裡有多把鎖,每一把鎖用於鎖容器的一部分資料,那麼當多執行緒訪問容器裡不同資料段的資料時,執行緒間就不會存在鎖競爭,從而可以有效地提高併發訪問效率,這就是鎖分段技術。首先將資料分成一段段的儲存,然後給每段資料配一把鎖,當一個執行緒佔用鎖訪問其中一個段資料的時候,其他段的資料也能被其他執行緒訪問。而既然資料被分成了多個段,執行緒如何定位要訪問的段的資料呢?這裡其實是通過雜湊演算法來定位的。

現在來談談阻塞佇列,阻塞佇列其實跟後面要談的執行緒池息息相關的,JDK7提供了7個阻塞佇列,分別是

  • ArrayBlockingQueue :一個由陣列結構組成的有界阻塞佇列。
  • LinkedBlockingQueue :一個由連結串列結構組成的有界阻塞佇列。
  • PriorityBlockingQueue :一個支援優先順序排序的無界阻塞佇列。
  • DelayQueue:一個使用優先順序佇列實現的無界阻塞佇列。
  • SynchronousQueue:一個不儲存元素的阻塞佇列。
  • LinkedTransferQueue:一個由連結串列結構組成的無界阻塞佇列。
  • LinkedBlockingDeque:一個由連結串列結構組成的雙向阻塞佇列。

如果佇列是空的,消費者會一直等待,當生產者新增元素時候,消費者是如何知道當前佇列有元素的呢?如果讓你來設計阻塞佇列你會如何設計,讓生產者和消費者能夠高效率的進行通訊呢?讓我們先來看看JDK是如何實現的。

使用通知模式實現。所謂通知模式,就是當生產者往滿的佇列裡新增元素時會阻塞住生產者,當消費者消費了一個佇列中的元素後,會通知生產者當前佇列可用。通過檢視JDK原始碼發現ArrayBlockingQueue使用了Condition來實現,程式碼如下:

private final Condition notFull;
private final Condition notEmpty;

    public ArrayBlockingQueue(int capacity, boolean fair) {
        //省略其他程式碼
        notEmpty = lock.newCondition();
        notFull =  lock.newCondition();
    }

    public void put(E e) throws InterruptedException {
        checkNotNull(e);
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            while (count == items.length)
                notFull.await();
            insert(e);
        } finally {
            lock.unlock();
        }
    }

    public E take() throws InterruptedException {
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            while (count == 0)
                notEmpty.await();
            return extract();
        } finally {
            lock.unlock();
        }
    }

    private void insert(E x) {
        items[putIndex] = x;
        putIndex = inc(putIndex);
        ++count;
        notEmpty.signal();
    }

當我們往佇列裡插入一個元素時,如果佇列不可用,阻塞生產者主要通過LockSupport.park(this)來實現

public final void await() throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    Node node = addConditionWaiter();
    int savedState = fullyRelease(node);
    int interruptMode = 0;
    while (!isOnSyncQueue(node)) {
        LockSupport.park(this);
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
            break;
    }
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
        interruptMode = REINTERRUPT;
    if (node.nextWaiter != null) // clean up if cancelled
        unlinkCancelledWaiters();
    if (interruptMode != 0)
        reportInterruptAfterWait(interruptMode);
}

繼續進入原始碼,發現呼叫setBlocker先儲存下將要阻塞的執行緒,然後呼叫unsafe.park阻塞當前執行緒。

public static void park(Object blocker) {
    Thread t = Thread.currentThread();
    setBlocker(t, blocker);
    unsafe.park(false, 0L);
    setBlocker(t, null);
}

unsafe.park是個native方法,程式碼如下:

public native void park(boolean isAbsolute, long time);

park這個方法會阻塞當前執行緒,只有以下四種情況中的一種發生時,該方法才會返回。

  • 與park對應的unpark執行或已經執行時。注意:已經執行是指unpark先執行,然後再執行的park。
  • 執行緒被中斷時。
  • 如果引數中的time不是零,等待了指定的毫秒數時。
  • 發生異常現象時。這些異常事先無法確定。

我們繼續看一下JVM是如何實現park方法的,park在不同的作業系統使用不同的方式實現,在linux下是使用的是系統方法pthread_cond_wait實現。實現程式碼在JVM原始碼路徑src/os/linux/vm/os_linux.cpp裡的 os::PlatformEvent::park方法,程式碼如下:

void os::PlatformEvent::park() {
            int v ;
            for (;;) {
                v = _Event ;
            if (Atomic::cmpxchg (v-1, &_Event, v) == v) break ;
            }
            guarantee (v >= 0, "invariant") ;
            if (v == 0) {
            // Do this the hard way by blocking ...
            int status = pthread_mutex_lock(_mutex);
            assert_status(status == 0, status, "mutex_lock");
            guarantee (_nParked == 0, "invariant") ;
            ++ _nParked ;
           while (_Event < 0) {          
           status = pthread_cond_wait(_cond, _mutex);        
           // for some reason, under 2.7 lwp_cond_wait() may return ETIME ...           
           // Treat this the same as if the wait was interrupted         
            if (status == ETIME) { status = EINTR; }        
            assert_status(status == 0 || status == EINTR, status, "cond_wait");        
           }    
           -- _nParked ;          
          // In theory we could move the ST of 0 into _Event past the unlock(),       
          // but then we'd need a MEMBAR after the ST.       
          _Event = 0 ;     
         status = pthread_mutex_unlock(_mutex);    
         assert_status(status == 0, status, "mutex_unlock");       
         }        
          guarantee (_Event >= 0, "invariant") ;
        }
    }

pthread_cond_wait是一個多執行緒的條件變數函式,cond是condition的縮寫,字面意思可以理解為執行緒在等待一個條件發生,這個條件是一個全域性變數。這個方法接收兩個引數,一個共享變數_cond,一個互斥量_mutex。而unpark方法在linux下是使用pthread_cond_signal實現的。park 在windows下則是使用WaitForSingleObject實現的。

當佇列滿時,生產者往阻塞佇列裡插入一個元素,生產者執行緒會進入WAITING (parking)狀態。

executor

Executor框架提供了各種型別的執行緒池,不同的執行緒池應用了前面介紹的不同的堵塞佇列

enter image description here

Executor框架最核心的類是ThreadPoolExecutor,它是執行緒池的實現類。 對於核心的幾個執行緒池,無論是newFixedThreadPool()、newSingleThreadExecutor()還是newCacheThreadPool()方法,雖然看起來建立的執行緒具有完全不同的功能特點,但其內部均使用了ThreadPoolExecutor實現

public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
            0L, TimeUnit.MILLISECONDS,
            new LinkedBlockingQueue());
}
public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                    0L, TimeUnit.MILLISECONDS,
                    new LinkedBlockingQueue()));
}
public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
            60L, TimeUnit.SECONDS,
            new SynchronousQueue());
}
  • newFixedThreadPool()方法的實現,它返回一個corePoolSize和maximumPoolSize一樣的,並使用了LinkedBlockingQueue任務佇列(無界佇列)的執行緒池。當任務提交非常頻繁時,該佇列可能迅速膨脹,從而系統資源耗盡。
  • newSingleThreadExecutor()返回單執行緒執行緒池,是newFixedThreadPool()方法的退化,只是簡單的將執行緒池數量設定為1。
  • newCachedThreadPool()方法返回corePoolSize為0而maximumPoolSize無窮大的執行緒池,這意味著沒有任務的時候執行緒池內沒有現場,而當任務提交時,該執行緒池使用空閒執行緒執行任務,若無空閒則將任務加入SynchronousQueue佇列,而SynchronousQueue佇列是直接提交佇列,它總是破事執行緒池增加新的執行緒來執行任務。當任務執行完後由於corePoolSize為0,因此空閒執行緒在指定時間內(60s)被回收。對於newCachedThreadPool(),如果有大量任務提交,而任務又不那麼快執行時,那麼系統變回開啟等量的執行緒處理,這樣做法可能會很快耗盡系統的資源,因為它會增加無窮大數量的執行緒。

由以上執行緒池的實現可以看到,它們都只是ThreadPoolExecutor類的封裝。我們看下ThreadPoolExecutor最重要的建構函式:

public ThreadPoolExecutor(
            //核心執行緒池,指定了執行緒池中的執行緒數量
            int corePoolSize,
            //基本執行緒池,指定了執行緒池中的最大執行緒數量
            int maximumPoolSize,
            //當前執行緒池數量超過corePoolSize時,多餘的空閒執行緒的存活時間,即多次時間內會被銷燬。
            long keepAliveTime,
            //keepAliveTime的單位
            TimeUnit unit,
            //任務佇列,被提交但尚未被執行的任務。
            BlockingQueue workQueue,
            //執行緒工廠,用於建立執行緒,一般用預設的即可
            ThreadFactory threadFactory,
            //拒絕策略,當任務太多來不及處理,如何拒絕任務。
            RejectedExecutionHandler handler)

ThreadPoolExecutor的任務排程邏輯如下

enter image description here

從上圖我們可以看出,當提交一個新任務到執行緒池時,執行緒池的處理流程如下:

  • 首先執行緒池判斷基本執行緒池是否已滿,如果沒滿,建立一個工作執行緒來執行任務。滿了,則進入下個流程。
  • 其次執行緒池判斷工作佇列是否已滿,如果沒滿,則將新提交的任務儲存在工作佇列裡。滿了,則進入下個流程。
  • 最後執行緒池判斷整個執行緒池是否已滿,如果沒滿,則建立一個新的工作執行緒來執行任務,滿了,則交給飽和策略來處理這個任務。

下面我們來看看ThreadPoolExecutor核心排程程式碼

public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        int c = ctl.get();
        /**
        * workerCountOf(c)獲取當前執行緒池執行緒總數
        * 當前執行緒數小於corePoolSize核心執行緒數時,會將任務通過addWorker(command, true)方法直接排程執行。
        * 否則進入下個if,將任務加入等待佇列
        **/
        if (workerCountOf(c) < corePoolSize) {
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
        /**
        * workQueue.offer(command) 將任務加入等待佇列。
        * 如果加入失敗(比如有界佇列達到上限或者使用了synchronousQueue)則會執行else。
        *
        **/
        if (isRunning(c) && workQueue.offer(command)) {
            int recheck = ctl.get();
            if (! isRunning(recheck) && remove(command))
                reject(command);
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
        /**
        * addWorker(command, false)直接交給執行緒池,
        * 如果當前執行緒已達到maximumPoolSize,則提交失敗執行reject()拒絕策略。
        **/
        else if (!addWorker(command, false))
            reject(command);
    }

從上面的原始碼我們可以知道execute的執行步驟:

  • 如果當前執行的執行緒少於corePoolSize,則建立新執行緒來執行任務(注意,執行這一步驟需要獲取全域性鎖)。
  • 如果執行的執行緒等於或多於corePoolSize,則將任務加入到BlockingQueue。
  • 如果無法將任務假如BlockingQueue(佇列已滿),則建立新的執行緒來處理任務(注意,執行這一步驟需要獲取全域性鎖)。
  • 如果建立新執行緒將使當前執行的執行緒超出maximumPoolSize,任務將被拒絕,並呼叫RejectedExecutionHandler.rejectedExecution()方法。

ThreadPoolExecutor採取上述步驟的總體設計思路,是為了在執行execute()方法時,儘可能的避免獲取全域性鎖(那將會是一個嚴重的 可伸縮瓶頸)。在ThreadPoolExecutor完成預熱之後(當前執行的執行緒數大於等於corePoolSize),幾乎所有的execute()方法呼叫都是執行步驟2,而步驟2不需要獲取全域性鎖。

參考閱讀

本文部分內容參考自《Java併發程式設計的藝術》、《深入理解Java虛擬機器(第2版)》、《實戰Java高併發程式設計》、《深入Java記憶體模型》、《Java併發程式設計實踐》,感興趣的可自行查閱。

相關文章