1. 程式人生 > >Java併發程式設計札記-總結

Java併發程式設計札記-總結

(一)基礎

01基本概念

併發

什麼是併發

併發是一種能力,是一種將程式分為幾個片段,在單獨的處理器上執行每個片段,而不影響最終結果的能力。

併發的優點

可以顯著提高程式在多處理器和多核系統中的速度。

多執行緒

多執行緒就是達到併發目的的一種手段。多執行緒,是指從軟體或者硬體上實現多個執行緒併發執行的技術。應用程式可以使用多執行緒將程式分割為多個子任務,並讓底層體系結構管理執行緒如何執行,可以併發在一個核心上,也可以並行在多個核心上執行。

執行緒

執行緒是可以由排程程式獨立管理的最小程式指令序列。

併發與並行

通俗的說,併發是多個任務交替執行,而並行是多個任務同時執行。兩者的關鍵在於“同時”這個關鍵詞。

執行緒和程序

在計算中,程序是正在執行的計算機程式的一個例項。執行緒是可以由排程程式獨立管理的最小程式指令序列。一個程序可以由多個執行執行緒組成。

02建立執行緒

建立執行緒的方式

  • Runnable-定義無返回值的任務
  • Thread-執行緒構造器
  • Callable-定義可以返回值的任務

我認為Runnable和Callable的作用只是定義任務,建立執行緒還是需要Thread構造器來完成。

Thread和Runnable該如何選擇?

  • 因為Java不支援多繼承,但支援多實現。所以從這個方面考慮Runnable比Thread有優勢。
  • Thread中提供了一些基本方法。而Runnable中只有run方法。如果只想重寫run()方法,而不重寫其他Thread方法,那麼應使用Runnable介面。除非打算修改或增強Thread類的基本行為,否則應該選擇Runnable。

從上面的分析可以看到,一般情況下Runnable更有優勢。

run方法與start方法的區別

啟動執行緒的方法是start方法。執行緒t啟動後,t從新建狀態轉為就緒狀態, 但並沒有執行。 t獲取CPU許可權後才會執行,執行時執行的方法就是run方法。此時有t和主執行緒兩個執行緒在執行,如果t阻塞,可以直接繼續執行主執行緒中的程式碼。
直接執行run方法也是合法的,但此時並沒有新啟動一個執行緒,run方法是在主執行緒中執行的。此時只有主執行緒在執行,必須等到run方法中的程式碼執行完後才可以繼續執行主執行緒中的程式碼。

03執行緒的生命週期

執行緒的生命週期圖

MarkdownPhotos/master/CSDNBlogs/concurrency/0103/threadStaus.jpg
此圖是根據自己的瞭解畫的,如果有不足或錯誤歡迎指正。

執行緒的狀態

Java中執行緒有哪些狀態在Thread.State列舉中的介紹得很清楚。六種狀態分別是NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED。

04Thread詳解

執行緒等待與喚醒(wait()、notify()/notifyAll())

wait()作用是在其他執行緒呼叫此物件的notify()方法或notifyAll()方法前,或者其他某個執行緒中斷當前執行緒前,導致當前執行緒等待。wait(long timeout)、wait(long timeout, int nanos)作用是在已超過某個實際時間量前,或者其他某個執行緒中斷當前執行緒前,導致當前執行緒等待。使用wait方法有個條件:當前執行緒必須持有物件的鎖。執行wait後,當前執行緒會失去物件的鎖,狀態變為WAITING或者TIMED_WAITING狀態。
notify()可以隨機喚醒正在等待的多個執行緒中的一個。被喚醒的執行緒並不能馬上參與對鎖的競爭,必須等呼叫notify的執行緒釋放鎖後才能參與對鎖的競爭。而且被喚醒的執行緒在競爭鎖時沒有任何優勢。
同wait方法一樣,使用notify方法有個條件:執行緒必須持有物件的鎖。執行notify方法後,執行緒會繼續執行,並不會馬上釋放物件的鎖。所以才有了上文中的“被喚醒的執行緒並不能馬上參與對鎖的競爭,必須等呼叫notify的執行緒釋放鎖後才能參與對鎖的競爭。”。
notifyAll()與notify()類似,區別是它可以喚醒在此物件監視器上等待的所有執行緒。

執行緒讓步(yield())

API中對yield()的介紹是可以暫停當前正在執行的執行緒物件,並執行其他執行緒。“暫停”代表著讓出CPU,但不會釋放鎖。執行yield()後,當前執行緒由執行狀態變為就緒狀態。但不能保證在當前執行緒呼叫yield()之後,其它具有相同優先順序的執行緒就一定能獲得執行權,也有可能是當前執行緒又進入到執行狀態繼續執行!
yield()與無參的wait()的區別:

  • 執行yield()後,當前執行緒由執行狀態變為就緒狀態。執行wait後,當前執行緒會失去物件的鎖,狀態變為WAITING狀態。
  • 執行yield()後,當前執行緒不會釋放鎖。執行wait後,當前執行緒會釋放鎖。

執行緒休眠(sleep())

執行緒的休眠(暫停執行)與sleep(long millis)和sleep(long millis, int nanos)有關。API中的介紹是sleep(long millis) 方法可以在指定的毫秒數內讓當前正在執行的執行緒休眠;sleep(long millis, int nanos) 可以在指定的毫秒數加指定的納秒數內讓當前正在執行的執行緒休眠。該執行緒不丟失任何監視器的所屬權。簡單來說就是sleep方法可以使正在執行的執行緒讓出CPU,但不會釋放鎖。執行sleep方法後,當前執行緒由執行狀態變為TIMED_WAITING狀態。

sleep()與有參的wait()的區別是:

  • 執行sleep()後,當前執行緒不會釋放鎖。執行有參的wait()後,當前執行緒會釋放鎖。

sleep()與yield()的區別是:

  • 執行sleep後,當前執行緒狀態變為TIMED_WAITING狀態。執行yield()後,當前執行緒由執行狀態變為WAITING狀態。

執行緒啟動(start())

中斷執行緒(interrupt())

interrupt()常常被用來中斷處於阻塞狀態的執行緒。
interrupted()與isInterrupted()都可以測試當前執行緒是否已經中斷。區別在於執行緒的中斷狀態由interrupted()清除。換句話說,如果連續兩次呼叫interrupted(),則第二次呼叫將返回false。

執行緒優先順序

每個執行緒都有一個優先順序,高優先順序執行緒的執行優先於低優先順序執行緒。java 中的執行緒優先順序的範圍是1~10,預設的優先順序是5。
setPriority(int newPriority)和getPriority()分別用來更改執行緒的優先順序和返回執行緒的優先順序。

