關於Java高併發程式設計你需要知道的“升段攻略”

zh發表於2021-02-28

關於Java高併發程式設計你需要知道的“升段攻略”

基礎

  1. Thread物件呼叫start()方法包含的步驟

    1. 通過jvm告訴作業系統建立Thread
    2. 作業系統開闢記憶體並使用Windows SDK中的createThread()函式建立Thread執行緒物件
    3. 作業系統對Thread物件進行排程,以確定執行時機
    4. Thread在作業系統中被成功執行
  2. 執行start的順序不代表執行run的順序

  3. 執行方法run和start有區別

    1. xxx.run():立即執行run()方法,不啟動新的執行緒
    2. xxx.start():執行run()方法時機不確定,啟動新的執行緒
  4. 執行緒停止:

    1. public static boolean interrupted():測試當前執行緒是否已經是中斷狀態,執行後具有清除狀態標誌值的功能

    2. public boolean this.isInterrupted():測試當前執行緒是否已經是中斷狀態,不清除狀態標誌

    3. 停止執行緒-異常

    4. 在sleep下停止執行緒,當sleep和interrupt同時出現時,會報錯

    5. stop終止執行緒

    6. return停止執行緒

    7. 暫停執行緒

      suspend 和 resume獨佔

  5. synchronized原理:

    1. 方法:使用ACC_SYNCHRONIZED標記
    2. 程式碼塊:使用monitorentermonitorexit指令
  6. 靜態同步synchronized方法與synchronized(class)程式碼塊,同步synchronized方法與synchronized(this)程式碼塊

  7. volatile關鍵字特性:

    1. 可見性:一個執行緒修改共享變數的值,其他執行緒立馬就知道
    2. 原子性:double和long資料型別具有原子性,針對volatile宣告變數的自增不具有原子性
    3. 禁止程式碼重排序
  8. 利用volatile關鍵字解決多執行緒出現的死迴圈(可見性),本質是因為執行緒的私有堆疊和公有堆疊不同步

  9. synchronized程式碼塊具有增加可見性的作用

  10. 自增/自減操作的步驟

    1. 從記憶體中取值
    2. 計算值
    3. 將值寫入記憶體
  11. 使用Atomic原子類進行i++操作實現原子性

  12. sychronized程式碼塊和volatile關鍵字都禁止程式碼重排序:位於程式碼塊/關鍵字兩側的不能互相重排序,只能各自在前面或者後面重排序

  13. wait/notify機制

    1. 使用前都必須獲得鎖,即必須位於synchronized修飾的方法內或者程式碼塊內,且必須是同一把鎖
    2. 使用wait後會釋放鎖,當呼叫notify後,執行緒再次獲得鎖並執行
    3. wait執行後會立即釋放鎖,而notify執行後不會立即讓出鎖,而是等到執行notify方法的執行緒將程式執行完以後才讓出鎖
    4. wait使執行緒暫停執行,notify使執行緒繼續執行
    5. 處於wait狀態下的執行緒會一直等待
    6. 在呼叫notify時若沒有處於wait狀態的執行緒,命令會被忽略
    7. 當呼叫了多個wait方法,同時需要多個notify方法時,所有等待的wait狀態執行緒獲取鎖的順序是執行wait方法的順序,即先呼叫先獲得鎖
    8. notifyAll方法可以一次通知所有的處理wait狀態的執行緒,但是這些執行緒獲得鎖的順序是先呼叫的最後獲得,後呼叫的先獲得,通常還有其他的順序,取決於jvm的具體實現
  14. wait立即釋放鎖,notify不立即釋放鎖,sleep不會釋放鎖

  15. wait與interrupt方法同時出現會出現異常

  16. wait(long)表示等待一段時間後,再次獲取鎖,不需要notify方法通知,若沒有獲取鎖則會一直等待,直到獲取鎖為止

  17. if+wait建議替換成while+wait

  18. 執行緒間通過管道通訊(字元流/位元組流):PipInputStream/PipOutputStream、PipWriter/PipReader

  19. join方法是所屬的執行緒物件x正常執行run()方法中的任務,而使當前執行緒進行無期限的阻塞,等待執行緒x銷燬後再繼續執行執行緒z後面的程式碼,具有串聯執行的效果

  20. join方法具有使執行緒排隊執行的效果,有類似同步的執行效果,但是join方法與synchronized的區別是join方法內部使用wait方法進行等待(會釋放鎖),而synchronized關鍵字使用鎖作為同步

  21. join與interrupt同時出現會出現異常

  22. join(long)執行後會呼叫內部的wait(long)會釋放鎖,而Thread.sleep(long)則不會釋放鎖

  23. ThreadLocal

    1. 將資料放入到當前執行緒物件中的Map中,這個Map是Thread類的例項變數。類ThreadLocal自己不管理、不儲存任何資料,它只是資料和Map之間的橋樑,用於將資料放入Map中,執行流程如下:資料-->ThreadLocal-->currentThread() -->Map

    2. 執行後每個執行緒中的Map存有自己的資料,Map中的key儲存的是ThreadLocal物件,value就是儲存的值。每個Thread中的Map值只對當前執行緒可見,其他執行緒不可以訪問。當前執行緒銷燬,Map隨之銷燬,Map中的資料如果沒有被引用、沒有被使用,則隨時GC回收

    3. 執行緒、Map、資料之間的關係可以做如下類比:

      人(Thread)隨身帶有兜子(Map),兜子(Map)裡面有東西(Value),這樣,Thread隨身也有資料,隨時可以訪問自己的資料

    4. 可以通過繼承類,並重寫initialValue()來解決get()返回null的問題

    5. Thread本身不能實現子執行緒使用父執行緒的值,但是使用InheritableLocalThread則可以訪問,不過,不能實現同步更新值,也就是說子執行緒更新了值,父執行緒中還是舊值,父執行緒更新了值,子執行緒中還是舊值

    6. 通過重寫InheritableLocalThread的childValue可以實現子執行緒對父執行緒繼承的值進行加工修改

  24. Java多執行緒可以使用synchronized關鍵字來實現執行緒同步,不過jdk1.5新增加的ReentrantLock類可能達到同樣的效果,並且在擴充套件功能上更加強大,如具有嗅探鎖定、多路分支通知等功能

  25. 關鍵字synchronized與wait()、notify()/notifyAll()方法相結合可以實現wait/notify模型,ReentrantLock類也可以實現同樣的功能,但需要藉助於Condition物件。Condition類是jdk5的技術,具有更好的靈活性,例如,可以實現多路通知功能,也就是在一個Lock物件中可以建立多個Condition例項,執行緒物件註冊在指定的Condition中,從而可以有選擇性的進行執行緒通知,在排程執行緒上更加靈活;在使用notify()/notifyAll()方法進行通知時,被通知的執行緒有jvm進行選擇,而方法notifyAll()會通知所有waiting執行緒,沒有選擇權,會出現相當大的效率問題,但使用ReentrantLock結合Condition類可以實現“選擇性通知”,這個功能是Condition預設提供的

  26. Condition類

    1. Condition物件的建立是使用ReentrantLoack的newCondition方法建立的
    2. Condition物件的作用是控制並處理執行緒的狀態,它可以使執行緒呈wait狀態,也可以讓執行緒繼續執行
    3. 通過Condition中的await/signal可以實現wait/notify一樣的效果
      1. Object中的wait方法相當於Condition類中的await方法
      2. Object中的wait(long timeout)方法相當於Condition類中的await(long time, TimeUnit unit)方法
      3. Object中的notify方法相當於Condition類中的signal1方法
      4. Object類中的notifyAll方法相當於Condition類中的signalAll方法
    4. await方法暫停執行緒的原理
      1. 併發包內部執行了unsafe類中的public native void park(boolean isAbsolute, long time)方法,讓當前執行緒呈暫停狀態,方法引數isAbsolute代表是否為絕對時間
    5. 公平鎖:採用先到先得的鎖,每次獲取之前都會檢查佇列裡面有沒有排隊等待的執行緒,沒有才嘗試獲取鎖,如果有就將當前執行緒追加到佇列中
    6. 非公平鎖:採用“有機會插隊”的策略,一個執行緒獲取之前要先嚐試獲取鎖而不是在佇列中等待,如果獲取成功,則說明執行緒雖然是啟動的,但先獲得了鎖,如果沒有獲取成功,就將自身新增到佇列中進行等待
    7. 執行緒執行lock方法後內部執行了unsafe.park(false, 0L)程式碼;執行緒執行unlock方法後內部執行unsafe.unpark(bThread)方法
    8. public int getHoldCount()查詢“當前執行緒”保持鎖定的個數,即呼叫lock方法的次數
    9. public final int getQueueLength()返回正等待獲取此鎖的執行緒估計數,例如,這裡有5個執行緒,其中1個執行緒長時間佔有鎖,那麼呼叫getQueueLength()方法後,其返回值是4,說明有4個執行緒同時在等待鎖的釋放
    10. public int getWaitQueueLength()返回等待與此鎖有關的給定條件Condition的執行緒統計數。例如,有5個執行緒,每個執行緒都執行了同一個Condition物件的await()方法,則呼叫getWaitQueueLength()方法時,其返回的值是5
    11. public final boolean hasQueueThread(Thread thread)查詢指定的執行緒是否正在等待獲取此鎖,也就是判斷引數中的執行緒是否在等待佇列中
    12. public final boolean hasQueueThreads()查詢是否有執行緒正在等待獲取此鎖,也就是等待佇列中是否有等待的執行緒
    13. public boolean hasWaiters(Condition condition)查詢是否有執行緒正在等待與此鎖有關的condition條件,也就是是否有執行緒執行了condition物件中的await方法而呈等待狀態。而public int getWaitQueueLength(Condition condition)方法是返回有多少個執行緒執行了condition物件中的await方法而呈等待狀態
    14. public final boolean isFair()判斷是不是公平鎖
    15. public boolean isHeldByCurrentThread()是查詢當前執行緒是否保持此鎖
    16. public boolean isLocked()查詢此鎖是否由任意執行緒保持,並沒有釋放
    17. public void lockInterruptibly()當某個執行緒嘗試獲取鎖並且阻塞在lockInterruptibly()方法時,該執行緒可以被中斷
    18. public boolean tryLock()嗅探拿鎖,如果當前執行緒發現鎖被其他執行緒持有,則返回false,程式繼續執行後面的程式碼,而不是呈阻塞等待鎖的狀態
    19. public boolean tryLock(long timeout, TimeUnit unit)嗅探拿鎖,如果當前執行緒發現鎖被其他執行緒持有了,則返回false,程式繼續執行後面的程式碼,而不是呈阻塞等待狀態。如果當前執行緒在指定的timeout內持有了鎖,則返回值是true,超過時間則返回false。引數timeout代表當前執行緒搶鎖的時間
    20. public boolean await(long time, TimeUnit unit)public final native void wait(long timeout)方法一樣,都具有自動喚醒執行緒的功能
    21. public long awaitNanos(long nanoTimeout)public final native void wait(long timeout)方法一樣,都具有自動喚醒執行緒的功能,時間單位是納秒(ns)。
    22. public boolean awaitUntil(Date deadline)在指定的Date結束等待
    23. public void awaitUninterruptibly()在實現執行緒在等待的過程中,不允許被中斷
  27. ReentrantReadWriteLock類

    1. ReentrantLock類具有完全互斥排他的效果,同一時間只有一個執行緒在執行ReentrantLock.lock()方法後面的任務,這樣做雖然保證了同時寫例項變數的執行緒安全性,但效率非常低下,所以jdk提供了一種讀寫鎖——ReentrantReadWriteLock類,使用它可以在進行讀操作時不需要同步執行,提升執行速度,加快執行效率
    2. 讀寫鎖有兩個鎖:一個是讀操作相關的鎖,也稱共享鎖;另一個是寫操作相關的鎖,也稱排他鎖
    3. 讀寫互斥,寫讀互斥,寫寫互斥,讀讀非同步
  28. Timer定時器

    1. 定時/計劃任務功能在Java中主要使用Timer物件實現,它在內部使用多執行緒的方式進行處理,所以它和執行緒技術有很大的關聯
    2. 在指定的日期執行任務:schedule(TimerTask task, Date firstTime, long period)
    3. 按指定週期執行任務:schedule(TimerTask task, long delay, long period)
    4. 注意:在建立Timer物件時會建立一個守護執行緒,需要手動關閉,有兩種方法
      1. new Timer().concel():將任務佇列中的任務全部清空
      2. new TimerTask().concel():僅將自身從任務佇列中清空
    5. scheduleAtFixedRate()具有追趕性,也就是若任務的啟動時間早於當前時間,那麼它會將任務在這個時間差內按照任務執行的規律執行一次,相當於“彌補”流逝的時間
    6. 但timer佇列中有多個任務時,這些任務執行順序的演算法是每次將最後一個任務放入佇列頭,再執行佇列頭TimerTask任務的run方法
  29. 單例模式與多執行緒

    1. 餓漢式
    2. 懶漢式
      1. 使用DCL(雙重檢查鎖)來實現(volatile+synchronized)

