Java平臺(tái)類庫包含了豐富的并發(fā)基礎(chǔ)構(gòu)建模塊,例如線程安全的容器類以及各種用于協(xié)調(diào)多個(gè)相互協(xié)作的線程控制流的同步工具類( Synchronizer)。本章將介紹其中一些最有用的并發(fā)構(gòu)建模塊,特別是在Java5.0和Java6中引入的一些新模塊,以及在使用這些模塊來構(gòu)造并發(fā)應(yīng)用程序時(shí)的一些常用模式。
同步容器類
同步容器類包括 Vector和 Hashtable,二者是早期JDK的一部分,此外還包括在JDK1.2中添加的一些功能相似的類,這些同步的封裝器類是由 Collections. synchronizedxxx等工廠方法創(chuàng)建的。這些類實(shí)現(xiàn)線程安全的方式是:將它們的狀態(tài)封裝起來,并對(duì)每個(gè)公有方法都進(jìn)行同步,使得每次只有一個(gè)線程能訪問容器的狀態(tài)。
同步容器類的問題
迭代器與Concurrent-ModificationException
隱藏迭代器
并發(fā)容器
ConcurrentHashMap
為什么要使用ConcurrentHashMap
在并發(fā)編程中使用HashMap可能導(dǎo)致程序死循環(huán)。而使用線程安全的HashTable效率又非常低下,基于以上兩個(gè)原因,所以考慮使用ConcurrentHashMap。
(1)線程不安全的HashMap
在多線程環(huán)境下,使用HashMap進(jìn)行put操作會(huì)引起死循環(huán),導(dǎo)致CPU利用率接近100%,所以在并發(fā)情況下不能使用HashMap。例如,執(zhí)行以下代碼會(huì)引起死循環(huán)。
HashMap在并發(fā)執(zhí)行put操作時(shí)會(huì)引起死循環(huán),是因?yàn)槎嗑€程會(huì)導(dǎo)致HashMap的Entry鏈表形成環(huán)形數(shù)據(jù)結(jié)構(gòu),一旦形成環(huán)形數(shù)據(jù)結(jié)構(gòu),Entry的next節(jié)點(diǎn)永遠(yuǎn)不為空,就會(huì)產(chǎn)生死循環(huán)獲取Entry。
(2)效率低下的HashTable
HashTable容器使用synchronized來保證線程安全,但在線程競(jìng)爭(zhēng)激烈的情況下HashTable的效率非常低下。因?yàn)楫?dāng)一個(gè)線程訪問HashTable的同步方法,其他線程也訪問HashTable的同步方法時(shí),會(huì)進(jìn)入阻塞或輪詢狀態(tài)。如線程1使用put進(jìn)行元素添加,線程2不但不能使用put方法添加元素,也不能使用get方法來獲取元素,所以競(jìng)爭(zhēng)越激烈效率越低。
(3)ConcurrentHashMap的鎖分段技術(shù)可有效提升并發(fā)訪問率
HashTable容器在競(jìng)爭(zhēng)激烈的并發(fā)環(huán)境下表現(xiàn)出效率低下的原因是所有訪問HashTable的線程都必須競(jìng)爭(zhēng)同一把鎖,假如容器里有多把鎖,每一把鎖用于鎖容器其中一部分?jǐn)?shù)據(jù),那么當(dāng)多線程訪問容器里不同數(shù)據(jù)段的數(shù)據(jù)時(shí),線程間就不會(huì)存在鎖競(jìng)爭(zhēng),從而可以有效提高并發(fā)訪問效率,這就是ConcurrentHashMap所使用的鎖分段技術(shù)。首先將數(shù)據(jù)分成一段一段地存儲(chǔ),然后給每一段數(shù)據(jù)配一把鎖,當(dāng)一個(gè)線程占用鎖訪問其中一個(gè)段數(shù)據(jù)的時(shí)候,其他段的數(shù)據(jù)也能被其他線程訪問。
ConcurrentHashmap與其他并發(fā)容器一起增強(qiáng)了同步容器類:它們提供的迭代器不會(huì)拋出ConcurrentModificationException,因此不需要在迭代過程中對(duì)容器加鎖。 ConcurrentHashmap返回的迭代器具有弱一致性( Weakly Consistent),而并非“及時(shí)失敗”。弱一致性的迭代器可以容忍并發(fā)的修改,當(dāng)創(chuàng)建迭代器時(shí)會(huì)遍歷已有的元素,并可以(但是不保證)在迭代器被構(gòu)造后將修改操作反映給容器。
ConcurrentHashMap的結(jié)構(gòu)
通過ConcurrentHashMap的類圖來分析ConcurrentHashMap的結(jié)構(gòu),如下所示:
ConcurrentHashMap是由Segment數(shù)組結(jié)構(gòu)和HashEntry數(shù)組結(jié)構(gòu)組成。Segment是一種可重入鎖(ReentrantLock),在ConcurrentHashMap里扮演鎖的角色;HashEntry則用于存儲(chǔ)鍵值對(duì)數(shù)據(jù)。一個(gè)ConcurrentHashMap里包含一個(gè)Segment數(shù)組。Segment的結(jié)構(gòu)和HashMap類似,是一種數(shù)組和鏈表結(jié)構(gòu)。一個(gè)Segment里包含一個(gè)HashEntry數(shù)組,每個(gè)HashEntry是一個(gè)鏈表結(jié)構(gòu)的元素,每個(gè)Segment守護(hù)著一個(gè)HashEntry數(shù)組里的元素,當(dāng)對(duì)HashEntry數(shù)組的數(shù)據(jù)進(jìn)行修改時(shí),必須首先獲得與它對(duì)應(yīng)的Segment鎖,如下所示:
ConcurrentHashMap的初始化
ConcurrentHashMap初始化方法是通過initialCapacity、loadFactor和concurrencyLevel等幾個(gè)參數(shù)來初始化segment數(shù)組、段偏移量segmentShift、段掩碼segmentMask和每個(gè)segment里的HashEntry數(shù)組來實(shí)現(xiàn)的。
1.初始化segments數(shù)組
讓我們來看一下初始化segments數(shù)組的源代碼。
由上面的代碼可知,segments數(shù)組的長(zhǎng)度ssize是通過concurrencyLevel計(jì)算得出的。為了能通過按位與的散列算法來定位segments數(shù)組的索引,必須保證segments數(shù)組的長(zhǎng)度是2的N次方(power-of-two size),所以必須計(jì)算出一個(gè)大于或等于concurrencyLevel的最小的2的N次方值來作為segments數(shù)組的長(zhǎng)度。假如concurrencyLevel等于14、15或16,ssize都會(huì)等于16,即容器里鎖的個(gè)數(shù)也是16。
注意:concurrencyLevel的最大值是65535,這意味著segments數(shù)組的長(zhǎng)度最大為65536,對(duì)應(yīng)的二進(jìn)制是16位。
2.初始化segmentShift和segmentMask
這兩個(gè)全局變量需要在定位segment時(shí)的散列算法里使用,sshift等于ssize從1向左移位的次數(shù),在默認(rèn)情況下concurrencyLevel等于16,1需要向左移位移動(dòng)4次,所以sshift等于4。segmentShift用于定位參與散列運(yùn)算的位數(shù),segmentShift等于32減sshift,所以等于28,這里之所以用32是因?yàn)镃oncurrentHashMap里的hash()方法輸出的最大數(shù)是32位的,后面的測(cè)試中我們可以看到這點(diǎn)。segmentMask是散列運(yùn)算的掩碼,等于ssize減1,即15,掩碼的二進(jìn)制各個(gè)位的值都是1。因?yàn)閟size的最大長(zhǎng)度是65536,所以segmentShift最大值是16,segmentMask最大值是65535,對(duì)應(yīng)的二進(jìn)制是16位,每個(gè)位都是1。
3.初始化每個(gè)segment
輸入?yún)?shù)initialCapacity是ConcurrentHashMap的初始化容量,loadfactor是每個(gè)segment的負(fù)載因子,在構(gòu)造方法里需要通過這兩個(gè)參數(shù)來初始化數(shù)組中的每個(gè)segment。
上面代碼中的變量cap就是segment里HashEntry數(shù)組的長(zhǎng)度,它等于initialCapacity除以ssize的倍數(shù)c,如果c大于1,就會(huì)取大于等于c的2的N次方值,所以cap不是1,就是2的N次方。
segment的容量threshold=(int)cap*loadFactor,默認(rèn)情況下initialCapacity等于16,loadfactor等于0.75,通過運(yùn)算cap等于1,threshold等于零。
定位Segment
既然ConcurrentHashMap使用分段鎖Segment來保護(hù)不同段的數(shù)據(jù),那么在插入和獲取元素的時(shí)候,必須先通過散列算法定位到Segment??梢钥吹紺oncurrentHashMap會(huì)首先使用Wang/Jenkins hash的變種算法對(duì)元素的hashCode進(jìn)行一次再散列。
之所以進(jìn)行再散列,目的是減少散列沖突,使元素能夠均勻地分布在不同的Segment上,從而提高容器的存取效率。假如散列的質(zhì)量差到極點(diǎn),那么所有的元素都在一個(gè)Segment中,不僅存取元素緩慢,分段鎖也會(huì)失去意義。下面測(cè)試不通過再散列而直接執(zhí)行散列計(jì)算:
計(jì)算后輸出的散列值全是15,通過這個(gè)例子可以發(fā)現(xiàn),如果不進(jìn)行再散列,散列沖突會(huì)非常嚴(yán)重,因?yàn)橹灰臀灰粯?,無論高位是什么數(shù),其散列值總是一樣。我們?cè)侔焉厦娴亩M(jìn)制數(shù)據(jù)進(jìn)行再散列后結(jié)果如下(為了方便閱讀,不足32位的高位補(bǔ)了0,每隔4位用豎線分割下)。
可以發(fā)現(xiàn),每一位的數(shù)據(jù)都散列開了,通過這種再散列能讓數(shù)字的每一位都參加到散列運(yùn)算當(dāng)中,從而減少散列沖突。ConcurrentHashMap通過以下散列算法定位segment。
默認(rèn)情況下segmentShift為28,segmentMask為15,再散列后的數(shù)最大是32位二進(jìn)制數(shù)據(jù),向右無符號(hào)移動(dòng)28位,意思是讓高4位參與到散列運(yùn)算中,(hash>>>segmentShift)&segmentMask的運(yùn)算結(jié)果分別是4、15、7和8,可以看到散列值沒有發(fā)生沖突。
ConcurrentHashMap的操作
下面介紹ConcurrentHashMap的3種操作——get操作、put操作、size操作和額外的原子操作。
1.get操作
Segment的get操作實(shí)現(xiàn)非常簡(jiǎn)單和高效。先經(jīng)過一次再散列,然后使用這個(gè)散列值通過散列運(yùn)算定位到Segment,再通過散列算法定位到元素,代碼如下。
get操作的高效之處在于整個(gè)get過程不需要加鎖,除非讀到的值是空才會(huì)加鎖重讀。我們知道HashTable容器的get方法是需要加鎖的,那么ConcurrentHashMap的get操作是如何做到不加鎖的呢?原因是它的get方法里將要使用的共享變量都定義成volatile類型,如用于統(tǒng)計(jì)當(dāng)前Segement大小的count字段和用于存儲(chǔ)值的HashEntry的value。定義成volatile的變量,能夠在線程之間保持可見性,能夠被多線程同時(shí)讀,并且保證不會(huì)讀到過期的值,但是只能被單線程寫(有一種情況可以被多線程寫,就是寫入的值不依賴于原值),在get操作里只需要讀不需要寫共享變量count和value,所以可以不用加鎖。之所以不會(huì)讀到過期的值,是因?yàn)楦鶕?jù)Java內(nèi)存模型的happen before原則,對(duì)volatile字段的寫入操作先于讀操作,即使兩個(gè)線程同時(shí)修改和獲取volatile變量,get操作也能拿到最新的值,這是用volatile替換鎖的經(jīng)典應(yīng)用場(chǎng)景。
在定位元素的代碼里我們可以發(fā)現(xiàn),定位HashEntry和定位Segment的散列算法雖然一樣,都與數(shù)組的長(zhǎng)度減去1再相“與”,但是相“與”的值不一樣,定位Segment使用的是元素的hashcode通過再散列后得到的值的高位,而定位HashEntry直接使用的是再散列后的值。其目的是避免兩次散列后的值一樣,雖然元素在Segment里散列開了,但是卻沒有在HashEntry里散列開。
2.put操作
由于put方法里需要對(duì)共享變量進(jìn)行寫入操作,所以為了線程安全,在操作共享變量時(shí)必須加鎖。put方法首先定位到Segment,然后在Segment里進(jìn)行插入操作。插入操作需要經(jīng)歷兩個(gè)步驟,第一步判斷是否需要對(duì)Segment里的HashEntry數(shù)組進(jìn)行擴(kuò)容,第二步定位添加元素的位置,然后將其放在HashEntry數(shù)組里。
(1)是否需要擴(kuò)容
在插入元素前會(huì)先判斷Segment里的HashEntry數(shù)組是否超過容量(threshold),如果超過閾值,則對(duì)數(shù)組進(jìn)行擴(kuò)容。值得一提的是,Segment的擴(kuò)容判斷比HashMap更恰當(dāng),因?yàn)镠ashMap是在插入元素后判斷元素是否已經(jīng)到達(dá)容量的,如果到達(dá)了就進(jìn)行擴(kuò)容,但是很有可能擴(kuò)容之后沒有新元素插入,這時(shí)HashMap就進(jìn)行了一次無效的擴(kuò)容。
(2)如何擴(kuò)容
在擴(kuò)容的時(shí)候,首先會(huì)創(chuàng)建一個(gè)容量是原來容量?jī)杀兜臄?shù)組,然后將原數(shù)組里的元素進(jìn)行再散列后插入到新的數(shù)組里。為了高效,ConcurrentHashMap不會(huì)對(duì)整個(gè)容器進(jìn)行擴(kuò)容,而只對(duì)某個(gè)segment進(jìn)行擴(kuò)容。
3.size操作
如果要統(tǒng)計(jì)整個(gè)ConcurrentHashMap里元素的大小,就必須統(tǒng)計(jì)所有Segment里元素的大小后求和。Segment里的全局變量count是一個(gè)volatile變量,那么在多線程場(chǎng)景下,是不是直接把所有Segment的count相加就可以得到整個(gè)ConcurrentHashMap大小了呢?不是的,雖然相加時(shí)可以獲取每個(gè)Segment的count的最新值,但是可能累加前使用的count發(fā)生了變化,那么統(tǒng)計(jì)結(jié)果就不準(zhǔn)了。所以,最安全的做法是在統(tǒng)計(jì)size的時(shí)候把所有Segment的put、remove和clean方法全部鎖住,但是這種做法顯然非常低效。
因?yàn)樵诶奂觕ount操作過程中,之前累加過的count發(fā)生變化的幾率非常小,所以ConcurrentHashMap的做法是先嘗試2次通過不鎖住Segment的方式來統(tǒng)計(jì)各個(gè)Segment大小,如果統(tǒng)計(jì)的過程中,容器的count發(fā)生了變化,則再采用加鎖的方式來統(tǒng)計(jì)所有Segment的大小。
那么ConcurrentHashMap是如何判斷在統(tǒng)計(jì)的時(shí)候容器是否發(fā)生了變化呢?使用modCount變量,在put、remove和clean方法里操作元素前都會(huì)將變量modCount進(jìn)行加1,那么在統(tǒng)計(jì)size前后比較modCount是否發(fā)生變化,從而得知容器的大小是否發(fā)生變化。
4.額外的原子Map操作
CopyOnWriteArrayList
阻塞隊(duì)列
什么是阻塞隊(duì)列
阻塞隊(duì)列(BlockingQueue)是一個(gè)支持兩個(gè)附加操作的隊(duì)列。這兩個(gè)附加的操作支持阻塞的插入和移除方法。
1)支持阻塞的插入方法:意思是當(dāng)隊(duì)列滿時(shí),隊(duì)列會(huì)阻塞插入元素的線程,直到隊(duì)列不滿。
2)支持阻塞的移除方法:意思是在隊(duì)列為空時(shí),獲取元素的線程會(huì)等待隊(duì)列變?yōu)榉强铡?strong>阻塞隊(duì)列常用于生產(chǎn)者和消費(fèi)者的場(chǎng)景,生產(chǎn)者是向隊(duì)列里添加元素的線程,消費(fèi)者是從隊(duì)列里取元素的線程。阻塞隊(duì)列就是生產(chǎn)者用來存放元素、消費(fèi)者用來獲取元素的容器。
在阻塞隊(duì)列不可用時(shí),這兩個(gè)附加操作提供了4種處理方式,如下表所示:
- 拋出異常:當(dāng)隊(duì)列滿時(shí),如果再往隊(duì)列里插入元素,會(huì)拋出IllegalStateException("Queue
full")異常。當(dāng)隊(duì)列空時(shí),從隊(duì)列里獲取元素會(huì)拋出NoSuchElementException異常。 - 返回特殊值:當(dāng)往隊(duì)列插入元素時(shí),會(huì)返回元素是否插入成功,成功返回true。如果是移除方法,則是從隊(duì)列里取出一個(gè)元素,如果沒有則返回null。
- 一直阻塞:當(dāng)阻塞隊(duì)列滿時(shí),如果生產(chǎn)者線程往隊(duì)列里put元素,隊(duì)列會(huì)一直阻塞生產(chǎn)者線程,直到隊(duì)列可用或者響應(yīng)中斷退出。當(dāng)隊(duì)列空時(shí),如果消費(fèi)者線程從隊(duì)列里take元素,隊(duì)列會(huì)阻塞住消費(fèi)者線程,直到隊(duì)列不為空。
- 超時(shí)退出:當(dāng)阻塞隊(duì)列滿時(shí),如果生產(chǎn)者線程往隊(duì)列里插入元素,隊(duì)列會(huì)阻塞生產(chǎn)者線程一段時(shí)間,如果超過了指定的時(shí)間,生產(chǎn)者線程就會(huì)退出。
這兩個(gè)附加操作的4種處理方式不方便記憶,所以我找了一下這幾個(gè)方法的規(guī)律。put和take分別尾首含有字母t,offer和poll都含有字母o。
注意:
對(duì)于有界阻塞隊(duì)列:如果隊(duì)列為空 take() 將阻塞,直到隊(duì)列中有內(nèi)容;如果隊(duì)列為滿 put() 將阻塞,直到隊(duì)列有空閑位置;
對(duì)于無界阻塞隊(duì)列:隊(duì)列不可能會(huì)出現(xiàn)滿的情況,所以使用put或offer方法永遠(yuǎn)不會(huì)被阻塞,而且使用offer方法時(shí),該方法永遠(yuǎn)返回true。
使用 BlockingQueue 實(shí)現(xiàn)生產(chǎn)者消費(fèi)者問題
public class ProducerConsumer {
private static BlockingQueue<String> queue = new ArrayBlockingQueue<>(5);
private static class Producer extends Thread {
@Override
public void run() {
try {
queue.put("product");
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.print("produce..");
}
}
private static class Consumer extends Thread {
@Override
public void run() {
try {
String product = queue.take();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.print("consume..");
}
}
}
public static void main(String[] args) {
for (int i = 0; i < 2; i++) {
Producer producer = new Producer();
producer.start();
}
for (int i = 0; i < 5; i++) {
Consumer consumer = new Consumer();
consumer.start();
}
for (int i = 0; i < 3; i++) {
Producer producer = new Producer();
producer.start();
}
}
結(jié)果
produce..produce..consume..consume..produce..
consume..produce..consume..produce..consume..
阻塞隊(duì)列分類
JDK 7提供了7個(gè)阻塞隊(duì)列,如下:
- ArrayBlockingQueue:一個(gè)基于數(shù)組結(jié)構(gòu)的有界阻塞隊(duì)列。
- LinkedBlockingQueue:一個(gè)基于鏈表結(jié)構(gòu)的有界/無界阻塞隊(duì)列。
- PriorityBlockingQueue:一個(gè)支持優(yōu)先級(jí)排序的無界阻塞隊(duì)列。
- DelayQueue:一個(gè)支持延時(shí)獲取元素的無界阻塞隊(duì)列。
- SynchronousQueue:一個(gè)不存儲(chǔ)元素的阻塞隊(duì)列。
- LinkedTransferQueue:一個(gè)基于鏈表結(jié)構(gòu)的無界阻塞隊(duì)列。
- LinkedBlockingDeque:一個(gè)基于鏈表結(jié)構(gòu)的雙向阻塞隊(duì)列。
1.ArrayBlockingQueue
ArrayBlockingQueue是一個(gè)基于數(shù)組結(jié)構(gòu)的有界阻塞隊(duì)列。此隊(duì)列按照先進(jìn)先出(FIFO)的原則對(duì)元素進(jìn)行排序。
默認(rèn)情況下不保證線程公平的訪問隊(duì)列,所謂公平訪問隊(duì)列是指阻塞的線程,可以按照阻塞的先后順序訪問隊(duì)列,即先阻塞線程先訪問隊(duì)列。非公平性是對(duì)先等待的線程是非公平的,當(dāng)隊(duì)列可用時(shí),阻塞的線程都可以爭(zhēng)奪訪問隊(duì)列的資格,有可能先阻塞的線程最后才訪問隊(duì)列。為了保證公平性,通常會(huì)降低吞吐量。我們可以使用以下代碼創(chuàng)建一個(gè)公平的阻塞隊(duì)列:
訪問者的公平性是使用可重入鎖實(shí)現(xiàn)的,代碼如下:
2.LinkedBlockingQueue
LinkedBlockingQueue是一個(gè)基于鏈表結(jié)構(gòu)的有界/無界阻塞隊(duì)列,指定了容量就是有界阻塞隊(duì)列,未指定容量默認(rèn)為Integer.MAX_VALUE,為無界阻塞隊(duì)列。此隊(duì)列按FIFO排序元素。
3.PriorityBlockingQueue
PriorityBlockingQueue是一個(gè)支持優(yōu)先級(jí)的無界阻塞隊(duì)列。默認(rèn)情況下元素采取自然順序升序排列。也可以自定義類實(shí)現(xiàn)compareTo()方法來指定元素排序規(guī)則,或者初始化PriorityBlockingQueue時(shí),指定構(gòu)造參數(shù)Comparator來對(duì)元素進(jìn)行排序。需要注意的是不能保證同優(yōu)先級(jí)元素的順序。
4.DelayQueue
DelayQueue是一個(gè)支持延時(shí)獲取元素的無界阻塞隊(duì)列。隊(duì)列使用PriorityQueue來實(shí)現(xiàn)。隊(duì)列中的元素必須實(shí)現(xiàn)Delayed接口,在創(chuàng)建元素時(shí)可以指定多久才能從隊(duì)列中獲取當(dāng)前元素。只有在延遲期滿時(shí)才能從隊(duì)列中提取元素。
DelayQueue非常有用,可以將DelayQueue運(yùn)用在以下應(yīng)用場(chǎng)景:
- 緩存系統(tǒng)的設(shè)計(jì):可以用DelayQueue保存緩存元素的有效期,使用一個(gè)線程循環(huán)查詢DelayQueue,一旦能從DelayQueue中獲取元素時(shí),表示緩存有效期到了。
- 定時(shí)任務(wù)調(diào)度:使用DelayQueue保存當(dāng)天將會(huì)執(zhí)行的任務(wù)和執(zhí)行時(shí)間,一旦從DelayQueue中獲取到任務(wù)就開始執(zhí)行,比如TimerQueue就是使用DelayQueue實(shí)現(xiàn)的。
5.SynchronousQueue
SynchronousQueue是一個(gè)不存儲(chǔ)元素的阻塞隊(duì)列。每一個(gè)put操作必須等待一個(gè)take操作,否則不能繼續(xù)添加元素。
它支持公平訪問隊(duì)列。默認(rèn)情況下線程采用非公平性策略訪問隊(duì)列。使用以下構(gòu)造方法可以創(chuàng)建公平性訪問的SynchronousQueue,如果設(shè)置為true,則等待的線程會(huì)采用先進(jìn)先出的順序訪問隊(duì)列。
SynchronousQueue可以看成是一個(gè)傳球手,負(fù)責(zé)把生產(chǎn)者線程處理的數(shù)據(jù)直接傳遞給消費(fèi)者線程。隊(duì)列本身并不存儲(chǔ)任何元素,非常適合傳遞性場(chǎng)景。SynchronousQueue降低了將數(shù)據(jù)從生產(chǎn)者移動(dòng)到消費(fèi)者的延遲(不必出隊(duì)和入隊(duì)),它的吞吐量高于LinkedBlockingQueue和ArrayBlockingQueue。
因?yàn)镾ynchronousQueue沒有存儲(chǔ)功能,因此put和take會(huì)一直阻塞,直到由另一個(gè)線程已經(jīng)準(zhǔn)備好參與到交付過程中。僅當(dāng)有足夠多的消費(fèi)者,并且總是有一個(gè)消費(fèi)者準(zhǔn)備好獲取交付的工作時(shí),才適合使用SynchronousQueue。
6.LinkedTransferQueue
LinkedTransferQueue是一個(gè)基于鏈表結(jié)構(gòu)的無界阻塞TransferQueue隊(duì)列。相對(duì)于其他阻塞隊(duì)列,LinkedTransferQueue多了tryTransfer和transfer方法。
(1)transfer方法
如果當(dāng)前有消費(fèi)者正在等待接收元素(消費(fèi)者使用take()方法或帶時(shí)間限制的poll()方法時(shí)),transfer方法可以把生產(chǎn)者傳入的元素立刻transfer(傳輸)給消費(fèi)者。如果沒有消費(fèi)者在等待接收元素,transfer方法會(huì)將元素存放在隊(duì)列的tail節(jié)點(diǎn),并等到該元素被消費(fèi)者消費(fèi)了才返回。
(2)tryTransfer方法
tryTransfer方法是用來試探生產(chǎn)者傳入的元素是否能直接傳給消費(fèi)者。如果沒有消費(fèi)者等待接收元素,則返回false。和transfer方法的區(qū)別是tryTransfer方法無論消費(fèi)者是否接收,方法立即返回,而transfer方法是必須等到消費(fèi)者消費(fèi)了才返回。對(duì)于帶有時(shí)間限制的tryTransfer(E e,long timeout,TimeUnit unit)方法,試圖把生產(chǎn)者傳入的元素直接傳給消費(fèi)者,但是如果沒有消費(fèi)者消費(fèi)該元素則等待指定的時(shí)間再返回,如果超時(shí)還沒消費(fèi)元素,則返回false,如果在超時(shí)時(shí)間內(nèi)消費(fèi)了元素,則返回true。
7.LinkedBlockingDeque
LinkedBlockingDeque是一個(gè)基于鏈表結(jié)構(gòu)的雙向阻塞隊(duì)列。所謂雙向隊(duì)列指的是可以從隊(duì)列的兩端插入和移出元素。雙向隊(duì)列因?yàn)槎嗔艘粋€(gè)操作隊(duì)列的入口,在多線程同時(shí)入隊(duì)時(shí),也就減少了一半的競(jìng)爭(zhēng)。相比其他的阻塞隊(duì)列,LinkedBlockingDeque多了addFirst、addLast、offerFirst、offerLast、peekFirst和peekLast等方法,以First單詞結(jié)尾的方法,表示插入、獲取(peek)或移除雙端隊(duì)列的第一個(gè)元素。以Last單詞結(jié)尾的方法,表示插入、獲取或移除雙端隊(duì)列的最后一個(gè)元素。另外,插入方法add等同于addLast,移除方法remove等效于removeFirst。但是take方法卻等同于takeFirst,不知道是不是JDK的bug,使用時(shí)還是用帶有First和Last后綴的方法更清楚。
在初始化LinkedBlockingDeque時(shí)可以設(shè)置容量防止其過度膨脹。另外,雙向阻塞隊(duì)列可以運(yùn)用在“工作竊取”模式中。
阻塞方法和中斷方法
Fork/Join框架
什么是Fork/Join框架
Fork/Join框架是Java 7提供的一個(gè)用于并行執(zhí)行任務(wù)的框架,是一個(gè)把大任務(wù)分割成若干個(gè)小任務(wù),最終匯總每個(gè)小任務(wù)結(jié)果后得到大任務(wù)結(jié)果的框架。
我們?cè)偻ㄟ^Fork和Join這兩個(gè)單詞來理解一下Fork/Join框架。Fork就是把一個(gè)大任務(wù)切分為若干子任務(wù)并行的執(zhí)行,Join就是合并這些子任務(wù)的執(zhí)行結(jié)果,最后得到這個(gè)大任務(wù)的結(jié)果。比如計(jì)算1+2+…+10000,可以分割成10個(gè)子任務(wù),每個(gè)子任務(wù)分別對(duì)1000個(gè)數(shù)進(jìn)行求和,最終匯總這10個(gè)子任務(wù)的結(jié)果。
Fork/Join的運(yùn)行流程如下:
雙端隊(duì)列與工作竊取算法
Java6增加了兩種容器類型,Deque 和 BlockingDeque,它們分別對(duì)Queue和 BlockingQueue進(jìn)行了擴(kuò)展。 Deque是一個(gè)雙端隊(duì)列,實(shí)現(xiàn)了在隊(duì)列頭和隊(duì)列尾的高效插入和移除。具體實(shí)現(xiàn)包括 ArrayDeque和LinkedblockingDeque。正如阻塞隊(duì)列適用于生產(chǎn)者一消費(fèi)者模式,雙端隊(duì)列同樣適用于另一種相關(guān)模式,即工作竊取( Work Stealing)算法。
工作竊?。╳ork-stealing)算法是指某個(gè)線程從其他隊(duì)列里竊取任務(wù)來執(zhí)行。那么,為什么需要使用工作竊取算法呢?假如我們需要做一個(gè)比較大的任務(wù),可以把這個(gè)任務(wù)分割為若干互不依賴的子任務(wù),為了減少線程間的競(jìng)爭(zhēng),把這些子任務(wù)分別放到不同的隊(duì)列里,并為每個(gè)隊(duì)列創(chuàng)建一個(gè)單獨(dú)的線程來執(zhí)行隊(duì)列里的任務(wù),線程和隊(duì)列一一對(duì)應(yīng)。比如A線程負(fù)責(zé)處理A隊(duì)列里的任務(wù)。但是,有的線程會(huì)先把自己隊(duì)列里的任務(wù)干完,而其他線程對(duì)應(yīng)的隊(duì)列里還有任務(wù)等待處理。干完活的線程與其等著,不如去幫其他線程干活,于是它就去其他線程的隊(duì)列里竊取一個(gè)任務(wù)來執(zhí)行。而在這時(shí)它們會(huì)訪問同一個(gè)隊(duì)列,所以為了減少竊取任務(wù)線程和被竊取任務(wù)線程之間的競(jìng)爭(zhēng),通常會(huì)使用雙端隊(duì)列,被竊取任務(wù)線程永遠(yuǎn)從雙端隊(duì)列的頭部拿任務(wù)執(zhí)行,而竊取任務(wù)的線程永遠(yuǎn)從雙端隊(duì)列的尾部拿任務(wù)執(zhí)行。
在生產(chǎn)者一消費(fèi)者設(shè)計(jì)中,所有消費(fèi)者有一個(gè)共享的工作隊(duì)列,而在工作竊取設(shè)計(jì)中,每個(gè)消費(fèi)者都有各自的雙端隊(duì)列。如果一個(gè)消費(fèi)者完成了自己雙端隊(duì)列中的全部任務(wù),那么它可以從其他消費(fèi)者雙端隊(duì)列末尾竊取任務(wù)。工作竊取模式比傳統(tǒng)的生產(chǎn)者一消費(fèi)者模式具有更高的可伸縮性,這是因?yàn)楣ぷ髡呔€程不會(huì)在單個(gè)共享的任務(wù)隊(duì)列上發(fā)生競(jìng)爭(zhēng)。在大多數(shù)時(shí)候,它們都只是訪問自己的雙端隊(duì)列,從而極大地減少了競(jìng)爭(zhēng)。當(dāng)工作者線程需要訪問另一個(gè)隊(duì)列時(shí),它會(huì)從隊(duì)列的尾部而不是從頭部獲取工作,因此進(jìn)一步降低了隊(duì)列上的競(jìng)爭(zhēng)程度。工作竊取非常適用于既是消費(fèi)者也是生產(chǎn)者問題—當(dāng)執(zhí)行某個(gè)工作時(shí)可能導(dǎo)致出現(xiàn)更多的工作。例如,在網(wǎng)頁爬蟲程序中處理一個(gè)頁面時(shí),通常會(huì)發(fā)現(xiàn)有更多的頁面需要處理。類似的還有許多搜索圖的算法,例如在垃圾回收階段對(duì)堆進(jìn)行標(biāo)記,都可以通過工作密取機(jī)制來實(shí)現(xiàn)高效并行。當(dāng)一個(gè)工作線程找到新的任務(wù)單元時(shí),它會(huì)將其放到自己隊(duì)列的末尾(或者在工作共享設(shè)計(jì)模式中,放入其他工作者線程的隊(duì)列中)。當(dāng)雙端隊(duì)列為空時(shí),它會(huì)在另一個(gè)線程的隊(duì)列隊(duì)尾査找新的任務(wù),從而確保每個(gè)線程都保持忙碌狀態(tài)。
工作竊取運(yùn)行流程圖如下:
工作竊取算法的優(yōu)點(diǎn):充分利用線程進(jìn)行并行計(jì)算,減少了線程間的競(jìng)爭(zhēng)。
工作竊取算法的缺點(diǎn):在某些情況下還是存在競(jìng)爭(zhēng),比如雙端隊(duì)列里只有一個(gè)任務(wù)時(shí)。并且該算法會(huì)消耗了更多的系統(tǒng)資源,比如創(chuàng)建多個(gè)線程和多個(gè)雙端隊(duì)列。
Fork/Join框架的設(shè)計(jì)
我們已經(jīng)很清楚Fork/Join框架的需求了,那么可以思考一下,如果讓我們來設(shè)計(jì)一個(gè)Fork/Join框架,該如何設(shè)計(jì)?這個(gè)思考有助于你理解Fork/Join框架的設(shè)計(jì)。
步驟1 分割任務(wù)。首先我們需要有一個(gè)fork類來把大任務(wù)分割成子任務(wù),有可能子任務(wù)還是很大,所以還需要不停地分割,直到分割出的子任務(wù)足夠小。
步驟2 執(zhí)行任務(wù)并合并結(jié)果。分割的子任務(wù)分別放在雙端隊(duì)列里,然后幾個(gè)啟動(dòng)線程分別從雙端隊(duì)列里獲取任務(wù)執(zhí)行。子任務(wù)執(zhí)行完的結(jié)果都統(tǒng)一放在一個(gè)隊(duì)列里,啟動(dòng)一個(gè)線程從隊(duì)列里拿數(shù)據(jù),然后合并這些數(shù)據(jù)。
Fork/Join使用兩個(gè)類來完成以上兩件事情。
① ForkJoinTask:我們要使用ForkJoin框架,必須首先創(chuàng)建一個(gè)ForkJoin任務(wù)。它提供在任務(wù)中執(zhí)行fork()和join()操作的機(jī)制。通常情況下,我們不需要直接繼承ForkJoinTask類,只需要繼承它的子類,F(xiàn)ork/Join框架提供了以下兩個(gè)子類:
- RecursiveAction:用于沒有返回結(jié)果的任務(wù)。
- RecursiveTask:用于有返回結(jié)果的任務(wù)。
② ForkJoinPool:ForkJoinTask需要通過ForkJoinPool來執(zhí)行。
任務(wù)分割出的子任務(wù)會(huì)添加到當(dāng)前工作線程所維護(hù)的雙端隊(duì)列中,進(jìn)入隊(duì)列的頭部。當(dāng)一個(gè)工作線程的隊(duì)列里暫時(shí)沒有任務(wù)時(shí),它會(huì)隨機(jī)從其他工作線程的隊(duì)列的尾部獲取一個(gè)任務(wù)。
使用Fork/Join框架
讓我們通過一個(gè)簡(jiǎn)單的需求來使用Fork/Join框架,需求是:計(jì)算1+2+3+4的結(jié)果。
使用Fork/Join框架首先要考慮到的是如何分割任務(wù),如果希望每個(gè)子任務(wù)最多執(zhí)行兩個(gè)數(shù)的相加,那么我們?cè)O(shè)置分割的閾值是2,由于是4個(gè)數(shù)字相加,所以Fork/Join框架會(huì)把這個(gè)任務(wù)fork成兩個(gè)子任務(wù),子任務(wù)一負(fù)責(zé)計(jì)算1+2,子任務(wù)二負(fù)責(zé)計(jì)算3+4,然后再join兩個(gè)子任務(wù)的結(jié)果。因?yàn)槭怯薪Y(jié)果的任務(wù),所以必須繼承RecursiveTask,實(shí)現(xiàn)代碼如下:
通過這個(gè)例子,我們進(jìn)一步了解ForkJoinTask,F(xiàn)orkJoinTask與一般任務(wù)的主要區(qū)別在于它需要實(shí)現(xiàn)compute方法,在這個(gè)方法里,首先需要判斷任務(wù)是否足夠小,如果足夠小就直接執(zhí)行任務(wù)。如果不足夠小,就必須分割成兩個(gè)子任務(wù),每個(gè)子任務(wù)在調(diào)用fork方法時(shí),又會(huì)進(jìn)入compute方法,看看當(dāng)前子任務(wù)是否需要繼續(xù)分割成子任務(wù),如果不需要繼續(xù)分割,則執(zhí)行當(dāng)前子任務(wù)并返回結(jié)果。使用join方法會(huì)等待子任務(wù)執(zhí)行完并得到其結(jié)果。
ForkJoin 使用 ForkJoinPool 來啟動(dòng),它是一個(gè)特殊的線程池,線程數(shù)量取決于 CPU 核數(shù)。
public class ForkJoinPool extends AbstractExecutorService
ForkJoinPool 實(shí)現(xiàn)了工作竊取算法來提高 CPU 的利用率。每個(gè)線程都維護(hù)了一個(gè)雙端隊(duì)列,用來存儲(chǔ)需要執(zhí)行的任務(wù)。工作竊取算法允許空閑的線程從其它線程的雙端隊(duì)列中竊取一個(gè)任務(wù)來執(zhí)行。竊取的任務(wù)必須是最晚的任務(wù),避免和隊(duì)列所屬線程發(fā)生競(jìng)爭(zhēng)。例如下圖中,Thread2 從 Thread1 的隊(duì)列中拿出最晚的 Task1 任務(wù),Thread1 會(huì)拿出 Task2 來執(zhí)行,這樣就避免發(fā)生競(jìng)爭(zhēng)。但是如果隊(duì)列中只有一個(gè)任務(wù)時(shí)還是會(huì)發(fā)生競(jìng)爭(zhēng)。
Fork/Join框架的異常處理
ForkJoinTask在執(zhí)行的時(shí)候可能會(huì)拋出異常,但是我們沒辦法在主線程里直接捕獲異常,所以ForkJoinTask提供了isCompletedAbnormally()方法來檢查任務(wù)是否已經(jīng)拋出異?;蛞呀?jīng)被取消了,并且可以通過ForkJoinTask的getException方法獲取異常。使用如下代碼:
getException方法返回Throwable對(duì)象,如果任務(wù)被取消了則返回CancellationException。如果任務(wù)沒有完成或者沒有拋出異常則返回null。