執行緒等待(join())

join()的作用是等待該執行緒終止,常常被用來讓主執行緒在子執行緒執行結束之後才能繼續執行。如在主執行緒main中呼叫了thread.join(),那麼主執行緒會等待thread執行完後才繼續執行。join(long millis)、join(long millis , int nanos)功能與join()類似,但限定了等待時間,join(long millis)意味著等待該執行緒終止的時間最長為millis毫秒,join(long millis , int nanos)意味著等待該執行緒終止的時間最長為millis毫秒 + nanos 納秒,超時將主執行緒將繼續執行。join()等價於join(0),即超時為0,意味著要一直等下去。
從原始碼中可以瞭解到,join()的實現依賴於wait方法,所以join()釋放鎖。

守護執行緒

Java中有兩種執行緒:使用者執行緒和守護執行緒。守護執行緒是一種特殊的執行緒,它的作用是為其他執行緒提供服務。例如GC執行緒就是守護執行緒。當正在執行的執行緒都是守護執行緒時,Java虛擬機器退出,守護執行緒自動銷燬。
setDaemon(boolean on)用於將該執行緒標記為守護執行緒或使用者執行緒。該方法必須在啟動執行緒前呼叫。isDaemon()用於測試該執行緒是否為守護執行緒。

wait(),sleep(),yield(),join()區別

  • wait()方法在Object中定義的,其他只有Thread中有。
  • wait(),join()會釋放鎖,sleep(),yield()不會。
  • sleep()可以有引數,yield()沒有。

05執行緒安全問題

執行緒安全問題有哪些

在多執行緒程式設計中,可能會出現多個執行緒訪問一個資源的情況,資源可以是同一記憶體區(變數,陣列,或物件)、系統(資料庫,web services等)或檔案等等。如果不對這樣的訪問做控制,就可能出現不可預知的結果。這就是執行緒安全問題,常見的情況是“丟失修改”、“不可重複讀”、“讀‘髒’資料”等等。

執行緒安全問題解決方法

  • 內部鎖(Synchronized)和顯式鎖(Lock)。這兩種方式是重量級的多執行緒同步機制,可能會引起上下文切換和執行緒排程,它同時提供記憶體可見性、有序性和原子性。
  • volatile:輕量級多執行緒同步機制,不會引起上下文切換和執行緒排程。僅提供記憶體可見性、有序性保證,不提供原子性。
  • CAS原子指令:輕量級多執行緒同步機制,不會引起上下文切換和執行緒排程。它同時提供記憶體可見性、有序性和原子化更新保證。

06synchronized

修飾物件

  • 方法。作用範圍是整個方法,作用的物件是呼叫這個方法的物件;
  • 程式碼塊。作用範圍是大括號{}括起來的程式碼,作用的物件是呼叫這個程式碼塊的物件;
  • 靜態方法。作用範圍是整個靜態方法,作用的物件是這個類的所有物件;
  • 類。作用範圍是synchronized後面括號括起來的部分,作用的物件是這個類的所有物件。

注意事項

  • 將域設定為private。在使用併發時,要將域設定為private,否則synchronized就不能阻止其他任務直接訪問域,這樣可能會產生不可預知的結果。
  • 一個任務可以多次獲得物件的鎖。如果一個任務在同一個物件上呼叫了第二個方法,後者又呼叫了同一個物件上的第三個方法,這個任務就會多次獲取這個物件的鎖。每當任務執行所有的方法,鎖才被完全釋放。
  • 每個訪問臨界資源的方法都必須被同步。如果在你的類中有超過一個方法在處理臨界資料,那麼必須同步所有的方法。如果只同步一個方法,其他方法可以忽略這個鎖。所以,每個訪問臨界資源的方法都必須被同步。
  • 異常自動釋放鎖 。當一個執行緒執行的程式碼出現異常時,其所持有的鎖會自動釋放。

07volatile詳解

什麼是volatile

Java中的volatile可以看做是“輕量級的synchronized”。synchronized可能會引起上下文切換和執行緒排程,同時保證可見性、有序性和原子性。volatile不會引起上下文切換和執行緒排程,但僅提供可見性和有序性保證,不提供原子性保證。

原子操作與原子性

如果一系列(或者一個)操作是不可中斷的,要麼都執行,要麼不執行,就稱操作是原子操作,具有原子性。
拿移動支付舉例,A使用者向B使用者付款100元,其中包含兩個操作:A使用者賬戶扣減100元,B使用者賬戶增加100元。如果這兩個操作不是原子操作就可能會出錯,比如A賬戶賬戶扣減100元,但B使用者賬戶並沒有增加100元。

可見性

可見性指的是多個執行緒對共享資源的可見性。當一個執行緒修改了某一資源,其他執行緒能夠看到修改結果。

有序性

有效性指程式按照程式碼的先後順序執行。

synchronized鎖與volatile變數的比較

  • volatile變數最大的優點是使用方便。在某些情形下,使用volatile變數要比使用相應的synchronized鎖簡單得多。
  • 某些情況下,volatile變數同步機制的效能要優於synchronized鎖。
  • volatile變數不會像synchronized鎖一樣造成阻塞。
  • volatile變數最大的缺點在於使用範圍有限,而且容易出錯。

總的來說volatile變數使用範圍有限,不能替代synchronized,但在某些場景下,使用volatile更好。

(二)JUC概述

從今天開始學習JUC。JUC是java.util.concurrent包的簡稱。下圖是JUC的整體結構。參考JDK1.8的java.util.concurrent,畫出下圖。

MarkdownPhotos/master/CSDNBlogs/concurrency/0201/J.U.C.png

atomic

以下是JUC中的原子類。
MarkdownPhotos/master/CSDNBlogs/concurrency/0201/atomic.png

locks

以下是JUC中的鎖,也稱顯示鎖。

MarkdownPhotos/master/CSDNBlogs/concurrency/0201/locks.png

collections

以下是JUC中的集合。

MarkdownPhotos/master/CSDNBlogs/concurrency/0201/collections.png

threadPool

以下是JUC中與執行緒池有關的類。

MarkdownPhotos/master/CSDNBlogs/concurrency/0201/executor.png

tools

以下是JUC中的工具類。

MarkdownPhotos/master/CSDNBlogs/concurrency/0201/tools.png

(三)JUC原子類

01概述

參考JDK1.8的java.util.concurrent.atomic包,畫出如下圖:

可以將包中的類分為五類:

  • 基本型別:AtomicBoolean、AtomicInteger、AtomicLong
  • 引用型別:AtomicReference、AtomicStampedRerence、AtomicMarkableReference
  • 陣列:AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray
  • 物件的屬性:AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater
  • JDK1.8新增:DoubleAccumulator、LongAccumulator、DoubleAdder、LongAdder