進階

併發程式設計的進階問題

  1. 併發執行程式一定比序列執行程式快嗎?

    答:不是的。因為執行緒有建立和上下文切換的開銷,所以說,在某種情況下線併發執行程式會比序列執行程式慢

  2. 上下文切換:cpu通過時間片分配演算法來迴圈執行任務,當前任務執行一個時間片後會切換到下一個任務。但是,在切換前會儲存是一個任務的狀態,以便於下次切換回這個任務時,可以再載入這個任務的狀態,故任務從儲存到再載入的過程就是一次上下文切換。這就像我們同時讀兩本書,當我們在第一本英文的技術書時,發現某個單詞不認識,於是便開啟英文字典,但是放下英文技術書之前,大腦必須記住這本書讀到了多少頁的多少行,等查完單詞後,能夠繼續讀這本書。這樣的切換是會影響讀書效率的,同樣上下文切換也會影響多執行緒的執行速度

  3. 減少上下文切換:

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

Java併發機制的底層實現原理

關於volatile

  1. Java程式碼在編譯後會變成Java位元組碼,位元組碼被類載入器載入到jvm裡,jvm執行位元組碼,最終需要轉化為彙編指令在CPU上執行,Java中所有的併發機制依賴於jvm的實現和CPU的命令
  2. volatile是輕量級的synchronized,它在多處理器開發中保證了共享變數的“可見性”。可見性的意思是當一個執行緒修改一個共享變數時另外一個執行緒能讀到這個修改的值。如果volatile變數修飾符使用恰當的話,它比synchronized的使用和執行成本更低,因為它不會引起執行緒上下文的切換和排程
  3. volatile的定義:Java程式語言允許執行緒訪問共享變數,為了確保共享變數能被準確和一致地更新,執行緒應該確保排他鎖單獨獲得這個變數
  4. 相關CPU的術語定義
    1. 記憶體屏障(memory barries):是一組處理器指令,用於實現對記憶體操作的順序限制
    2. 緩衝行(cache line):快取中可以分配的最小儲存單元。處理器填寫快取線時會載入整個快取線,需要使用多個主記憶體讀週期
    3. 原子操作(atomic operations):不可中斷的一個或一系列操作
    4. 快取行填充(cache line fill):當處理器識別到從記憶體中讀取運算元是可快取的,處理器讀取整個快取行到適當的快取
    5. 快取命中(cache hit):如果進行快取記憶體填充操作的記憶體位置仍然是下次處理器訪問的地址時,處理器從快取中讀取資料,而不是從記憶體讀取
    6. 寫命中(write hit):當處理器將運算元寫回到一個記憶體快取的區域時,它首先會檢查這個快取的記憶體地址是否在快取行中,如果存在一個有效的快取行,則處理器將這個運算元寫回到快取,而不是寫回到記憶體
    7. 寫缺失(write misses the cache):一個有效的快取行被寫入到不存在的記憶體區域
  5. volatile底層通過使用lock字首的指令來保證可見性,lock字首的指令在多核處理器下會引發兩件事情:
    1. 將當前處理器快取行的資料寫回到系統記憶體
    2. 這個寫回記憶體操作會使其他CPU裡快取了該記憶體地址的資料無效

原子操作

原子操作(atomic operation):不可中斷的一個或一系列操作

  1. 相關的CPU術語
    1. 快取行(cache line):快取的最小操作單位
    2. 比較並交換(compare and swap):CAS操作需要輸入兩個數值,一箇舊值和新值,在操作期間先比較舊值有沒有發生變化,若沒有發生變化才交換成新值,發生了變化則不交換
    3. CPU流水線(CPU pipeline):CPU流水線的工作方式就像工業生產上的裝配流水線,在CPU中由56個不同功能的電路單元組成一條指令處理流水線,然後將x86指令分成56步後再由這些電路單元分別執行,這樣就能實現在一個CPU時鐘週期完成一條指令,因此提高CPU的運算速度
    4. 記憶體順序衝突(memory order violation):記憶體順序衝突一般是由假共享引起的,假共享是指多個CPU同時修改同一個快取行的不同部分而引起其中一個CPU的操作無效,當出現這個記憶體順序衝突時,CPU必須清空流水線
  2. 處理器提供匯流排鎖定和快取鎖定兩個機制來保證複雜記憶體操作的原子性
    1. 匯流排鎖就是使用處理器提供的一個LOCK#訊號,當一個處理器在匯流排上輸出此訊號時,其他處理器的請求被阻塞住,該處理器獨佔記憶體。例如:當多個執行緒執行i++操作時,多個處理器同時從各自的快取中讀取變數i,分別進行加1操作,然後分別寫入系統記憶體中。那麼,想要保證讀改寫共享變數的操作是原子的,就必須保證CPU1讀改寫共享變數的時候,CPU2不能操作快取了該共享變數記憶體地址的快取
    2. 頻繁使用的記憶體會快取在處理器的L1、L2和L3快取記憶體裡,那麼原子操作就可以直接在處理器內部快取中進行,並不需要宣告匯流排鎖。快取鎖定是指記憶體區域如果被快取在處理器的快取行中,並且在LOCK期間被鎖定,那麼但它執行鎖操作回寫到記憶體時,處理器不在匯流排上聲言LOCK#訊號,而是修改內部的記憶體地址,並允許它的快取一致機制來保證操作的原子性,因為快取一致性機制會阻止同時修改由兩個以上處理器快取的記憶體區域資料,當其他處理器回寫已被鎖定的快取行的資料時,會使快取行無效。當CPU1修改快取行中的i時使用了快取鎖定,那麼CPU2不能同時快取i的快取行
    3. 兩種情況不會使用快取鎖定
      1. 當操作的資料不能被快取在處理器內部,或操作的資料跨多個快取行時,則處理器會呼叫匯流排鎖定
      2. 有些處理器不支援快取鎖定
  3. Java實現原子操作的方式:鎖和迴圈CAS
    1. jvm中的CAS操作正是利用了處理器提供的CMPXCHG指令實現的。自旋CAS實現的基本思路就是迴圈進行CAS操作直接成功為止
    2. 鎖機制保證了只有獲得鎖的執行緒才能夠操作鎖定的記憶體區域。jvm內部實現了很多種鎖,有偏向鎖、輕量級鎖和互斥鎖。除了偏向鎖,jvm實現鎖的方式都用了迴圈CAS,即當一個執行緒想進入同步塊的時候使用迴圈CAS的方式來獲取鎖,當它退出同步塊的時候使用迴圈CAS釋放鎖

Java記憶體模型(Java Memory Module,JMM)

Java記憶體模型基礎
  1. 在併發程式設計中,需要處理兩個問題:執行緒之間如何通訊及執行緒之間如何同步。通訊是指執行緒之間以何種機制來交換資訊。在指令式程式設計中,執行緒之間的通訊機制有兩種:共享記憶體和資訊傳遞
  2. 在共享記憶體的併發模型裡,執行緒之間共享程式的公共狀態,通過寫—讀記憶體中的公共狀態進行隱式通訊。在訊息傳遞的併發模型裡,執行緒之間沒有公共狀態,執行緒之間必須通過傳送訊息來顯示進行通訊
  3. 同步是指程式中用於控制不同執行緒間操作發生相對順序的機制。在訊息傳遞的併發模型裡,由於訊息的傳送必須在訊息的接受之前,因此同步是隱式進行的
Java記憶體結構的抽象結構
  1. 在Java中,所有例項域、靜態域和陣列元素都儲存在堆記憶體中,堆記憶體線上程之間共享。區域性變數(Local Variables),方法定義引數(Java語言規範稱之為Formal Method Parameters)和異常處理器引數(ExceptionHandler Parameters)不會線上程之間共享,它們不會有記憶體可見性問題,也不受記憶體模型的影響

  2. Java執行緒之間的通訊由Java記憶體模型控制,JMM決定一個執行緒對共享變數的寫入何時對另一個執行緒可見。從抽象的角度來看,JMM定義了執行緒和主記憶體之間的抽象關係:執行緒之間的共享變數儲存在主記憶體(Main Memory)中,每個執行緒都有一個私有的本地記憶體(Local Memory),本地記憶體中儲存了該執行緒以讀/寫共享變數的副本。本地記憶體是JMM的一個抽象概念,並不真實存在。它涵蓋了快取、寫緩衝區、暫存器以及其他的硬體和編譯器優化

  1. 如果執行緒A與執行緒B之間要通訊的話,必須要經歷下面2個步驟。

    1. 執行緒A把本地記憶體A中更新過的共享變數重新整理到主記憶體中去。
    2. 執行緒B到主記憶體中去讀取執行緒A之前已更新過的共享變數

本地記憶體A和本地記憶體B由主記憶體中共享變數x的副本。假設初始時,這3個記憶體中的x值都為0。執行緒A在執行時,把更新後的x值(假設值為1)臨時存放在自己的本地記憶體A中。當執行緒A和執行緒B需要通訊時,執行緒A首先會把自己本地記憶體中修改後的x值重新整理到主記憶體中,此時主記憶體中的x值變為了1。隨後,執行緒B到主記憶體中去讀取執行緒A更新後的x值,此時執行緒B的本地記憶體的x值也變為了1

從整體來看,這兩個步驟實質上是執行緒A在向執行緒B傳送訊息,而且這個通訊過程必須要經過主記憶體。JMM通過控制主記憶體與每個執行緒的本地記憶體之間的互動,來為Java程式設計師提供記憶體可見性保證

從原始碼到指令序列的重排序

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

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

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

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

併發程式設計模型分類
  1. 現代的處理器使用寫緩衝區臨時儲存向記憶體寫入的資料。寫緩衝區可以保證指令流水線持續執行,它可以避免由於處理器停頓下來等待向記憶體寫入資料而產生的延遲。同時,通過以批處理的方式重新整理寫緩衝區,以及合併寫緩衝區中對同一記憶體地址的多次寫,減少對記憶體匯流排的佔用。雖然寫緩衝區有這麼多好處,但每個處理器上的寫緩衝區,僅僅對它所在的處理器可見。這個特性會對記憶體操作的執行順序產生重要的影響:處理器對記憶體的讀/寫操作的執行順序,不一定與記憶體實際發生的讀/寫操作順序一致!

  2. 為了保證記憶體可見性,Java編譯器在生成指令序列的適當位置會插入記憶體屏障指令來禁止特定型別的處理器重排序。JMM把記憶體屏障指令分為4類 :

  1. StoreLoad Barriers是一個“全能型”的屏障,它同時具有其他3個屏障的效果。現代的多處理器大多支援該屏障(其他型別的屏障不一定被所有處理器支援)。執行該屏障開銷會很昂貴,因為當前處理器通常要把寫緩衝區中的資料全部重新整理到記憶體中(Buffer Fully Flush)。
happens-before
  1. 在JMM中,如果一個操作執行的結果需要對另一個操作可見,那麼這兩個操作之間必須要存在happens-before關係。這裡提到的兩個操作既可以是在一個執行緒之內,也可以是在不同執行緒之間
  2. 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-before C。
重排序
  1. 重排序是指編譯器和處理器為了優化程式效能而對指令序列進行重新排序的一種手段

  2. 如果兩個操作訪問同一個變數,且這兩個操作中有一個為寫操作,此時這兩個操作之間就存在資料依賴性。存在資料依賴性的操作不能重排序

  3. as-if-serial語義

    1. as-if-serial語義的意思是:不管怎麼重排序(編譯器和處理器為了提高並行度),(單執行緒)程式的執行結果不能被改變。編譯器、runtime和處理器都必須遵守as-if-serial語義。
    2. 為了遵守as-if-serial語義,編譯器和處理器不會對存在資料依賴關係的操作做重排序,因為這種重排序會改變執行結果。但是,如果操作之間不存在資料依賴關係,這些操作就可能被編譯器和處理器重排序
    3. as-if-serial語義把單執行緒程式保護了起來,遵守as-if-serial語義的編譯器、runtime和處理器共同為編寫單執行緒程式的程式設計師建立了一個幻覺:單執行緒程式是按程式的順序來執行的。asif-serial語義使單執行緒程式設計師無需擔心重排序會干擾他們,也無需擔心記憶體可見性問題
    4. 在計算機中,軟體技術和硬體技術有一個共同的目標:在不改變程式執行結果的前提下,儘可能提高並行度
    5. 在單執行緒程式中,對存在控制依賴的操作重排序,不會改變執行結果(這也是as-if-serial語義允許對存在控制依賴的操作做重排序的原因);但在多執行緒程式中,對存在控制依賴的操作重排序,可能會改變程式的執行結果
  4. 順序一致性

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

    2. 如果程式是正確同步的,程式的執行將具有順序一致性(Sequentially Consistent)——即程式的執行結果與該程式在順序一致性記憶體模型中的執行結果相同

    3. 順序一致性記憶體模型有兩大特性 :

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

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

      3. 在概念上,順序一致性模型有一個單一的全域性記憶體,這個記憶體通過一個左右擺動的開關可以連線到任意一個執行緒,同時每一個執行緒必須按照程式的順序來執行記憶體讀/寫操作。從上
        面的示意圖可以看出,在任意時間點最多隻能有一個執行緒可以連線到記憶體。當多個執行緒併發執行時,圖中的開關裝置能把所有執行緒的所有記憶體讀/寫操作序列化(即在順序一致性模型中,所有操作之間具有全序關係)

      4. 對於未同步或未正確同步的多執行緒程式,JMM只提供最小安全性:執行緒執行時讀取到的值,要麼是之前某個執行緒寫入的值,要麼是預設值(0,Null,False),JMM保證執行緒讀操作讀取到的值不會無中生有(Out Of Thin Air)的冒出來。為了實現最小安全性,JVM在堆上分配物件時,首先會對記憶體空間進行清零,然後才會在上面分配物件(JVM內部會同步這兩個操作)。因此,在已清零的記憶體空間(Pre-zeroed Memory)分配物件時,域的預設初始化已經完成了

      5. 在計算機中,資料通過匯流排在處理器和記憶體之間傳遞。每次處理器和記憶體之間的資料傳遞都是通過一系列步驟來完成的,這一系列步驟稱之為匯流排事務(Bus Transaction)。匯流排事務包括讀事務(Read Transaction)和寫事務(Write
        Transaction)。讀事務從記憶體傳送資料到處理器,寫事務從處理器傳送資料到記憶體,每個事務會讀/寫記憶體中一個或多個物理上連續的字。這裡的關鍵是,匯流排會同步試圖併發使用匯流排的事務。在一個處理器執行匯流排事務期間,匯流排會禁止其他的處理器和I/O裝置執行記憶體的讀/寫