基本型別

AtomicBoolean、AtomicInteger、AtomicLong和AtomicReference的例項各自提供對相應型別單個變數的原子方式訪問和更新功能。例如AtomicBoolean提供對int型別單個變數的原子方式訪問和更新功能。
每個類也為該型別提供適當的實用工具方法。例如,類AtomicLong和AtomicInteger提供了原子增量方法,可以用於生成序列號。

引用型別

AtomicStampedRerence維護帶有整數“標誌”的物件引用,可以用原子方式對其進行更新。AtomicMarkableReference維護帶有標記位的物件引用,可以原子方式對其進行更新。

陣列

AtomicIntegerArray、AtomicLongArray和AtomicReferenceArray類進一步擴充套件了原子操作,對這些型別的陣列提供了支援。例如AtomicIntegerArray是可以用原子方式更新其元素的int陣列。

物件的屬性

AtomicReferenceFieldUpdater、AtomicIntegerFieldUpdater和AtomicLongFieldUpdater是基於反射的實用工具,可以提供對關聯欄位型別的訪問。例如AtomicIntegerFieldUpdater可以對指定類的指定volatile int欄位進行原子更新。

JDK1.8新增

DoubleAccumulator、LongAccumulator、DoubleAdder、LongAdder是JDK1.8新增的部分,是對AtomicLong等類的改進。比如LongAccumulator與LongAdder在高併發環境下比AtomicLong更高效。

原子類可以替換鎖嗎?

原子類不是鎖的常規替換方法。僅當物件的重要更新限定於單個變數時才應用它。

原子類和java.lang.Integer等類的區別

原子類不提供諸如hashCode和compareTo之類的方法。因為原子變數是可變的。

為什麼只提供了int、long、boolean這幾種基本型別的原子類?

待補充。

原子類的實現原理

原子類是基於CAS實現的。

07CAS

CAS,compare and swap的縮寫,意為比較並交換。CAS操作包含三個運算元:記憶體值(V),預期值(A)、新值(B)。如果記憶體值與預期值相同,就將記憶體值修改為新值,否則不做任何操作。

java.util.concurrent.atomic是建立在CAS之上的。下面以AtomicLong為例看下是如何使用CAS的。
下面看下AtomicLong的compareAndSet方法。

// Java不能直接訪問作業系統底層,所以使用Unsafe類提供硬體級別的原子操作。
//Unsafe.compareAndSwapLong是CAS操作
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;

/**
 * 如果當前記憶體值等於預期值,原子更新當前值為新值value
 * 
 * @param expect 預期值
 * @param update 新值
 * @return {@code true} 如果成功,則返回 true。返回 false 指示實際值與預期值不相等。
 */
public final boolean compareAndSet(long expect, long update) {
    //使用unsafe來實現CAS
    return unsafe.compareAndSwapLong(this, valueOffset, expect, update);
}

從原始碼中可以看到,AtomicLong.compareAndSet利用unsafe..compareAndSwapLong(this, valueOffset, expect, update)實現CAS操作,而unsafe通過呼叫JNI來完成CPU指令的操作。JNI是Java Native Interface的縮寫,允許Java呼叫其他語言,unsafe.compareAndSwapLong方法就是藉助C來呼叫CPU底層指令實現。其他的原子類中也大量使用了類似unsafe..compareAndSwap×××的方式。

CAS缺點

  • ABA問題:CAS操作先取出記憶體值,然後才將記憶體值與期望值比較。這當中可能會出現某些問題,比如,thread1獲得了記憶體值V,thread2也從記憶體中取出V,並且thread2進行了一些操作將記憶體值修改為B,然後two又將記憶體值修改為V,當前執行緒的CAS操作無法分辨記憶體值V是否發生過變化。儘管CAS成功,但可能存在潛在的問題。舉個生活中的例子,你倒了一杯水,然後有事離開,回來後看到還是一杯水,但你不能確定是不是有人已經把這杯水喝掉然後又給你倒了一杯水。儘管還是一杯水,但已經不一樣了,而且可能存在潛在的問題。解決問題的方法是對“這杯水”設定一個標記,這樣回來時就可以判斷“這杯水”是不是被動過。AtomicStampedReference和AtomicMarkableReference可以實現標記的功能。

(四)JUC鎖

01概述

JUC鎖位於java.util.concurrent.locks包下,為鎖和等待條件提供一個框架的介面和類,它不同於內建同步和監視器。參考JDK1.8的java.util.concurrent.locks包,畫出如下圖:
MarkdownPhotos/master/CSDNBlogs/concurrency/0201/locks.png
CountDownLatch,CyclicBarrier和Semaphore不在包中,但也是通過AQS來實現的。因此,我也將它們歸納到JUC鎖中進行介紹。

Lock
Lock實現提供了比使用synchronized方法和語句可獲得的更廣泛的鎖定操作。

ReentrantLock
一個可重入的互斥鎖,它具有與隱式鎖synchronized相同的一些基本行為和語義,但功能更強大。

AbstractOwnableSynchronizer/AbstractQueuedSynchronizer/AbstractQueuedLongSynchronizer
AbstractQueuedSynchronizer就是被稱之為AQS的類,為實現依賴於先進先出 (FIFO) 等待佇列的阻塞鎖和相關同步器(訊號量、事件,等等)提供一個框架。ReentrantLock,ReentrantReadWriteLock,CountDownLatch,CyclicBarrier和Semaphore等這些類都是基於AQS類實現的。
AbstractQueuedLongSynchronizer以long形式維護同步狀態的一個AbstractQueuedSynchronizer版本。
AbstractQueuedSynchronizer與AbstractQueuedLongSynchronizer都繼承了AbstractOwnableSynchronizer。AbstractOwnableSynchronizer是可以由執行緒以獨佔方式擁有的同步器。

Condition
Condition又稱等待條件,它實現了對鎖更精確的控制。Condition中的await()方法相當於Object的wait()方法,Condition中的signal()方法相當於Object的notify()方法,Condition中的signalAll()相當於Object的notifyAll()方法。不同的是,Object中的wait(),notify(),notifyAll()方法是和synchronized組合使用的;而Condition需要與Lock組合使用。

ReentrantReadWriteLock
ReentrantReadWriteLock維護了一對相關的鎖,一個用於只讀操作,另一個用於寫入操作。

LockSupport
用來建立鎖和其他同步類的基本執行緒阻塞原語。

CountDownLatch
一個同步輔助類,在完成一組正在其他執行緒中執行的操作之前,它允許一個或多個執行緒一直等待。