volatile的記憶體語義
  1. 鎖的happens-before規則保證釋放鎖和獲取鎖的兩個執行緒之間的記憶體可見性,這意味著對一個volatile變數的讀,總是能看到(任意執行緒)對這個volatile變數最後的寫入
  2. volatile變數自身具有下列特性:
    1. 可見性:對一個volatile變數的讀,總是能看到(任意執行緒)對這個volatile變數最後的寫入
    2. 原子性:對任意單個volatile變數的讀/寫具有原子性,但類似於volatile++這種複合操作不具有原子性
  3. 從記憶體語義的角度來說,volatile的寫-讀與鎖的釋放-獲取有相同的記憶體效果:volatile寫和鎖的釋放有相同的記憶體語義;volatile讀與鎖的獲取有相同的記憶體語義
  4. volatile寫的記憶體語義:當寫一個volatile變數時,JMM會把該執行緒對應的本地記憶體中的共享變數值重新整理到主記憶體
  5. volatile讀的記憶體語義:當讀一個volatile變數時,JMM會把該執行緒對應的本地記憶體置為無效。執行緒接下來將從主記憶體中讀取共享變數
  6. 如果我們把volatile寫和volatile讀兩個步驟綜合起來看的話,在讀執行緒B讀一個volatile變數後,寫執行緒A在寫這個volatile變數之前所有可見的共享變數的值都將立即變得對讀執行緒B可見
  7. 對volatile寫和volatile讀的記憶體語義做個總結:
    1. 執行緒A寫一個volatile變數,實質上是執行緒A向接下來將要讀這個volatile變數的某個執行緒發出了(其對共享變數所做修改的)訊息。
    2. 執行緒B讀一個volatile變數,實質上是執行緒B接收了之前某個執行緒發出的(在寫這個volatile變數之前對共享變數所做修改的)訊息。
    3. 執行緒A寫一個volatile變數,隨後執行緒B讀這個volatile變數,這個過程實質上是執行緒A通過主記憶體向執行緒B傳送訊息
  8. 為了實現volatile記憶體語義,JMM會分別限制這兩種型別的重排序型別
    1. 由表得出:

      1. 當第二個操作是volatile寫時,不管第一個操作是什麼,都不能重排序。這個規則確保volatile寫之前的操作不會被編譯器重排序到volatile寫之後。
      2. 當第一個操作是volatile讀時,不管第二個操作是什麼,都不能重排序。這個規則確保volatile讀之後的操作不會被編譯器重排序到volatile讀之前。
      3. 當第一個操作是volatile寫,第二個操作是volatile讀時,不能重排序
  9. 為了實現volatile的記憶體語義,編譯器在生成位元組碼時,會在指令序列中插入記憶體屏障來禁止特定型別的處理器重排序。對於編譯器來說,發現一個最優佈置來最小化插入屏障的總數幾乎不可能。為此,JMM採取保守策略。下面是基於保守策略的JMM記憶體屏障插入策略:
    1. 在每個volatile寫操作的前面插入一個StoreStore屏障
    2. 在每個volatile寫操作的後面插入一個StoreLoad屏障
    3. 在每個volatile讀操作的後面插入一個LoadLoad屏障
    4. 在每個volatile讀操作的後面插入一個LoadStore屏障
鎖的記憶體語義
  1. 鎖是Java併發程式設計中最重要的同步機制。鎖除了讓臨界區互斥執行外,還可以讓釋放鎖的執行緒向獲取同一個鎖的執行緒傳送訊息

  2. 鎖的釋放和獲取的記憶體語義

    1. 當執行緒釋放鎖時,JMM會把該執行緒對應的本地記憶體中的共享變數重新整理到主記憶體中
    2. 當執行緒獲取鎖時,JMM會把該執行緒對應的本地記憶體置為無效
  3. 對鎖釋放和鎖獲取的記憶體語義做個總結:

    1. 執行緒A釋放一個鎖,實質上是執行緒A向接下來將要獲取這個鎖的某個執行緒發出了(執行緒A對共享變數所做修改的)訊息
    2. 執行緒B獲取一個鎖,實質上是執行緒B接收了之前某個執行緒發出的(在釋放這個鎖之前對共享變數所做修改的)訊息
    3. 執行緒A釋放鎖,隨後執行緒B獲取這個鎖,這個過程實質上是執行緒A通過主記憶體向執行緒B傳送訊息
  4. 鎖記憶體語義的實現

    1. 利用volatile變數的寫-讀所具有的記憶體語義
    2. 利用CAS所附帶的volatile讀和volatile寫的記憶體語義
final域的記憶體語義
  1. 對於final域,編譯器和處理器要遵守兩個重排序規則:

    1. 在建構函式內對一個final域的寫入,與隨後把這個被構造物件的引用賦值給一個引用變數,這兩個操作之間不能重排序
    2. 初次讀一個包含final域的物件的引用,與隨後初次讀這個final域,這兩個操作之間不能重排序
  2. 寫final域的重排序規則

    1. 寫final域的重排序規則禁止把final域的寫重排序到建構函式之外
      1. JMM禁止編譯器把final域的寫重排序到建構函式之外
      2. 編譯器會在final域的寫之後,建構函式return之前,插入一個StoreStore屏障。這個屏障禁止處理器把final域的寫重排序到建構函式之外
      3. 簡單的來說就是建構函式可以分批次初始化,final域要最先初始化
    2. 寫final域的重排序規則可以確保:在物件引用為任意執行緒可見之前,物件的final域已經被正確初始化過了,而普通域不具有這個保障
  3. 讀final域的重排序規則

    1. 讀final域的重排序規則是,在一個執行緒中,初次讀物件引用與初次讀該物件包含的final域,JMM禁止處理器重排序這兩個操作(注意,這個規則僅僅針對處理器)。編譯器會在讀final域操作的前面插入一個LoadLoad屏障
    2. 初次讀物件引用與初次讀該物件包含的final域,這兩個操作之間存在間接依賴關係。由於編譯器遵守間接依賴關係,因此編譯器不會重排序這兩個操作。大多數處理器也會遵守間接依賴,也不會重排序這兩個操作。但有少數處理器允許對存在間接依賴關係的操作做重排序(比如alpha處理器),這個規則就是專門用來針對這種處理器的
    3. 讀final域的重排序規則可以確保:在讀一個物件的final域之前,一定會先讀包含這個final域的物件的引用
  4. 當final域為引用型別,寫final域的重排序規則對編譯器和處理器增加了如下約束:在建構函式內對一個final引用的物件的成員域的寫入,與隨後在建構函式外把這個被構造物件的引用賦值給一個引用變數,這兩個操作之間不能重排序

  5. happens-before

    1. 首先,讓我們來看JMM的設計意圖。從JMM設計者的角度,在設計JMM時,需要考慮兩個關鍵因素:
      1. 程式設計師對記憶體模型的使用。程式設計師希望記憶體模型易於理解、易於程式設計。程式設計師希望基於一個強記憶體模型來編寫程式碼
      2. 編譯器和處理器對記憶體模型的實現。編譯器和處理器希望記憶體模型對它們的束縛越少越好,這樣它們就可以做盡可能多的優化來提高效能。編譯器和處理器希望實現一個弱記憶體模型
    2. 由於這兩個因素互相矛盾,所以JSR-133專家組在設計JMM時的核心目標就是找到一個好的平衡點:一方面,要為程式設計師提供足夠強的記憶體可見性保證;另一方面,對編譯器和處理。因此,JMM把happens-before要求禁止的重排序分為了下面兩類。 器的限制要儘可能地放鬆:
      1. 會改變程式執行結果的重排序
      2. 不會改變程式執行結果的重排序
    3. JMM對這兩種不同性質的重排序,採取了不同的策略:
      1. 對於會改變程式執行結果的重排序,JMM要求編譯器和處理器必須禁止這種重排序
      2. 對於不會改變程式執行結果的重排序,JMM對編譯器和處理器不做要求(JMM允許這種重排序)
    4. 只要不改變程式的執行結果(指的是單執行緒程式和正確同步的多執行緒程式),編譯器和處理器怎麼優化都行。例如,如果編譯器經過細緻的分析後,認定一個鎖只會被單個執行緒訪問,那麼這個鎖可以被消除。再如,如果編譯器經過細緻的分析後,認定一個volatile變數只會被單個執行緒訪問,那麼編譯器可以把這個volatile變數當作一個普通變數來對待。這些優化既不會改變程式的執行結果,又能提高程式的執行效率
    5. happens-before關係本質上和as-if-serial語義是一回事,都是為了在不改變程式執行結果的前提下,儘可能地提高程式執行的並行度 :
      1. as-if-serial語義保證單執行緒內程式的執行結果不被改變,happens-before關係保證正確同步的多執行緒程式的執行結果不被改變
      2. as-if-serial語義給編寫單執行緒程式的程式設計師創造了一個幻境:單執行緒程式是按程式的順序來執行的。happens-before關係給編寫正確同步的多執行緒程式的程式設計師創造了一個幻境:正確同步的多執行緒程式是按happens-before指定的順序來執行的
  6. 雙重檢查鎖定與延遲初始化

    1. 在早期的JVM中,synchronized(甚至是無競爭的synchronized)存在巨大的效能開銷。因此,人們想出了一個“聰明”的技巧:雙重檢查鎖定(Double-Checked Locking)。人們想通過雙重檢查鎖定來降低同步的開銷。下面是使用雙重檢查鎖定來實現延遲初始化的示例程式碼

      線上程執行到第4行,程式碼讀取到instance不為null時,instance引用的物件有可能還沒有完成初始化

    2. 前面的雙重檢查鎖定示例程式碼的第7行(instance=new Singleton();)建立了一個物件。這一行程式碼可以分解為如下的3行虛擬碼。

      上面3行虛擬碼中的2和3之間,可能會被重排序

      DoubleCheckedLocking示例程式碼的第7行(instance=new Singleton();)如果發生重排序,另一個併發執行的執行緒B就有可能在第4行判斷instance不為null。執行緒B接下來將訪問instance所引用的物件,但此時這個物件可能還沒有被A執行緒初始化

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

      1. 不允許2和3重排序 (volatile)
      2. 允許2和3重排序,但不允許其他執行緒“看到”這個重排序(靜態內部類)
    4. 基於volatile解決辦法

      程式碼中的2和3之間的重排序,在多執行緒環境中將會被禁止

    5. 基於類初始化的解決方案

      1. JVM在類的初始化階段(即在Class被載入後,且被執行緒使用之前),會執行類的初始化。在執行類的初始化期間,JVM會去獲取一個鎖。這個鎖可以同步多個執行緒對同一個類的初始化

      2. 這個方案的實質是:允許上面中的3行虛擬碼中的2和3重排序,但不允許非構造執行緒(這裡指執行緒B)“看到”這個重排序。

      3. 初始化一個類,包括執行這個類的靜態初始化和初始化在這個類中宣告的靜態欄位。根據Java語言規範,在首次發生下列任意一種情況時,一個類或介面型別T將被立即初始:

        1. T是一個類,而且一個T型別的例項被建立
        2. T是一個類,且T中宣告的一個靜態方法被呼叫
        3. T中宣告的一個靜態欄位被賦值
        4. T中宣告的一個靜態欄位被使用,而且這個欄位不是一個常量欄位
        5. T是一個頂級類,而且一個斷言語句巢狀在T內部被執行
      4. Java語言規範規定,對於每一個類或介面C,都有一個唯一的初始化鎖LC與之對應。從C到LC的對映,由JVM的具體實現去自由實現。JVM在類初始化期間會獲取這個初始化鎖,並且每個執行緒至少獲取一次鎖來確保這個類已經被初始化過了

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

Java中的鎖

Lock介面
  1. 鎖是用來控制多個執行緒訪問共享資源的方式,一般來說,一個鎖能夠防止多個執行緒同時訪問共享資源(但是有些鎖可以允許多個執行緒併發的訪問共享資源,比如讀寫鎖)。在Lock介面出現之前,Java程式是靠synchronized關鍵字實現鎖功能的,而Java SE 5之後,併發包中新增了Lock介面(以及相關實現類)用來實現鎖功能,它提供了與synchronized關鍵字類似的同步功
    能,只是在使用時需要顯式地獲取和釋放鎖。雖然它缺少了(通過synchronized塊或者方法所提供的)隱式獲取釋放鎖的便捷性,但是卻擁有了鎖獲取與釋放的可操作性、可中斷的獲取鎖以及超時獲取鎖等多種synchronized關鍵字所不具備的同步特性
  2. 使用synchronized關鍵字將會隱式地獲取鎖,但是它將鎖的獲取和釋放固化了,也就是先獲取再釋放。當然,這種方式簡化了同步的管理,可是擴充套件性沒有顯示的鎖獲取和釋放來的好。例如,針對一個場景,手把手進行鎖獲取和釋放,先獲得鎖A,然後再獲取鎖B,當鎖B獲得後,釋放鎖A同時獲取鎖C,當鎖C獲得後,再釋放B同時獲取鎖D,以此類推。這種場景下,synchronized關鍵字就不那麼容易實現了,而使用Lock卻容易許多
佇列同步器
  1. 佇列同步器AbstractQueuedSynchronizer(以下簡稱同步器),是用來構建鎖或者其他同步元件的基礎框架,它使用了一個int成員變數表示同步狀態,通過內建的FIFO佇列來完成資源獲取執行緒的排隊工作

  2. 同步器是實現鎖(也可以是任意同步元件)的關鍵,在鎖的實現中聚合同步器,利用同步器實現鎖的語義。可以這樣理解二者之間的關係:鎖是面向使用者的,它定義了使用者與鎖互動的介面(比如可以允許兩個執行緒並行訪問),隱藏了實現細節;同步器面向的是鎖的實現者,它簡化了鎖的實現方式,遮蔽了同步狀態管理、執行緒的排隊、等待與喚醒等底層操作。鎖和同步器很好地隔離了使用者和實現者所需關注的領域

  3. 同步器的設計是基於模板方法模式的,也就是說,使用者需要繼承同步器並重寫指定的方法,隨後將同步器組合在自定義同步元件的實現中,並呼叫同步器提供的模板方法,而這些模板方法將會呼叫使用者重寫的方法

  4. 可重寫的方法

  1. 模板方法

  2. 同步器提供的模板方法基本上分為3類:獨佔式獲取與釋放同步狀態、共享式獲取與釋放同步狀態和查詢同步佇列中的等待執行緒情況。自定義同步元件將使用同步器提供的模板方法來實現自己的同步語義

  3. 自定義一個鎖:繼承實現Lock介面,使用靜態內部類繼承AbstractQueueSynchronizer,定義模板方法,重寫相關的方法,使用模板方法模式以及代理模式,案例如下:

    class Mutex implements Lock {
    // 靜態內部類,自定義同步器
    private static class Sync extends AbstractQueuedSynchronizer {
    // 是否處於佔用狀態
    protected boolean isHeldExclusively() {
    return getState() == 1;
    } /
    / 當狀態為0的時候獲取鎖
    public boolean tryAcquire(int acquires) {
    if (compareAndSetState(0, 1)) {
    setExclusiveOwnerThread(Thread.currentThread());
    return true;
    } r
    eturn false;
    } /
    / 釋放鎖,將狀態設定為0
    protected boolean tryRelease(int releases) {
    if (getState() == 0) throw new
    IllegalMonitorStateException();
    setExclusiveOwnerThread(null);
    setState(0);
    return true;
    } /
    / 返回一個Condition,每個condition都包含了一個condition佇列
    Condition newCondition() { return new ConditionObject(); }
    } /
    / 僅需要將操作代理到Sync上即可
    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);
    } p
    ublic boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
    return sync.tryAcquireNanos(1, unit.toNanos(timeout));
    }
    }
    
  4. 上述示例中,獨佔鎖Mutex是一個自定義同步元件,它在同一時刻只允許一個執行緒佔有鎖。Mutex中定義了一個靜態內部類,該內部類繼承了同步器並實現了獨佔式獲取和釋放同步狀態。在tryAcquire(int acquires)方法中,如果經過CAS設定成功(同步狀態設定為1),則代表獲取了同步狀態,而在tryRelease(int releases)方法中只是將同步狀態重置為0。使用者使用Mutex時並不會直接和內部同步器的實現打交道,而是呼叫Mutex提供的方法,在Mutex的實現中,以獲取鎖的lock()方法為例,只需要在方法實現中呼叫同步器的模板方法acquire(int args)即可,當前執行緒呼叫該方法獲取同步狀態失敗後會被加入到同步佇列中等待,這樣就大大降低了實現一個可靠自定義同步元件的門檻

  5. 實現原理:單執行緒在獲取同步狀態(鎖)時,如果未獲取到,則會被創造成一個節點新增到同步佇列的末尾,同步佇列的所有節點都表示一個執行緒的引用,每個節點都在自旋,檢視前一個結點是否是頭節點,是的話就嘗試獲取同步狀態,當成功獲取後,會喚醒下一個節點,然後首節點去執行自己的任務,否則新增到佇列末尾

重入鎖
  1. 重入鎖ReentrantLock,顧名思義,就是支援重進入的鎖,它表示該鎖能夠支援一個執行緒對資源的重複加鎖。除此之外,該鎖的還支援獲取鎖時的公平和非公平性選擇

  2. 重進入是指任意執行緒在獲取到鎖之後能夠再次獲取該鎖而不會被鎖所阻塞,該特性的實現需要解決以下兩個問題

    1. 執行緒再次獲取鎖。鎖需要去識別獲取鎖的執行緒是否為當前佔據鎖的執行緒,如果是,則再次成功獲取
    2. 鎖的最終釋放。執行緒重複n次獲取了鎖,隨後在第n次釋放該鎖後,其他執行緒能夠獲取到該鎖。鎖的最終釋放要求鎖對於獲取進行計數自增,計數表示當前鎖被重複獲取的次數,而鎖被釋放時,計數自減,當計數等於0時表示鎖已經成功釋放
    3. 通過判斷前執行緒是否為獲取鎖的執行緒來決定獲取操作是否成功,如果是獲取鎖的執行緒再次求,則將同步狀態值進行增加並返回true,表示獲取同步狀態成功
    4. 成功獲取鎖的執行緒再次獲取鎖,只是增加了同步狀態值,這也就要求ReentrantLock在釋放同步狀態時減少同步狀態值
  3. 公平鎖與非公平鎖

    公平性與否是針對獲取鎖而言的,如果一個鎖是公平的,那麼鎖的獲取順序就應該符合請求的絕對時間順序,也就是FIFO

讀寫鎖
  1. 在基礎部分已經介紹了相關的概念,這裡說一些關於鎖的設計的東西

  2. 讀寫鎖同樣依賴自定義同步器來實現同步功能,而讀寫狀態就是其同步器的同步狀態。回想ReentrantLock中自定義同步器的實現,同步狀態表示鎖被一個執行緒重複獲取的次數,而讀寫鎖的自定義同步器需要在同步狀態(一個整型變數)上維護多個讀執行緒和一個寫執行緒的狀態,使得該狀態的設計成為讀寫鎖實現的關鍵

  3. 如果在一個整型變數上維護多種狀態,就一定需要“按位切割使用”這個變數,讀寫鎖將變數切分成了兩個部分,高16位表示讀,低16位表示寫