CyclicBarrier
一個同步輔助類,它允許一組執行緒互相等待,直到到達某個公共屏障點。

Semaphore
一個計數訊號量。從概念上講,訊號量維護了一個許可集。Semaphore通常用於限制可以訪問某些資源的執行緒數目。

02Lock與ReentrantLock

synchronized 與Lock比較

  • 與synchronized 相比Lock的使用更靈活。Lock介面的實現允許鎖在不同的範圍內獲取和釋放,並支援以任何順序獲取和釋放多個鎖。
  • ReentrantLock具有與使用 synchronized 相同的一些基本行為和語義,但功能更強大。包括提供了一個非塊結構的獲取鎖嘗試 (tryLock())、一個獲取可中斷鎖的嘗試 (lockInterruptibly()) 和一個獲取超時失效鎖的嘗試 (tryLock(long, TimeUnit))。
  • ReentrantLock具有synchronized所沒有的許多特性,比如時間鎖等候、可中斷鎖等候、無塊結構鎖、多個條件變數或者輪詢鎖。
  • ReentrantLock可伸縮性強,應當在高度爭用的情況下使用它。

特性

  • ReentrantLock是一個可重入的互斥鎖。
  • ReentrantLock既可以是公平鎖又可以是非公平鎖。當此類的構造方法ReentrantLock(boolean fair) 接收true作為引數時,ReentrantLock就是公平鎖。

03AQS

AQS,AbstractQueuedSynchronizer的縮寫,是JUC中非常重要的一個類。javadoc中對其的介紹是:

為實現依賴於先進先出 (FIFO) 等待佇列的阻塞鎖和相關同步器(訊號量、事件,等等)提供一個框架。此類的設計目標是成為依靠單個原子 int 值來表示狀態的大多數同步器的一個有用基礎。

AbstractQueuedSynchronizer是CountDownLatch、ReentrantLock、RenntrantReadWriteLock、Semaphore等類實現的基礎。
待補充。。。

04Condition簡介

Condition地位

在任務協作中,關鍵問題是任務之間的通訊。握手可以通過Object的監視器方法(wait()和notify()/notifyAll())和synchronized方法和語句來安全地實現。Java SE5的JUC提供了具有await()和signal()/signalAll()方法的Condition和Lock來實現。其中,Lock代替了synchronized方法和語句,Condition代替了Object的監視器方法。與Object相比,Condition可以更精細地控制執行緒的休眠與喚醒。

與Object監視器監視器方法的比較

對比項 Condition Object監視器 備註
使用條件 獲取鎖 獲取鎖,建立Condition物件
等待佇列的個數 一個 多個
是否支援通知指定等待佇列 支援 不支援
是否支援當前執行緒釋放鎖進入等待狀態 支援 支援
是否支援當前執行緒釋放鎖並進入超時等待狀態 支援 支援
是否支援當前執行緒釋放鎖並進入等待狀態直到指定最後期限 支援 不支援
是否支援喚醒等待佇列中的一個任務 支援 支援
是否支援喚醒等待佇列中的全部任務 支援 支援

05ReentrantReadWriteLock

ReentrantReadWriteLock是一種共享鎖。ReadWriteLock維護了兩個鎖,讀鎖和寫鎖,所以一般稱其為讀寫鎖。寫鎖是獨佔的。讀鎖是共享的,如果沒有寫鎖,讀鎖可以由多個執行緒共享。與互斥鎖相比,雖然一次只能有一個寫執行緒可以修改共享資料,但大量讀執行緒可以同時讀取共享資料,所以,在共享資料很大,且讀操作遠多於寫操作的情況下,讀寫鎖值得一試。
ReentrantReadWriteLock具有以下特性:(有待詳細介紹)

  • 公平性
  • 重入性
  • 鎖降級
  • 鎖獲取中斷
  • 支援Condition
  • 檢測系統狀態

優點
與互斥鎖相比,雖然一次只能有一個寫執行緒可以修改共享資料,但大量讀執行緒可以同時讀取共享資料。在共享資料很大,且讀操作遠多於寫操作的情況下,ReentrantReadWriteLock值得一試。

缺點
只有當前沒有執行緒持有讀鎖或者寫鎖時才能獲取到寫鎖,這可能會導致寫執行緒發生飢餓現象,即讀執行緒太多導致寫執行緒遲遲競爭不到鎖而一直處於等待狀態。StampedLock可以解決這個問題,解決方法是如果在讀的過程中發生了寫操作,應該重新讀而不是直接阻塞寫執行緒。

06LockSupport

簡介

LockSupport是JUC鎖中比較基礎的類,用來建立鎖和其他同步類的基本執行緒阻塞原語。比如,在AQS中就使用LockSupport作為基本執行緒阻塞原語。它的park()和unpark()方法分別用於阻塞執行緒和解除阻塞執行緒。與Thread.suspend()相比,它沒有由於resume()在前發生,導致執行緒無法繼續執行的問題。和Object.wait()對比,它不需要先獲得某個物件的鎖,能夠響應中斷請求(中斷狀態被設定成true),也不會丟擲InterruptException異常。

許可

此類以及每個使用它的執行緒與一個許可關聯。如果許可可用,當前執行緒可獲取許可執行。如果許可不可用,呼叫park()後,當前執行緒阻塞,等待獲取許可。unpark(Thread thread)可使指定執行緒的許可可用。這與Semaphore相似,但LockSupport最多隻能有一個許可。

blocker

三種形式的park(park(Object blocker)、parkNanos(Object blocker, long nanos)、parkUntil(Object blocker, long deadline))都支援blocker物件引數。此物件線上程受阻塞時被記錄,以允許監視工具和診斷工具確定執行緒受阻塞的原因。監視工具和診斷工具可以使用方法getBlocker(java.lang.Thread)訪問 blocker。建議最好使用這些形式,而不是不帶此引數的原始形式。在鎖實現中提供的作為blocker的普通引數是this。

響應中斷

Object.wait()對比,LockSupport.park();不需要先獲得某個物件的鎖,能夠響應中斷請求(中斷狀態被設定成true),也不會丟擲InterruptException異常。

07讀寫鎖的升級—StampedLock

為什麼讀寫鎖需要升級

StampedLock是JDK1.8新增的一個鎖,是對讀寫鎖ReentrantReadWriteLock的改進。前面已經學習了ReentrantReadWriteLock,我們瞭解到,在共享資料很大,且讀操作遠多於寫操作的情況下,ReentrantReadWriteLock值得一試。但要注意的是,只有當前沒有執行緒持有讀鎖或者寫鎖時才能獲取到寫鎖,這可能會導致寫執行緒發生飢餓現象,即讀執行緒太多導致寫執行緒遲遲競爭不到鎖而一直處於等待狀態。StampedLock可以解決這個問題,解決方法是如果在讀的過程中發生了寫操作,應該重新讀而不是直接阻塞寫執行緒。