當前同步狀態表示一個執行緒已經獲取了寫鎖,且重進入了兩次,同時也連續獲取了兩次讀鎖。讀寫鎖是如何迅速確定讀和寫各自的狀態呢? 答案是通過位運算。假設當前同步狀態值為S,寫狀態等於S&0x0000FFFF(將高16位全部抹去),讀狀態等於S>>>16(無符號補0右移16位)。當寫狀態增加1時,等於S+1,當讀狀態增加1時,等於S+(1<<16),也就是S+0x00010000

根據狀態的劃分能得出一個推論:S不等於0時,當寫狀態(S&0x0000FFFF)等於0時,則讀狀態(S>>>16)大於0,即讀鎖已被獲取

Condition介面
  1. 任意一個Jav物件,都擁有一組監視器方法(定義在java.lang.Object上),主要包括wait()、wait(long timeout)、notify()以及notifyAll()方法,這些方法與synchronized同步關鍵字配合,可以實現等待/通知模式。Condition介面也提供了類似Object的監視器方法,與Lock配合可以實現等待/通知模式,但是這兩者在使用方式以及功能特性上還是有差別的

  2. Condition定義了等待/通知兩種型別的方法,當前執行緒呼叫這些方法時,需要提前獲取到Condition物件關聯的鎖。Condition物件是由Lock物件(呼叫Lock物件的newCondition()方法)建立出來的,換句話說,Condition是依賴Lock物件的

    Lock lock = new ReentrantLock();
    Condition condition = lock.newCondition();
    public void conditionWait() throws InterruptedException {
    lock.lock();
    try {
    condition.await();
    } finally {
    lock.unlock();
    }
    }p
    ublic void conditionSignal() throws InterruptedException {
    lock.lock();
    try {
    condition.signal();
    } finally {
    lock.unlock();
    }
    }
    
    1. 等待佇列

      1. 等待佇列是一個FIFO的佇列,在佇列中的每個節點都包含了一個執行緒引用,該執行緒就是在Condition物件上等待的執行緒,如果一個執行緒呼叫了Condition.await()方法,那麼該執行緒將會
        釋放鎖、構造成節點加入等待佇列並進入等待狀態。事實上,節點的定義複用了同步器中節點的定義,也就是說,同步佇列和等待佇列中節點型別都是同步器的靜態內部類AbstractQueuedSynchronizer.Node

    2. 在Object的監視器模型上,一個物件擁有一個同步佇列和等待佇列,而併發包中的Lock(更確切地說是同步器)擁有一個同步佇列和多個等待佇列

    3. 等待

      1. 呼叫Condition的await()方法(或者以await開頭的方法),會使當前執行緒進入等待佇列並釋放鎖,同時執行緒狀態變為等待狀態。當從await()方法返回時,當前執行緒一定獲取了Condition相關聯的鎖
      2. 如果從佇列(同步佇列和等待佇列)的角度看await()方法,當呼叫await()方法時,相當於同步佇列的首節點(獲取了鎖的節點)移動到Condition的等待佇列中
      3. 呼叫該方法的執行緒成功獲取了鎖的執行緒,也就是同步佇列中的首節點,該方法會將當前執行緒構造成節點並加入等待佇列中,然後釋放同步狀態,喚醒同步佇列中的後繼節點,然後當前執行緒會進入等待狀態
      4. 當等待佇列中的節點被喚醒,則喚醒節點的執行緒開始嘗試獲取同步狀態。如果不是通過其他執行緒呼叫Condition.signal()方法喚醒,而是對等待執行緒進行中斷,則會丟擲InterruptedException
    4. 通知

      1. 呼叫Condition的signal()方法,將會喚醒在等待佇列中等待時間最長的節點(首節點),在喚醒節點之前,會將節點移到同步佇列中

        public final void signal() {
        if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
        Node first = firstWaiter;
        if (first != null)
        doSignal(first);
        }
        

        呼叫該方法的前置條件是當前執行緒必須獲取了鎖,可以看到signal()方法進行了isHeldExclusively()檢查,也就是當前執行緒必須是獲取了鎖的執行緒。接著獲取等待佇列的首節點,將其移動到同步佇列並使用LockSupport喚醒節點中的執行緒

Java併發容器和框架

ConcurrentHashMap的實現原理與使用
  1. 為什麼要使用ConcurrentHashMap

    1. HashMap在併發執行put操作時會引起死迴圈,是因為多執行緒會導致HashMap的Entry連結串列形成環形資料結構,一旦形成環形資料結構,Entry的next節點永遠不為空,就會產生死迴圈獲
      取Entry
    2. HashMap在併發執行put操作時會引起死迴圈,是因為多執行緒會導致HashMap的Entry連結串列形成環形資料結構,一旦形成環形資料結構,Entry的next節點永遠不為空,就會產生死迴圈獲取Entry
    3. HashTable容器在競爭激烈的併發環境下表現出效率低下的原因是所有訪問HashTable的執行緒都必須競爭同一把鎖,假如容器裡有多把鎖,每一把鎖用於鎖容器其中一部分資料,那麼當多執行緒訪問容器裡不同資料段的資料時,執行緒間就不會存在鎖競爭,從而可以有效提高併發訪問效率,這就是ConcurrentHashMap所使用的鎖分段技術。首先將資料分成一段一段地儲存,然後給每一段資料配一把鎖,當一個執行緒佔用鎖訪問其中一個段資料的時候,其他段的資料也能被其他執行緒訪問
  2. ConcurrentHashMap的結構

    1. ConcurrentHashMap是由Segment陣列結構和HashEntry陣列結構組成。Segment是一種可重入鎖(ReentrantLock),在ConcurrentHashMap裡扮演鎖的角色;HashEntry則用於儲存鍵值對資料。一個ConcurrentHashMap裡包含一個Segment陣列。Segment的結構和HashMap類似,是一種陣列和連結串列結構。一個Segment裡包含一個HashEntry陣列,每個HashEntry是一個連結串列結構的元素,每個Segment守護著一個HashEntry陣列裡的元素,當對HashEntry陣列的資料進行修改時,必須首先獲得與它對應的Segment鎖

    2. ConcurrentHashMap的初始化

      ConcurrentHashMap初始化方法是通過initialCapacity、loadFactor和concurrencyLevel等幾個引數來初始化segment陣列、段偏移量segmentShift、段掩碼segmentMask和每個segment裡的HashEntry陣列來實現的

    3. 定位Segment

      既然ConcurrentHashMap使用分段鎖Segment來保護不同段的資料,那麼在插入和獲取元素的時候,必須先通過雜湊演算法定位到Segment。可以看到ConcurrentHashMap會首先使用Wang/Jenkins hash的變種演算法對元素的hashCode進行一次再雜湊。

    4. ConcurrentHashMap的操作

      1. get操作

        1. Segment的get操作實現非常簡單和高效。先經過一次再雜湊,然後使用這個雜湊值通過雜湊運算定位到Segment,再通過雜湊演算法定位到元素
        2. get操作的高效之處在於整個get過程不需要加鎖,除非讀到的值是空才會加鎖重讀。我們知道HashTable容器的get方法是需要加鎖的,那麼ConcurrentHashMap的get操作是如何做到不加鎖的呢? 原因是它的get方法裡將要使用的共享變數都定義成volatile型別,如用於統計當前Segement大小的count欄位和用於儲存值的HashEntry的value。定義成volatile的變數,能夠線上程之間保持可見性,能夠被多執行緒同時讀,並且保證不會讀到過期的值,但是隻能被單執行緒寫(有一種情況可以被多執行緒寫,就是寫入的值不依賴於原值),在get操作裡只需要讀不需要寫共享變數count和value,所以可以不用加鎖。之所以不會讀到過期的值,是因為根據Java記憶體模型的happen before原則,對volatile欄位的寫入操作先於讀操作,即使兩個執行緒同時修改和獲取volatile變數,get操作也能拿到最新的值,這是用volatile替換鎖的經典應用場景
      2. put操作

        1. 由於put方法裡需要對共享變數進行寫入操作,所以為了執行緒安全,在操作共享變數時必須加鎖。put方法首先定位到Segment,然後在Segment裡進行插入操作。插入操作需要經歷兩個步驟,第一步判斷是否需要對Segment裡的HashEntry陣列進行擴容,第二步定位新增元素的位置,然後將其放在HashEntry陣列
          1. 在插入元素前會先判斷Segment裡的HashEntry陣列是否超過容量(threshold),如果超過閾值,則對陣列進行擴容。值得一提的是,Segment的擴容判斷比HashMap更恰當,因為HashMap是在插入元素後判斷元素是否已經到達容量的,如果到達了就進行擴容,但是很有可能擴容之後沒有新元素插入,這時HashMap就進行了一次無效的擴容
          2. 在擴容的時候,首先會建立一個容量是原來容量兩倍的陣列,然後將原陣列裡的元素進行再雜湊後插入到新的陣列裡。為了高效,ConcurrentHashMap不會對整個容器進行擴容,而只對某個segment進行擴容
      3. size操作

        1. 如果要統計整個ConcurrentHashMap裡元素的大小,就必須統計所有Segment裡元素的大小後求和。Segment裡的全域性變數count是一個volatile變數,那麼在多執行緒場景下,是不是直接把所有Segment的count相加就可以得到整個ConcurrentHashMap大小了呢? 不是的,雖然相加時可以獲取每個Segment的count的最新值,但是可能累加前使用的count發生了變化,那麼統計結果就不準了。所以,最安全的做法是在統計size的時候把所有Segment的put、remove和clean方法全部鎖住,但是這種做法顯然非常低效

          因為在累加count操作過程中,之前累加過的count發生變化的機率非常小,所以ConcurrentHashMap的做法是先嚐試2次通過不鎖住Segment的方式來統計各個Segment大小,如果統計的過程中,容器的count發生了變化,則再採用加鎖的方式來統計所有Segment的大小

          那麼ConcurrentHashMap是如何判斷在統計的時候容器是否發生了變化呢? 使用modCount變數,在put、remove和clean方法裡操作元素前都會將變數modCount進行加1,那麼在統計size前後比較modCount是否發生變化,從而得知容器的大小是否發生變化

ConcurrentLinkedQueue

在併發程式設計中,有時候需要使用執行緒安全的佇列。如果要實現一個執行緒安全的佇列有兩種方式:一種是使用阻塞演算法,另一種是使用非阻塞演算法。使用阻塞演算法的佇列可以用一個鎖(入隊和出隊用同一把鎖)或兩個鎖(入隊和出隊用不同的鎖)等方式來實現。非阻塞的實現方式則可以使用迴圈CAS的方式來實現。本節讓我們一起來研究一下Doug Lea是如何使用非阻塞的方式來實現執行緒安全佇列ConcurrentLinkedQueue的,相信從大師身上我們能學到不少併發程式設計的技巧

ConcurrentLinkedQueue是一個基於連結節點的無界執行緒安全佇列,它採用先進先出的規則對節點進行排序,當我們新增一個元素的時候,它會新增到佇列的尾部;當我們獲取一個元素時,它會返回佇列頭部的元素。它採用了“wait-free”演算法(即CAS演算法)來實現,該演算法在Michael&Scott演算法上進行了一些修改

阻塞佇列
  1. 阻塞佇列(BlockingQueue)是一個支援兩個附加操作的佇列。這兩個附加的操作支援阻塞的插入和移除方法

    1. 支援阻塞的插入方法:意思是當佇列滿時,佇列會阻塞插入元素的執行緒,直到佇列不滿
    2. 支援阻塞的移除方法:意思是在佇列為空時,獲取元素的執行緒會等待佇列變為非空
  2. 阻塞佇列常用於生產者和消費者的場景,生產者是向佇列裡新增元素的執行緒,消費者是從佇列裡取元素的執行緒。阻塞佇列就是生產者用來存放元素、消費者用來獲取元素的容器

  3. Java裡的阻塞佇列

    1. ArrayBlockingQueue:一個由陣列結構組成的有界阻塞佇列
    2. LinkedBlockingQueue:一個由連結串列結構組成的有界阻塞佇列
    3. PriorityBlockingQueue:一個支援優先順序排序的無界阻塞佇列
    4. DelayQueue:一個使用優先順序佇列實現的無界阻塞佇列
    5. SynchronousQueue:一個不儲存元素的阻塞佇列
    6. LinkedTransferQueue:一個由連結串列結構組成的無界阻塞佇列
    7. LinkedBlockingDeque:一個由連結串列結構組成的雙向阻塞佇列
  4. 阻塞佇列的實現原理

    1. 使用通知模式實現。所謂通知模式,就是當生產者往滿的佇列裡新增元素時會阻塞住生產者,當消費者消費了一個佇列中的元素後,會通知生產者當前佇列可用

      1. 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();
        }
        }p
        ublic E take() throws InterruptedException {
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
        while (count == 0)
        notEmpty.await();
        return extract();
        } finally {
        lock.unlock();
        }
        }p
        rivate void insert(E x) {
        items[putIndex] = x;
        putIndex = inc(putIndex);
        ++count;
        notEmpty.signal();
        }
        
    2. 當往佇列裡插入一個元素時,如果佇列不可用,那麼阻塞生產者主要通過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;
      }i
      f (acquireQueued(node, savedState) && interruptMode != THROW_IE)
      interruptMode = REINTERRUPT;
      if (node.nextWaiter != null) // clean up if cancelled
      unlinkCancelledWaiters();
      if (interruptMode != 0)
      reportInterruptAfterWait(interruptMode);
      }
      

Java中的常用原子操作類

原子更新基本型別類
  1. AtomicBoolean:原子更新布林型別
  2. AtomicInteger:原子更新整型
  3. AtomicLong:原子更新長整型
原子更新陣列
  1. AtomicIntegerArray:原子更新整型陣列裡的元素
  2. AtomicLongArray:原子更新長整型陣列裡的元素
  3. AtomicReferenceArray:原子更新引用型別陣列裡的元素
原子更新引用型別
  1. AtomicReference:原子更新引用型別
  2. AtomicReferenceFieldUpdater:原子更新引用型別裡的欄位
  3. AtomicMarkableReference:原子更新帶有標記位的引用型別
原子更新欄位類
  1. AtomicIntegerFieldUpdater:原子更新整型的欄位的更新器
  2. AtomicLongFieldUpdater:原子更新長整型欄位的更新器
  3. AtomicStampedReference:原子更新帶有版本號的引用型別

Java中的併發工具類

等待多執行緒完成的CountDownLatch
  1. CountDownLatch允許一個或多個執行緒等待其他執行緒完成操作

  2. public class CountDownLatchTest {
    staticCountDownLatch c = new CountDownLatch(2);
    public static void main(String[] args) throws InterruptedException {
    new Thread(new Runnable() {
    @Override
    public void run() {
    System.out.println(1);
    c.countDown();
    System.out.println(2);
    c.countDown();
    }
    }).start();
    c.await();
    System.out.println("3");
    }
    }
    
  3. CountDownLatch的建構函式接收一個int型別的引數作為計數器,如果你想等待N個點完成,這裡就傳入N。當我們呼叫CountDownLatch的countDown方法時,N就會減1,CountDownLatch的await方法會阻塞當前執行緒,直到N變成零。由於countDown方法可以用在任何地方,所以這裡說的N個點,可以是N個執行緒,也可以是1個執行緒裡的N個執行步驟。用在多個執行緒時,只需要把這個CountDownLatch的引用傳遞到執行緒裡即可

  4. 注意:計數器必須大於等於0,只是等於0時候,計數器就是零,呼叫await方法時不會阻塞當前執行緒;若計數器大於執行緒數,那麼就會一直等待,不會執行await()後的語句。CountDownLatch不可能重新初始化或者修改CountDownLatch物件的內部計數器的值。一個執行緒呼叫countDown方法happen-before,另外一個執行緒呼叫await方法

同步屏障CyclicBarrier
  1. CyclicBarrier的字面意思是可迴圈使用(Cyclic)的屏障(Barrier)。它要做的事情是,讓一組執行緒到達一個屏障(也可以叫同步點)時被阻塞,直到最後一個執行緒到達屏障時,屏障才會開門,所有被屏障攔截的執行緒才會繼續執行

  2. CyclicBarrier預設的構造方法是CyclicBarrier(int parties),其參數列示屏障攔截的執行緒數量,每個執行緒呼叫await方法告訴CyclicBarrier我已經到達了屏障,然後當前執行緒被阻塞

  3. CyclicBarrier還提供一個更高階的建構函式CyclicBarrier(int parties,Runnable barrierAction),用於線上程到達屏障時,優先執行barrierAction,方便處理更復雜的業務場景

  4. package java_Multi_thread_programming.module.code46;
    
    import java.util.concurrent.BrokenBarrierException;
    import java.util.concurrent.CyclicBarrier;
    
    public class Test3 {
        public static void main(String[] args) {
            Thread[] threads = new Thread[10];
            CyclicBarrier cyclicBarrier = new CyclicBarrier(10, new Runnable() {
                @Override
                public void run() {
                    System.out.println("開始執行:"+System.currentTimeMillis());
                }
            });
    
            for (int i = 0; i < threads.length; i++) {
                threads[i] = new Thread(new Runnable() {
                    @Override
                    public void run() {
                        try {
                            cyclicBarrier.await();
                            System.out.println("執行緒"+Thread.currentThread().getName()+"執行:"+ System.currentTimeMillis());
                        } catch (InterruptedException | BrokenBarrierException e) {
                            e.printStackTrace();
                        }
                    }
                });
                threads[i].start();
            }
        }
    }
    
  5. 注意:當傳入的執行緒數大於執行緒使用的數時,會一直等待,知道使用的執行緒數等於攔截的執行緒數

  6. CyclicBarrier和CountDownLatch的區別

    1. CountDownLatch的計數器只能使用一次,而CyclicBarrier的計數器可以使用reset()方法重置。所以CyclicBarrier能處理更為複雜的業務場景。例如,如果計算髮生錯誤,可以重置計數器,並讓執行緒重新執行一次
    2. CountDownLatch的計數器只能使用一次,而CyclicBarrier的計數器可以使用reset()方法重置。所以CyclicBarrier能處理更為複雜的業務場景。例如,如果計算髮生錯誤,可以重置計數器,並讓執行緒重新執行一次
    3. CountDownLatch是執行完指定數量的執行緒後執行await()後的語句(代替join)使用;CyclicBarrier是當要執行的執行緒數夠了才執行每個執行緒中await()後面的語句
    4. CountDownLatch(需要最後執行的語句前面)主要使用await()方法的位置不同於CyclicBarrier(每個執行緒)