讀/寫模式

  • 寫。獨佔鎖,只有當前沒有執行緒持有讀鎖或者寫鎖時才能獲取到該鎖。方法writeLock()返回一個可用於unlockWrite(long)釋放鎖的方法的戳記。tryWriteLock()提供不計時和定時的版本。
  • 讀。共享鎖,如果當前沒有執行緒持有寫鎖即可獲取該鎖,可以由多個執行緒獲取到該鎖。方法readLock()返回可用於unlockRead(long)釋放鎖的方法的戳記。tryReadLock()也提供不計時和定時的版本。
  • 樂觀讀。方法tryOptimisticRead()僅當鎖定當前未處於寫入模式時,方法才會返回非零戳記。返回戳記後,需要呼叫validate(long stamp)方法驗證戳記是否可用。也就是看當呼叫tryOptimisticRead返回戳記後到到當前時間是否有其他執行緒持有了寫鎖,如果有,返回false,否則返回true,這時就可以使用該鎖了。

08CountDownLatch

CountDownLatch是一個通用同步器,用於同步一個或多個任務。在完成一組正在其他執行緒中執行的任務之前,它允許一個或多個執行緒一直等待。
可以用一個初始計數值來初始化CountDownLatch物件,任何在這個物件上呼叫await()的方法都將阻塞,直至計數值到達0。每完成一個任務,都可以在這個物件上呼叫countDown()減少計數值。當計數值減為0,所有等待的執行緒都會被釋放。CountDownLatch的計數值不能重置。如果需要重置計數器,請考慮使用CyclicBarrier。

09CyclicBarrier

CyclicBarrier允許一組執行緒互相等待,直到到達某個公共屏障點。如果你希望一組並行的任務在下個步驟之前相互等待,直到所有的任務都完成了下個步驟前的所有操作,才繼續向前執行,那麼CyclicBarrier很合適。

CyclicBarrier與CountDownLatch的比較

看過CyclicBarrier的方法列表後,有沒有發現CyclicBarrier與CountDownLatch比較像。它們之間最大的區別在於CyclicBarrier的計數器可以重置,相當於可以迴圈使用。cyclic,意為可迴圈的,barrier,意為屏障,剛好映照了CyclicBarrier的兩個特點。

10Semaphore簡介

一般的鎖在任意時刻只允許一個執行緒訪問一項資源,而計數訊號量允許n個任務同時訪問一項資源。我們可以將訊號量看做一個許可集,可以向執行緒分發使用資源的許可證。獲得資源前,執行緒呼叫acquire()從許可集中獲取許可。該執行緒結束後,通過release()將許可還給許可集。

(五)JUC容器

01概述

繼承實現關係圖

JUC提供了用於多執行緒上下文中的Collection實現與高效的、可伸縮的、執行緒安全的非阻塞FIFO佇列。參考JDK1.8,畫出下圖。
MarkdownPhotos/master/CSDNBlogs/concurrency/0201/collections.png

List

JUC容器中List的實現只有CopyOnWriteArrayList。CopyOnWriteArrayList相當於執行緒安全的ArrayList。

Set

JUC容器中Set的實現有CopyOnWriteArraySet與ConcurrentSkipListSet。CopyOnWriteArraySet相當於執行緒安全的HashSet,ConcurrentSkipListSet相當於執行緒安全的TreeSet。當set 大小通常保持很小,只讀操作遠多於可變操作,需要在遍歷期間防止執行緒間的衝突時,CopyOnWriteArraySet優於同步的HashSet。ConcurrentSkipListSet是一個基於 ConcurrentSkipListMap 的可縮放併發 NavigableSet 實現。set的元素可以根據它們的自然順序進行排序,也可以根據建立set時所提供的 Comparator 進行排序,具體取決於使用的構造方法。CopyOnWriteArraySet的實現依賴於CopyOnWriteArrayList。ConcurrentSkipListSet的實現依賴於ConcurrentSkipListMap。所以CopyOnWriteArraySet會在CopyOnWriteArrayList之後學習,ConcurrentSkipListSet會在ConcurrentSkipListMap之後學習。

Map

JUC容器中Map的實現只有ConcurrentHashMap和ConcurrentSkipListMap。
ConcurrentHashMap是執行緒安全的雜湊表,相當於執行緒安全的HashMap。ConcurrentSkipListMap是執行緒安全的有序的雜湊表,相當於執行緒安全的TreeMap。

Queue

JUC容器中Queue的常用實現有ArrayBlockingQueue、LinkedBlockingQueue、LinkedBlockingDeque、ConcurrentLinkedDeque和ConcurrentLinkedQueue。

  • ArrayBlockingQueue是一個由基於陣列的、執行緒安全的、有界阻塞佇列。
  • LinkedBlockingQueue是一個基於單向連結串列的、可指定大小的阻塞佇列。
  • LinkedBlockingDeque是一個基於單向連結串列的、可指定大小的雙端阻塞佇列。
  • ConcurrentLinkedDeque是一個基於雙向連結串列的、無界的佇列。
  • ConcurrentLinkedQueue是一個基於單向連結串列的、無界的佇列。

02CopyOnWrite

寫時複製

CopyOnWrite,簡稱COW。所謂寫時複製,即讀操作時不加鎖以保證效能不受影響,寫操作時加鎖,複製資源的一份副本,在副本上執行寫操作,寫操作完成後將資源的引用指向副本。高併發環境下,當讀操作次數遠遠大於寫操作次數時這種做法可以大大提高讀操作的效率。

寫操作時使用的鎖是ReentrantLock。

底層陣列

CopyOnWriteArrayList底層仍是陣列。為了當寫操作改變了底層陣列array時,讀操作可以得知這個訊息,需要使用volatile來保證array的可見性。

缺點

有利就有弊,寫時複製提高了讀操作的效能,但寫操作時記憶體中會同時存在資源和資源的副本,可能會佔用大量的記憶體。

03ConcurrentHashMap

鎖分段

MarkdownPhotos/master/CSDNBlogs/concurrency/0503/ConcurrentHashMapDS1.7.png
在JDK1.7中,ConcurrentHashMap通過“鎖分段”來實現執行緒安全。ConcurrentHashMap將雜湊表分成許多片段(segments),每一個片段(table)都類似於HashMap,它有一個HashEntry陣列,陣列的每項又是HashEntry組成的連結串列。每個片段都是Segment型別的,Segment繼承了ReentrantLock,所以Segment本質上是一個可重入的互斥鎖。這樣每個片段都有了一個鎖,這就是“鎖分段”。

JDK1.8結構

MarkdownPhotos/master/CSDNBlogs/concurrency/0503/ConcurrentHashMapDS1.8.png
在JDK1.8中,ConcurrentHashMap放棄了“鎖分段”,取而代之的是類似於HashMap的陣列+連結串列+紅黑樹結構,使用CAS演算法和synchronized實現執行緒安全。

ConcurrentSkipListMap

ConcurrentSkipListMap與TreeMap底層結構的不同

ConcurrentSkipListMap是執行緒安全的有序的雜湊表。與同是有序的雜湊表TreeMap相比,ConcurrentSkipListMap是執行緒安全的,TreeMap則不是,且ConcurrentSkipListMap是通過跳錶(skip list)實現的,而TreeMap是通過紅黑樹實現的。至於為什麼ConcurrentSkipListMap不像TreeMap一樣使用紅黑樹結構,在ConcurrentSkipListMap原始碼中Doug Lea已經給出解釋:

The reason is that there are no known efficient lock-free insertion and deletion algorithms for search trees.

有必要詳細瞭解下skip list。

skip list

Skip lists are a probabilistic data structure that seem likely to supplant balanced trees as the implementation method of choice for many applications. Skip list algorithms have the same asymptotic expected time bounds as balanced trees and are simpler, faster and use less space.

大意為跳過列表是一種概率資料結構,可能取代平衡樹作為許多應用程式的實現方法。跳過列表演算法具有與平衡樹相同的漸近期望時間界限,並且更簡單,更快速並且使用更少的空間。下圖是skip list的資料結構示意圖。
MarkdownPhotos/master/CSDNBlogs/concurrency/0504/ConcurrentSkipListMap.png
從圖中可以看出跳錶主要有以下幾個成員構成:

  • Node:節點,儲存map元素值。有三個屬性,key、value、指向下個node的指標next。
  • Index:索引節點。有三個屬性,指向最底層node的指標、指向下一層的index的指標down、指向本層中的下個index的指標right。
  • HeadIndex:索引頭節點。除了有Index的所有屬性外,還有一個表示此索引所在層的屬性。
  • NULL:表尾,全部為NULL。

05ArrayBlockingQueue與LinkedBlockingQueue

06LinkedBlockingDeque

LinkedBlockingDeque是一個基於連結串列的、可指定大小的阻塞雙端佇列。“雙端佇列”意味著可以操作佇列的頭尾兩端,所以LinkedBlockingDeque既支援FIFO,也支援FILO。
可選的容量範圍構造方法引數是一種防止過度膨脹的方式。如果未指定容量,那麼容量將等於 Integer.MAX_VALUE。只要插入元素不會使雙端佇列超出容量,每次插入後都將動態地建立連結節點。

07ConcurrentLinkedQueue

ConcurrentLinkedQueue是一個基於連結串列的、無界的、執行緒安全的佇列。此佇列按照FIFO原則對元素進行排序。此佇列不允許使用null元素,採用了有效的“無等待(wait-free)”演算法(CAS演算法)。
與大多數collection不同,size方法不是一個固定時間操作。由於這些佇列的非同步特性,確定當前元素的數量需要遍歷這些元素。

(六)執行緒池

01概述

前面的例子中總是需要執行緒時就建立,不需要就銷燬它。但頻繁建立和銷燬執行緒是很耗資源的,在併發量較高的情況下頻繁建立和銷燬執行緒會降低系統的效率。執行緒池可以通過重複利用已建立的執行緒降低執行緒建立和銷燬造成的消耗。
參考JDK1.8中的相關類,畫出下圖。
MarkdownPhotos/master/CSDNBlogs/concurrency/0201/threadPool.png
(此圖不是十分準確,有些類實現了兩個介面,這裡只展示出了一個)
本章只是簡單地介紹下它們,在以後的文章中會選一些最重要的來學習。

Executor

此介面提供一種將任務提交與每個任務將如何執行的機制分離開來的方法。它只提供了execute(Runnable)這麼一個方法,用於執行已提交的Runnable任務。

ExecutorService

繼承了Executor介面,用於提交一個用於執行的Runnable任務、試圖停止所有正在執行的活動任務,暫停處理正在等待的任務、執行給定的任務。

AbstractExecutorService

提供了ExecutorService的預設實現。

ThreadPoolExecutor

提供一個可擴充套件的執行緒池實現,是最出名的“執行緒池”。

ForkJoinPool

JDK1.7中新增的一個執行緒池,與ThreadPoolExecutor一樣,同樣繼承了AbstractExecutorService。ForkJoinPool是Fork/Join框架的兩大核心類之一。與其它型別的ExecutorService相比,其主要的不同在於採用了工作竊取演算法(work-stealing):所有池中執行緒會嘗試找到並執行已被提交到池中的或由其他執行緒建立的任務。這樣很少有執行緒會處於空閒狀態,非常高效。這使得能夠有效地處理以下情景:大多數由任務產生大量子任務的情況;從外部客戶端大量提交小任務到池中的情況。

ScheduledThreadPoolExecutor

ScheduledExecutorService繼承了ExecutorService,可安排在給定的延遲後執行或定期執行命令。
ScheduledThreadPoolExecutor繼承了ThreadPoolExecutor,實現了ScheduledExecutorService。

ExecutorCompletionService

CompletionService介面是將生產新的非同步任務與使用已完成任務的結果分離開來的服務。生產者利用submit()提交要執行的任務。使用者利用take()獲取並移除已完成的任務的返回值,並按照完成這些任務的順序處理它們的結果。
通常,CompletionService 依賴於一個單獨的 Executor 來實際執行任務,在這種情況下,CompletionService 只管理一個內部完成佇列。ExecutorCompletionService 類提供了此方法的一個實現。此類將那些完成時提交的任務放置在可使用take()訪問的佇列上。

Callable&Future

Callable介面類似於Runnable,兩者作用都是定義任務。不同的是,被執行緒執行後,Callable可以返回結果或丟擲異常。而Runnable不可以。Callable的返回值可以通過Future來獲取。

02ThreadPoolExecutor

為什麼要使用執行緒池

許多伺服器都面臨著處理大量客戶端遠端請求的壓力,如果每收到一個請求,就建立一個執行緒來處理,表面看是沒有問題的,但實際上存在著很嚴重的缺陷。伺服器應用程式中經常出現的情況是請求處理的任務很簡單但客戶端的數目卻是龐大的,這種情況下如果還是每收到一個請求就建立一個執行緒來處理它,伺服器在建立和銷燬執行緒所花費的時間和資源可能比處理客戶端請求處理的任務花費的時間和資源更多。為了緩解伺服器壓力,需要解決頻繁建立和銷燬執行緒的問題。執行緒池可以實現這個需求。