控制併發執行緒數的Semaphore
  1. Semaphore(訊號量)是用來控制同時訪問特定資源的執行緒數量,它通過協調各個執行緒,以保證合理的使用公共資源。它比作是控制流量的紅綠燈。比如××馬路要限制流量,只允許同時有一百輛車在這條路上行使,其他的都必須在路口等待,所以前一百輛車會看到綠燈,可以開進這條馬路,後面的車會看到紅燈,不能駛入××馬路,但是如果前一百輛中有5輛車已經離開了××馬路,那麼後面就允許有5輛車駛入馬路,這個例子裡說的車就是執行緒,駛入馬路就表示執行緒在執行,離開馬路就表示執行緒執行完成,看見紅燈就表示執行緒被阻塞,不能執行

  2. public class SemaphoreTest {
    private static final int THREAD_COUNT = 30;
    private static ExecutorServicethreadPool = Executors
    .newFixedThreadPool(THREAD_COUNT);
    private static Semaphore s = new Semaphore(10);
    public static void main(String[] args) {
    for (inti = 0; i< THREAD_COUNT; i++) {
    threadPool.execute(new Runnable() {
    @Override
    public void run() {
    try {
    s.acquire();System.out.println("save data");
    s.release();
    } catch (InterruptedException e) {
    }
    }
    });
    }
    threadPool.shutdown();
    }
    }
    
執行緒間交換資料的Exchanger
  1. Exchanger(交換者)是一個用於執行緒間協作的工具類。Exchanger用於進行執行緒間的資料交換。它提供一個同步點,在這個同步點,兩個執行緒可以交換彼此的資料。這兩個執行緒通過exchange方法交換資料,如果第一個執行緒先執行exchange()方法,它會一直等待第二個執行緒也執行exchange方法,當兩個執行緒都到達同步點時,這兩個執行緒就可以交換資料,將本執行緒生產出來的資料傳遞給對方

  2. package java_Multi_thread_programming.module.code46;
    
    import java.util.concurrent.Exchanger;
    
    public class Test4 {
        public static Exchanger<String> exchanger = new Exchanger<>();
        public static void main(String[] args) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    String string = ""+Thread.currentThread().getName();
                    try {
                        System.out.println(string+exchanger.exchange(string));
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }, "執行緒1").start();
    
            new Thread(new Runnable() {
                @Override
                public void run() {
                    String string1 = "" + Thread.currentThread().getName();
                    try {
                        System.out.println(string1+exchanger.exchange(string1));
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }, "執行緒2").start();
        }
    }
    
    
  3. 注意:如果兩個執行緒有一個沒有執行exchange()方法,則會一直等待,如果擔心有特殊情況發生,避免一直等待,可以使用exchange(V x,longtimeout,TimeUnit unit)設定最大等待時長

Java中的執行緒池

Java中的執行緒池是運用場景最多的併發框架,幾乎所有需要非同步或併發執行任務的程式都可以使用執行緒池。在開發過程中,合理地使用執行緒池能夠帶來3個好處
第一:降低資源消耗。通過重複利用已建立的執行緒降低執行緒建立和銷燬造成的消耗
第二:提高響應速度。當任務到達時,任務可以不需要等到執行緒建立就能立即執行
第三:提高執行緒的可管理性。執行緒是稀缺資源,如果無限制地建立,不僅會消耗系統資源,還會降低系統的穩定性,使用執行緒池可以進行統一分配、調優和監控

執行緒池的實現原理

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

  1. 執行緒池判斷核心執行緒池裡的執行緒是否都在執行任務。如果不是,則建立一個新的工作執行緒來執行任務。如果核心執行緒池裡的執行緒都在執行任務,則進入下個流程
  2. 執行緒池判斷工作佇列是否已經滿。如果工作佇列沒有滿,則將新提交的任務儲存在這個工作佇列裡。如果工作佇列滿了,則進入下個流程
  3. 執行緒池判斷執行緒池的執行緒是否都處於工作狀態。如果沒有,則建立一個新的工作執行緒來執行任務。如果已經滿了,則交給飽和策略來處理這個任務

ThreadPoolExecutor執行execute()方法的示意圖如下:

ThreadPoolExecutor執行execute方法分下面4種情況

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

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

執行緒池中的執行緒執行任務分兩種情況,如下

  1. 在execute()方法中建立一個執行緒時,會讓這個執行緒執行當前任務
  2. 這個執行緒執行完上圖中1的任務後,會反覆從BlockingQueue獲取任務來執行
執行緒池的使用
  1. 執行緒池的建立

    1. new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime,
      milliseconds,runnableTaskQueue, handler);
      
    2. 建立一個執行緒池時需要輸入幾個引數,如下 :

      1. corePoolSize(執行緒池的基本大小):當提交一個任務到執行緒池時,執行緒池會建立一個執行緒來執行任務,即使其他空閒的基本執行緒能夠執行新任務也會建立執行緒,等到需要執行的任
        務數大於執行緒池基本大小時就不再建立。如果呼叫了執行緒池的prestartAllCoreThreads()方法,執行緒池會提前建立並啟動所有基本執行緒

      2. runnableTaskQueue(任務佇列):用於儲存等待執行的任務的阻塞佇列。可以選擇以下幾個阻塞佇列

        1. ArrayBlockingQueue:是一個基於陣列結構的有界阻塞佇列,此佇列按FIFO(先進先出)原則對元素進行排序
        2. LinkedBlockingQueue:一個基於連結串列結構的阻塞佇列,此佇列按FIFO排序元素,吞吐量通常要高於ArrayBlockingQueue。靜態工廠方法Executors.newFixedThreadPool()使用了這個佇列
        3. SynchronousQueue:一個不儲存元素的阻塞佇列。每個插入操作必須等到另一個執行緒呼叫移除操作,否則插入操作一直處於阻塞狀態,吞吐量通常要高於Linked-BlockingQueue,靜態工廠方法Executors.newCachedThreadPool使用了這個佇列
        4. PriorityBlockingQueue:一個具有優先順序的無限阻塞佇列
      3. maximumPoolSize(執行緒池最大數量):執行緒池允許建立的最大執行緒數。如果佇列滿了,並且已建立的執行緒數小於最大執行緒數,則執行緒池會再建立新的執行緒執行任務。值得注意的是,如果使用了無界的任務佇列這個引數就沒什麼效果

      4. ThreadFactory:用於設定建立執行緒的工廠,可以通過執行緒工廠給每個建立出來的執行緒設定更有意義的名字。使用開源框架guava提供的ThreadFactoryBuilder可以快速給執行緒池裡的線
        程設定有意義的名字,程式碼如下

        new ThreadFactoryBuilder().setNameFormat("XX-task-%d").build();
        
      5. RejectedExecutionHandler(飽和策略):當佇列和執行緒池都滿了,說明執行緒池處於飽和狀態,那麼必須採取一種策略處理提交的新任務。這個策略預設情況下是AbortPolicy,表示無法處理新任務時丟擲異常。在JDK 1.5中Java執行緒池框架提供了以下4種策略

        1. AbortPolicy:直接丟擲異常
        2. CallerRunsPolicy:只用呼叫者所線上程來執行任務
        3. DiscardOldestPolicy:丟棄佇列裡最近的一個任務,並執行當前任務
        4. DiscardPolicy:不處理,丟棄掉。當然,也可以根據應用場景需要來實現RejectedExecutionHandler介面自定義策略。如記錄日誌或持久化儲存不能處理的任務
        5. keepAliveTime(執行緒活動保持時間):執行緒池的工作執行緒空閒後,保持存活的時間。所以,如果任務很多,並且每個任務執行的時間比較短,可以調大時間,提高執行緒的利用率
        6. TimeUnit(執行緒活動保持時間的單位):可選的單位有天(DAYS)、小時(HOURS)、分鐘(MINUTES)、毫秒(MILLISECONDS)、微秒(MICROSECONDS,千分之一毫秒)和納秒(NANOSECONDS,千分之一微秒)
  2. 向執行緒池提交任務

    1. 可以使用兩個方法向執行緒池提交任務,分別為execute()和submit()方法
    2. execute()方法用於提交不需要返回值的任務,所以無法判斷任務是否被執行緒池執行成功
    3. submit()方法用於提交需要返回值的任務。執行緒池會返回一個future型別的物件,通過這個future物件可以判斷任務是否執行成功,並且可以通過future的get()方法來獲取返回值,get()方法會阻塞當前執行緒直到任務完成,而使用get(long timeout,TimeUnit unit)方法則會阻塞當前執行緒一段時間後立即返回,這時候有可能任務沒有執行完
  3. 關閉執行緒池

    1. 可以通過呼叫執行緒池的shutdown或shutdownNow方法來關閉執行緒池。它們的原理是遍歷執行緒池中的工作執行緒,然後逐個呼叫執行緒的interrupt方法來中斷執行緒,所以無法響應中斷的任務可能永遠無法終止。但是它們存在一定的區別,shutdownNow首先將執行緒池的狀態設定成STOP,然後嘗試停止所有的正在執行或暫停任務的執行緒,並返回等待執行任務的列表,而shutdown只是將執行緒池的狀態設定成SHUTDOWN狀態,然後中斷所有沒有正在執行任務的執行緒
    2. 只要呼叫了這兩個關閉方法中的任意一個,isShutdown方法就會返回true。當所有的任務都已關閉後,才表示執行緒池關閉成功,這時呼叫isTerminaed方法會返回true。至於應該呼叫哪一種方法來關閉執行緒池,應該由提交到執行緒池的任務特性決定,通常呼叫shutdown方法來關閉執行緒池,如果任務不一定要執行完,則可以呼叫shutdownNow方法

相關文章