什麼是執行緒池

執行緒池可以看做是許多執行緒的集合。在沒有任務時執行緒處於空閒狀態,當請求到來,執行緒池給這個請求分配一個空閒的執行緒,任務完成後回到執行緒池中等待下次任務。這樣就實現了執行緒的重用。執行緒池會通過相應的排程策略和拒絕策略,對新增到執行緒池中的執行緒進行管理。

工作模型

MarkdownPhotos/master/CSDNBlogs/concurrency/0602/workModel.png
工作模型中一共有三種佇列:正在執行的任務佇列,等待被執行的阻塞佇列,等待被commit進阻塞佇列中的任務佇列。

Java中的執行緒池

Java中常用的執行緒池有三個,最出名的當然是ThreadPoolExecutor,除此之外還有ScheduledThreadPoolExecutor、ForkJoinPool。

建立ThreadPoolExecutor執行緒池

  • 強烈推薦使用Executors工廠方法建立執行緒池,如Executors.newCachedThreadPool()(無界執行緒池,可以進行自動執行緒回收)、Executors.newFixedThreadPool(int)(固定大小執行緒池)和 Executors.newSingleThreadExecutor()(單個後臺執行緒),它們均為大多數使用場景預定義了設定。
  • 通過構造方法手動配置執行緒池

構造方法

ThreadPoolExecutor一共有四個構造方法,其他三個構造方法都是通過上述的構造方法來實現的。毫無疑問手動配置執行緒池的關鍵就是學好構造方法中的幾個引數如何設定。這幾個引數對應著ThreadPoolExecutor中的幾個成員屬性。

corePoolSize與maximumPoolSize

corePoolSize與maximumPoolSize分別是核心池大小與最大池大小。在原始碼中的宣告為
private volatile int corePoolSize;private volatile int maximumPoolSize;
當新任務在方法 execute(java.lang.Runnable) 中提交時,如果執行的執行緒少於 corePoolSize,則建立新執行緒來處理請求,即使其他輔助執行緒是空閒的。如果執行的執行緒多於 corePoolSize 而少於 maximumPoolSize,則僅當佇列滿時才建立新執行緒。如果設定的 corePoolSize 和 maximumPoolSize 相同,則建立了固定大小的執行緒池。如果將 maximumPoolSize 設定為基本的無界值(如 Integer.MAX_VALUE),則允許池適應任意數量的併發任務。在大多數情況下,核心和最大池大小僅基於構造來設定,不過也可以使用 setCorePoolSize(int) 和 setMaximumPoolSize(int) 進行動態更改。

workQueue

workQueue是執行緒池工作模型中的阻塞佇列,用於傳輸和保持提交的任務。在原始碼中的宣告為private final BlockingQueue workQueue;。

keepAliveTime

keepAliveTime是池中執行緒空閒時的活動時間。如果池中當前有多於 corePoolSize 的執行緒,則這些多出的執行緒在空閒時間超過 keepAliveTime 時將會終止(參見 getKeepAliveTime(java.util.concurrent.TimeUnit))。這提供了當池處於非活動狀態時減少資源消耗的方法。如果池後來變得更為活動,則可以建立新的執行緒。也可以使用方法 setKeepAliveTime(long, java.util.concurrent.TimeUnit) 動態地更改此引數。使用 Long.MAX_VALUE TimeUnit.NANOSECONDS 的值在關閉前有效地從以前的終止狀態禁用空閒執行緒。預設情況下,保持活動策略只在有多於 corePoolSizeThreads 的執行緒時應用。但是隻要 keepAliveTime 值非 0, allowCoreThreadTimeOut(boolean) 方法也可將此超時策略應用於核心執行緒。

threadFactory

threadFactory是一個執行緒集合。執行緒池可以使用ThreadFactory建立新執行緒。如果沒有另外說明,則在同一個 ThreadGroup 中一律使用 Executors.defaultThreadFactory() 建立執行緒,並且這些執行緒具有相同的 NORM_PRIORITY 優先順序和非守護程序狀態。通過提供不同的 ThreadFactory,可以改變執行緒的名稱、執行緒組、優先順序、守護程序狀態,等等。如果從 newThread 返回 null 時 ThreadFactory 未能建立執行緒,則執行程式將繼續執行,但不能執行任何任務。

handler

handler是執行緒池拒絕策略,RejectedExecutionHandler型別的物件。當 Executor 已經關閉,並且 Executor 將有限邊界用於最大執行緒和工作佇列容量,且已經飽和時,在方法 execute(java.lang.Runnable) 中提交的新任務將被拒絕。在以上兩種情況下, execute 方法都將呼叫其 RejectedExecutionHandler 的 RejectedExecutionHandler.rejectedExecution(java.lang.Runnable, java.util.concurrent.ThreadPoolExecutor) 方法。下面提供了四種預定義的處理程式策略:

  • ThreadPoolExecutor.AbortPolicy ,預設策略,處理程式遭到拒絕將丟擲執行時 RejectedExecutionException。
  • ThreadPoolExecutor.CallerRunsPolicy,執行緒呼叫執行該任務的 execute 本身。此策略提供簡單的反饋控制機制,能夠減緩新任務的提交速度。
  • ThreadPoolExecutor.DiscardPolicy,不能執行的任務將被刪除。
  • ThreadPoolExecutor.DiscardOldestPolicy,如果執行程式尚未關閉,則位於工作佇列頭部的任務將被刪除,然後重試執行程式(如果再次失敗,則重複此過程)。

排隊策略

排隊有三種通用策略:

  • SynchronousQueue。它將任務直接傳輸給工作佇列workers,而不保持任務。如果不存在空閒執行緒,則會新建一個執行緒來執行任務。比如,在Executors.newCachedThreadPool()方法中使用的就是此策略。
  • LinkedBlockingQueue。無界佇列,使用此佇列會導致在所有corePoolSize執行緒都忙時新任務在佇列中等待。這樣,建立的執行緒就不會超過corePoolSize。比如,在Executors.newFixedThreadPool()和Executors.newSingleThreadExecutor()方法中使用的就是此策略。
  • ArrayBlockingQueue 。有界佇列,沒見到在哪裡用到了這種策略。

執行緒池狀態

原始碼已經告訴了我們執行緒池有幾個狀態。

private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
private static final int COUNT_BITS = Integer.SIZE - 3;
private static final int CAPACITY   = (1 << COUNT_BITS) - 1;

// runState is stored in the high-order bits
private static final int RUNNING    = -1 << COUNT_BITS;
private static final int SHUTDOWN   =  0 << COUNT_BITS;
private static final int STOP       =  1 << COUNT_BITS;
private static final int TIDYING    =  2 << COUNT_BITS;
private static final int TERMINATED =  3 << COUNT_BITS;

可以看出,一共有RUNNING、SHUTDOWN、STOP、TIDYING、TERMINATED五種狀態。ctl物件一共32位,高3位儲存執行緒池狀態資訊,後29位儲存執行緒池容量資訊。執行緒池的初始化狀態是RUNNING,在原始碼中體現為private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));

狀態 高三位 工作佇列workers中的任務 阻塞佇列workQueue中的任務 未新增的任務
RUNNING 111 繼續處理 繼續處理 新增
SHUTDOWN 000 繼續處理 繼續處理 不新增
STOP 001 嘗試中斷 不處理 不新增
TIDYING 010 處理完了 如果由SHUTDOWN - TIDYING ,那就是處理完了;如果由STOP - TIDYING ,那就是不處理 不新增
TERMINATED 011 同TIDYING 同TIDYING 同TIDYING

各個狀態的轉換圖如下所示
MarkdownPhotos/master/CSDNBlogs/concurrency/0602/status.png

執行任務

execute()分三種情況處理任務
case1:如果執行緒池中執行的執行緒數量<corePoolSize,則建立新執行緒來處理請求,即使其他輔助執行緒是空閒的。
case2:如果執行緒池中執行的執行緒數量>=corePoolSize,且執行緒池處於RUNNING狀態,且把提交的任務成功放入阻塞佇列中,就再次檢查執行緒池的狀態,1.如果執行緒池不是RUNNING狀態,且成功從阻塞佇列中刪除任務,則該任務由當前 RejectedExecutionHandler 處理。2.否則如果執行緒池中執行的執行緒數量為0,則通過addWorker(null, false)嘗試新建一個執行緒,新建執行緒對應的任務為null。
case3:如果以上兩種case不成立,即沒能將任務成功放入阻塞佇列中,且addWoker新建執行緒失敗,則該任務由當前 RejectedExecutionHandler 處理。

submit()方法是通過呼叫execute(Runnable)實現的。

shutdown()與shutdownNow()區別
  • 呼叫shutdown()後,執行緒池狀態立刻變為SHUTDOWN,而呼叫shutdownNow(),執行緒池狀態立刻變為STOP。
  • shutdown()通過中斷空閒執行緒、不接受新任務的方式按過去執行已提交任務的順序發起一個有序的關閉,shutdownNow()無差別地停止所有的活動執行任務,暫停等待任務的處理。也就是說,shutdown()等待任務執行完才中斷執行緒,而shutdownNow()不等任務執行完就中斷了執行緒。

Fork/Join框架

1. 什麼是Fork/Join框架

Fork/Join框架是Java7提供了的一個用於並行執行任務的框架, 是一個把大任務分割成若干個小任務,最終彙總每個小任務結果後得到大任務結果的框架。

我們再通過Fork和Join這兩個單詞來理解下Fork/Join框架,Fork就是把一個大任務切分為若干子任務並行的執行,Join就是合併這些子任務的執行結果,最後得到這個大任務的結果。比如計算1+2+。。+10000,可以分割成10個子任務,每個子任務分別對1000個數進行求和,最終彙總這10個子任務的結果。Fork/Join的執行流程圖如下:

2. 工作竊取演算法

工作竊取(work-stealing)演算法是指某個執行緒從其他佇列裡竊取任務來執行。工作竊取的執行流程圖如下:

那麼為什麼需要使用工作竊取演算法呢?假如我們需要做一個比較大的任務,我們可以把這個任務分割為若干互不依賴的子任務,為了減少執行緒間的競爭,於是把這些子任務分別放到不同的佇列裡,併為每個佇列建立一個單獨的執行緒來執行佇列裡的任務,執行緒和佇列一一對應,比如A執行緒負責處理A佇列裡的任務。但是有的執行緒會先把自己佇列裡的任務幹完,而其他執行緒對應的佇列裡還有任務等待處理。幹完活的執行緒與其等著,不如去幫其他執行緒幹活,於是它就去其他執行緒的佇列裡竊取一個任務來執行。而在這時它們會訪問同一個佇列,所以為了減少竊取任務執行緒和被竊取任務執行緒之間的競爭,通常會使用雙端佇列,被竊取任務執行緒永遠從雙端佇列的頭部拿任務執行,而竊取任務的執行緒永遠從雙端佇列的尾部拿任務執行。

工作竊取演算法的優點是充分利用執行緒進行平行計算,並減少了執行緒間的競爭,其缺點是在某些情況下還是存在競爭,比如雙端佇列裡只有一個任務時。並且消耗了更多的系統資源,比如建立多個執行緒和多個雙端佇列。

3. Fork/Join框架的介紹

我們已經很清楚Fork/Join框架的需求了,那麼我們可以思考一下,如果讓我們來設計一個Fork/Join框架,該如何設計?這個思考有助於你理解Fork/Join框架的設計。

第一步分割任務。首先我們需要有一個fork類來把大任務分割成子任務,有可能子任務還是很大,所以還需要不停的分割,直到分割出的子任務足夠小。

第二步執行任務併合並結果。分割的子任務分別放在雙端佇列裡,然後幾個啟動執行緒分別從雙端佇列裡獲取任務執行。子任務執行完的結果都統一放在一個佇列裡,啟動一個執行緒從佇列裡拿資料,然後合併這些資料。

Fork/Join使用兩個類來完成以上兩件事情:

  • ForkJoinTask:我們要使用ForkJoin框架,必須首先建立一個ForkJoin任務。它提供在任務中執行fork()和join()操作的機制,通常情況下我們不需要直接繼承ForkJoinTask類,而只需要繼承它的子類,Fork/Join框架提供了以下兩個子類:
    • RecursiveAction:用於沒有返回結果的任務。
    • RecursiveTask :用於有返回結果的任務。
  • ForkJoinPool :ForkJoinTask需要通過ForkJoinPool來執行,任務分割出的子任務會新增到當前工作執行緒所維護的雙端佇列中,進入佇列的頭部。當一個工作執行緒的佇列裡暫時沒有任務時,它會隨機從其他工作執行緒的佇列的尾部獲取一個任務。

5. Fork/Join框架的異常處理

ForkJoinTask在執行的時候可能會丟擲異常,但是我們沒辦法在主執行緒裡直接捕獲異常,所以ForkJoinTask提供了isCompletedAbnormally()方法來檢查任務是否已經丟擲異常或已經被取消了,並且可以