并發(fā)編程06--Java并發(fā)容器和框架

ConcurrentHashMap的實(shí)現(xiàn)原理與使用

ConcurrentHashMap是線程安全且高效的HashMap。

為什么要使用ConcurrentHashMap

在并發(fā)編程中使用HashMap可能導(dǎo)致程序死循環(huán)。而使用線程安全的HashTable效率又非常低下
1.線程不安全的HashMap
在并發(fā)情況下,使用HashMap進(jìn)行行put操作會(huì)引起死循環(huán),導(dǎo)致CPU利用率接近100%.

死循環(huán)結(jié)構(gòu)

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的結(jié)構(gòu)

ConcurrentHashMap的類圖

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的結(jié)構(gòu)圖

ConcurrentHashMap的初始化

ConcurrentHashMap初始化方法是通過initialCapacity、loadFactor和concurrencyLevel等幾個(gè)參數(shù)來初始化segment數(shù)組、段偏移量segmentShift、段掩碼segmentMask和每個(gè)segment里的HashEntry數(shù)組來實(shí)現(xiàn)的。

初始化segements數(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位。

初始化segmentShift和segmentMask

這兩個(gè)全局變量需要在定位segment時(shí)的散列算法里使用
sshift = ssize從1向左移位的次數(shù)

在默認(rèn)情況下concurrencyLevel等于16,1需要向左移位移動(dòng)4次,所以sshift等于4。

segmentShift(用于定位參與散列運(yùn)算的位數(shù)) = 32減sshift,所以默認(rèn)情況等于28.

這里之所以用32是因?yàn)闉镃oncurrentHashMap里的hash()方法輸出的最大數(shù)是32位的.

segmentMask(散列運(yùn)算的掩碼) = ssize減1,即默認(rèn)情況下為15,掩碼的二進(jìn)制各個(gè)位的值都是1。

因?yàn)閟size的最大長(zhǎng)度是65536,所以segmentShift最大值是16,segmentMask最大值是65535,對(duì)應(yīng)的二進(jìn)制是16位,每個(gè)位都是1。

初始化每個(gè)segment

輸入?yún)?shù)initialCapacityConcurrentHashMap的初始化容量,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

在插入和獲取元素的時(shí)候,必須先通過散列算法定位到Segment。

在插入和獲取元素的時(shí)候,必須先通過散列算法定位到Segment。


之所以進(jìn)行再散列,目的是減少散列沖突,使元素能夠均勻地分布在不同的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的操作

get操作

Segment的get操作實(shí)現(xiàn)非常簡(jiǎn)單和高效。先經(jīng)過一次再散列,然后使用這個(gè)散列值通過散列運(yùn)算定位到Segment,再通過散列算法定位到元素.


get操作的高效之處在于整個(gè)get過程不需要加鎖,除非讀到的值是空才會(huì)加鎖重讀。

HashTable容器的get方法是需要加鎖的,那么ConcurrentHashMap的get操作是如何做到不加鎖的呢?

  • ConcurrentHashMap的get方法里將要使用的共享變量都定義成volatile類型,如用于統(tǒng)計(jì)當(dāng)前
    Segement大小的count字段和用于存儲(chǔ)值的HashEntry的value。定義成volatile的變量,能夠在線
    程之間保持可見性,能夠被多線程同時(shí)讀,并且保證不會(huì)讀到過期的值,但是只能被單線程寫
    (有一種情況可以被多線程寫,就是寫入的值不依賴于原值),在get操作里只需要讀不需要寫
    共享變量count和value,所以可以不用加鎖。

定位HashEntry和定位Segment的散列算法雖然一樣,都與數(shù)組的長(zhǎng)度減去1再相“與”,但是相“與”的值不一樣,定位Segment使用的是元素的hashcode通過再散列后得到的值的高位,而定位HashEntry直接使用的是再散列后的值。其目的是避免兩次散列后的值一樣,雖然元素在Segment里散列開了,但是卻沒有在HashEntry里散列開。

put操作

由于put方法里需要對(duì)共享變量進(jìn)行寫入操作,所以為了線程安全,在操作共享變量時(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ò)容。

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方法全部鎖住,但是這種做法顯然非常低效。

在累加count操作過程中,之前累加過的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ā)生變化。

ConcurrentLinkedQueue

如果要實(shí)現(xiàn)一個(gè)線程安全的隊(duì)列有兩種方式:

  • 使用阻塞算法
  • 使用非阻塞算法

阻塞算法:可以用一個(gè)鎖(入隊(duì)和出隊(duì)用同一把鎖)或兩個(gè)鎖(入隊(duì)和出隊(duì)用不同的鎖)等方式來實(shí)現(xiàn)。
非阻塞算法:可以使用循環(huán)CAS的方式來實(shí)現(xiàn)。

ConcurrentLinkedQueue是一個(gè)基于鏈接節(jié)點(diǎn)的無界線程安全隊(duì)列,它采用先進(jìn)先出的規(guī)則對(duì)節(jié)點(diǎn)進(jìn)行排序,當(dāng)我們添加一個(gè)元素的時(shí)候,它會(huì)添加到隊(duì)列的尾部;當(dāng)我們獲取一個(gè)元素時(shí),它會(huì)返回隊(duì)列頭部的元素。它采用了“wait-free”算法(即CAS算法)來實(shí)現(xiàn),該算法在Michael&Scott算法上進(jìn)行了一些修改。

ConcurrentLinkedQueue的結(jié)構(gòu)

ConcurrentLinkedQueue的類圖

ConcurrentLinkedQueue由head節(jié)點(diǎn)和tail節(jié)點(diǎn)組成,每個(gè)節(jié)點(diǎn)(Node)由節(jié)點(diǎn)元素(item)和指向下一個(gè)節(jié)點(diǎn)(next)的引用組成,節(jié)點(diǎn)與節(jié)點(diǎn)之間就是通過這個(gè)next關(guān)聯(lián)起來,從而組成一
張鏈表結(jié)構(gòu)的隊(duì)列。默認(rèn)情況下head節(jié)點(diǎn)存儲(chǔ)的元素為空,tail節(jié)點(diǎn)等于head節(jié)點(diǎn)。
private transient volatile Node<E> tail = head;

入隊(duì)列

入隊(duì)列的過程

入隊(duì)列就是將入隊(duì)節(jié)點(diǎn)添加到隊(duì)列的尾部。

隊(duì)列添加元素的快照?qǐng)D

入隊(duì)主要做兩件事情:

  • 第一是將入隊(duì)節(jié)點(diǎn)設(shè)置成當(dāng)前隊(duì)列尾節(jié)點(diǎn)的下一個(gè)節(jié)點(diǎn);
  • 第二是更新tail節(jié)點(diǎn),如果tail節(jié)點(diǎn)的next節(jié)點(diǎn)不為空,則將入隊(duì)節(jié)點(diǎn)設(shè)置成tail節(jié)點(diǎn),如果tail節(jié)點(diǎn)的next節(jié)點(diǎn)為空,則將入隊(duì)節(jié)點(diǎn)設(shè)置成tail的next節(jié)點(diǎn),所以tail節(jié)點(diǎn)不總是尾節(jié)點(diǎn).
    CAS算法入隊(duì)操作

    整個(gè)入隊(duì)過程主要做兩件事情:第一是定位出尾節(jié)點(diǎn);第二是使用CAS算法將入隊(duì)節(jié)點(diǎn)設(shè)置成尾節(jié)點(diǎn)的next節(jié)點(diǎn),如不成功則重試。

定位尾節(jié)點(diǎn)

tail節(jié)點(diǎn)并不總是尾節(jié)點(diǎn),所以每次入隊(duì)都必須先通過tail節(jié)點(diǎn)來找到尾節(jié)點(diǎn)。

尾節(jié)點(diǎn)可能是tail節(jié)點(diǎn),也可能是tail節(jié)點(diǎn)的next節(jié)點(diǎn)。
代碼中循環(huán)體中的第一個(gè)if就是判斷tail是否有next節(jié)點(diǎn),有則表示next節(jié)點(diǎn)可能是尾節(jié)點(diǎn)。獲取tail節(jié)點(diǎn)的next節(jié)點(diǎn)需要注意的是p節(jié)點(diǎn)等于p的next節(jié)點(diǎn)的情況,只有一種可能就是p節(jié)點(diǎn)和p的next節(jié)點(diǎn)都等于空,表示這個(gè)隊(duì)列剛初始化,正準(zhǔn)備添加節(jié)點(diǎn),所以需要返回head節(jié)點(diǎn)。

設(shè)置入隊(duì)節(jié)點(diǎn)為尾節(jié)點(diǎn)

p.casNext(null,n)方法用于將入隊(duì)節(jié)點(diǎn)設(shè)置為當(dāng)前隊(duì)列尾節(jié)點(diǎn)的next節(jié)點(diǎn),如果p是null,表示p是當(dāng)前隊(duì)列的尾節(jié)點(diǎn),如果不為null,表示有其他線程更新了尾節(jié)點(diǎn),則需要重新獲取當(dāng)前隊(duì)列的尾節(jié)點(diǎn)。

HOPS的設(shè)計(jì)意圖

讓tail節(jié)點(diǎn)永遠(yuǎn)作為隊(duì)列的尾節(jié)點(diǎn),這樣實(shí)現(xiàn)代碼量非常少,而且邏輯清晰和易懂。但是,這么做有個(gè)缺點(diǎn),每次都需要使用循環(huán)CAS更新tail節(jié)點(diǎn)。

doug lea使用hops變量來控制并減少tail節(jié)點(diǎn)的更新頻率,并不是每次節(jié)點(diǎn)入隊(duì)后都將tail節(jié)點(diǎn)更新成尾節(jié)點(diǎn),而是當(dāng)tail節(jié)點(diǎn)和尾節(jié)點(diǎn)的距離大于等于常量HOPS的值(默認(rèn)等于1)時(shí)才更新tail節(jié)點(diǎn),tail和尾節(jié)點(diǎn)的距離越長(zhǎng),使用CAS更新tail節(jié)點(diǎn)的次數(shù)就會(huì)越少,但是距離越長(zhǎng)帶來的負(fù)面效果就是每次入隊(duì)時(shí)定位尾節(jié)點(diǎn)的時(shí)間就越長(zhǎng),因?yàn)檠h(huán)體需要多循環(huán)一次來定位出尾節(jié)點(diǎn),但是這樣仍然能提高入隊(duì)的效率.

從本質(zhì)上來看它通過增加對(duì)volatile變量的讀操作來減少對(duì)volatile變量的寫操作,而對(duì)volatile變量的寫操作開銷要遠(yuǎn)遠(yuǎn)大于讀操作,所以入隊(duì)效率會(huì)有所提升。

注意:入隊(duì)方法永遠(yuǎn)返回true,所以不要通過返回值判斷入隊(duì)是否成功。

出隊(duì)列

出隊(duì)列的就是從隊(duì)列里返回一個(gè)節(jié)點(diǎn)元素,并清空該節(jié)點(diǎn)對(duì)元素的引用。


隊(duì)列出節(jié)點(diǎn)快照?qǐng)D
LinkedListQueue出隊(duì)代碼實(shí)現(xiàn)

首先獲取頭結(jié)點(diǎn),然后判斷頭結(jié)點(diǎn)元素是否為空,如果為空,表示另外一個(gè)線程已經(jīng)進(jìn)行了一次出隊(duì)操作并將該節(jié)點(diǎn)的元素取走.如果不為空,則使用CAS的方式將頭節(jié)點(diǎn)的引用設(shè)置成null,如果CAS成功,則直接返回頭節(jié)點(diǎn)的元素,如果不成功,表示另外一個(gè)線程已經(jīng)進(jìn)行了一次出隊(duì)操作更新了head節(jié)點(diǎn),導(dǎo)致元素發(fā)生了變化,需要重新獲取頭節(jié)點(diǎn)。


Java中的阻塞隊(duì)列

什么是阻塞隊(duì)列

阻塞隊(duì)列(BlockingQueue):一個(gè)支持兩個(gè)附加操作的隊(duì)列。這兩個(gè)附加的操作支持阻塞的插入和移除方法。

  • 支持阻塞的插入方法:當(dāng)隊(duì)列為滿時(shí),隊(duì)列會(huì)阻塞插入元素的線程,知道隊(duì)列不滿.
  • 支持阻塞的溢出方法:當(dāng)隊(duì)列為空時(shí),獲取元素的線程會(huì)等待隊(duì)列變?yōu)榉强?

阻塞隊(duì)列常用于生產(chǎn)者和消費(fèi)者的場(chǎng)景,生產(chǎn)者是向隊(duì)列里添加元素的線程,消費(fèi)者是向隊(duì)列里取元素的線程.阻塞隊(duì)列就是生產(chǎn)者用來存放元素的,消費(fèi)者用來獲取元素的容器.

插入和移除操作的4中處理方式
  • 拋出異常:當(dāng)隊(duì)列滿時(shí),如果再往隊(duì)列里插入元素,會(huì)拋出IllegalStateException("Queuefull")異常。當(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ì)退出。

注意:如果是無界阻塞隊(duì)列,隊(duì)列不可能會(huì)出現(xiàn)滿的情況,所以使用put或offer方法永遠(yuǎn)不會(huì)被阻塞,而且使用offer方法時(shí),該方法永遠(yuǎn)返回true。

Java中的阻塞隊(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è)使用優(yōu)先級(jí)隊(duì)列實(shí)現(xiàn)的無界阻塞隊(duì)列。
·SynchronousQueue:一個(gè)不存儲(chǔ)元素的阻塞隊(duì)列。
·LinkedTransferQueue:一個(gè)由鏈表結(jié)構(gòu)組成的無界阻塞隊(duì)列。
·LinkedBlockingDeque:一個(gè)由鏈表結(jié)構(gòu)組成的雙向阻塞隊(duì)列。


1.ArrayBlockingQueue

ArrayBlockingQueue是一個(gè)用數(shù)組實(shí)現(xiàn)的有界阻塞隊(duì)列。此隊(duì)列按照先進(jìn)先出(FIFO)的原則對(duì)元素進(jìn)行排序。

默認(rèn)情況下不保證線程公平的訪問隊(duì)列.為了保證公平性,通常會(huì)使得吞吐量降低.

創(chuàng)建線程公平隊(duì)列代碼示例:

ArrayBlockingQueue fairQueue = new ArrayBlockingQueue(1000,true);

訪問者的公平性是使用可重入鎖實(shí)現(xiàn)的,代碼如下。

public ArrayBlockingQueue(int capacity, boolean fair) {
if (capacity <= 0)
throw new IllegalArgumentException();
this.items = new Object[capacity];
lock = new ReentrantLock(fair);
notEmpty = lock.newCondition();
notFull = lock.newCondition();
}

2.LinkedBlockingQueue

LinkedBlockingQueue是一個(gè)用鏈表實(shí)現(xiàn)的有界阻塞隊(duì)列。此隊(duì)列的默認(rèn)和最大長(zhǎng)度為Integer.MAX_VALUE。此隊(duì)列按照先進(jìn)先出的原則對(duì)元素進(jìn)行排序。

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的應(yīng)用場(chǎng)景

  • 緩存系統(tǒng)的設(shè)計(jì):可以用DelayQueue保存緩存元素的有效期,使用一個(gè)線程循環(huán)查詢DelayQueue,一旦能從DelayQueue中獲取元素時(shí),表示緩存有效期到了。

利用存在性來達(dá)到驗(yàn)證是否過期

  • 定時(shí)任務(wù)調(diào)度:使用DelayQueue保存當(dāng)天將會(huì)執(zhí)行的任務(wù)和執(zhí)行時(shí)間,一旦從DelayQueue中獲取到任務(wù)就開始執(zhí)行,比如TimerQueue就是使用DelayQueue實(shí)現(xiàn)的。
(1)如何實(shí)現(xiàn)Delayed接口

DelayQueue隊(duì)列的元素必須實(shí)現(xiàn)Delayed接口。
1.在對(duì)象創(chuàng)建的時(shí)候,初始化基本數(shù)據(jù)。(數(shù)據(jù),以及過期時(shí)間)
2.實(shí)現(xiàn)getDelay方法,該方法返回當(dāng)前元素還需要延時(shí)多長(zhǎng)時(shí)間,單位是納秒.
3.實(shí)現(xiàn)compareTo方法來指定元素的順序。

例如,讓延時(shí)時(shí)間最長(zhǎng)的放在隊(duì)列的末尾。

(2)如何實(shí)現(xiàn)延時(shí)阻塞隊(duì)列

當(dāng)消費(fèi)者從隊(duì)列里獲取元素時(shí),如果元素沒有達(dá)到延時(shí)時(shí)間,就阻塞當(dāng)前線程。

5.SynchronousQueue

SynchronousQueue是一個(gè)不存儲(chǔ)元素的阻塞隊(duì)列
每一個(gè)put操作必須等待一個(gè)take操作,否則不能繼續(xù)添加元素。
支持公平訪問.默認(rèn)情況下采用非公平性策略訪問隊(duì)列.

SynchronousQueue可以看成是一個(gè)傳球手,負(fù)責(zé)把生產(chǎn)者線程處理的數(shù)據(jù)直接傳遞給消費(fèi)者線程。隊(duì)列本身并不存儲(chǔ)任何元素,非常適合傳遞性場(chǎng)景。SynchronousQueue的吞吐量高于LinkedBlockingQueue和ArrayBlockingQueue。

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)了才返回。

transfer方法的關(guān)鍵代碼

第一行代碼是試圖把存放當(dāng)前元素的s節(jié)點(diǎn)作為tail節(jié)點(diǎn)。第二行代碼是讓CPU自旋等待消費(fèi)者消費(fèi)元素。因?yàn)樽孕龝?huì)消耗CPU,所以自旋一定的次數(shù)后使用Thread.yield()方法來暫停當(dāng)前正在執(zhí)行的線程,并執(zhí)行其他線程。

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)。

在初始化LinkedBlockingDeque時(shí)可以設(shè)置容量防止其過度膨脹。另外,雙向阻塞隊(duì)列可以運(yùn)用在“工作竊取”模式中。

阻塞隊(duì)列的實(shí)現(xiàn)原理

使用通知模式實(shí)現(xiàn):
所謂通知模式,就是當(dāng)生產(chǎn)者往滿的隊(duì)列中添加元素時(shí)會(huì)阻塞生產(chǎn)者,當(dāng)消費(fèi)者消費(fèi)了一個(gè)隊(duì)列中的元素后,會(huì)通知生產(chǎn)者當(dāng)前隊(duì)列可用.


現(xiàn)ArrayBlockingQueue使用了Condition來實(shí)現(xiàn)通知模式.

當(dāng)往隊(duì)列里插入一個(gè)元素時(shí),如果隊(duì)列不可用,那么阻塞生產(chǎn)者主要通過LockSupport.park(this)來實(shí)現(xiàn)。
![](https://upload-images.jianshu.io/upload_images/15454479-428bed08257e38e0.png?imageMogr2/auto-

2019-09-20 19-26-40屏幕截圖.png

orient/strip%7CimageView2/2/w/1240)

繼續(xù)進(jìn)入源碼,發(fā)現(xiàn)調(diào)用setBlocker先保存一下將要阻塞的線程,然后調(diào)用unsafe.park阻塞當(dāng)前線程。




unsafe.park()

park這個(gè)方法會(huì)阻塞當(dāng)前線程,只有以下4種情況中的一種發(fā)生時(shí),該方法才會(huì)返回。

  • 與park對(duì)應(yīng)的unpark執(zhí)行或已經(jīng)執(zhí)行時(shí)。“已經(jīng)執(zhí)行”是指unpark先執(zhí)行,然后再執(zhí)行park的情況
  • 線程被中斷時(shí)。
  • 等待完time參數(shù)指定的毫秒數(shù)時(shí)。
  • 異常現(xiàn)象發(fā)生時(shí),這個(gè)異常現(xiàn)象沒有任何原因。
linux環(huán)境下park()方法的具體實(shí)現(xiàn)--pthread_cond_wait

pthread_cond_wait是一個(gè)多線程的條件變量函數(shù),cond是condition的縮寫,字面意思可以理解為線程在等待一個(gè)條件發(fā)生,這個(gè)條件是一個(gè)全局變量。這個(gè)方法接收兩個(gè)參數(shù):一個(gè)共享變量_cond,一個(gè)互斥量_mutex。


Fork/Join框架

什么是Fork/Join框架

Fork/Join框架是Java 7提供的一個(gè)用于并行執(zhí)行任務(wù)的框架,是一個(gè)把大任務(wù)分割成若干個(gè)小任務(wù),最終匯總每個(gè)小任務(wù)結(jié)果后得到大任務(wù)結(jié)果的框架。


Fork Join的運(yùn)行流程圖

工作竊取算法

工作竊取(work-stealing)算法是指某個(gè)線程從其他隊(duì)列里竊取任務(wù)來執(zhí)行。

核心思想:將一個(gè)大任務(wù)活粉成為多個(gè)小任務(wù),把這些子任務(wù)分別放到不同的隊(duì)列里,并為每個(gè)隊(duì)列創(chuàng)建一個(gè)單獨(dú)的線程來執(zhí)行隊(duì)列里的任務(wù).當(dāng)某個(gè)線程完成自己隊(duì)列中的任務(wù)時(shí),就去其他線程的隊(duì)列里竊取一個(gè)任務(wù)來執(zhí)行。

為了減少竊取任務(wù)線程和被竊取任務(wù)線程之間的競(jìng)爭(zhēng),通常會(huì)使用雙端隊(duì)列,被竊取任務(wù)線程永遠(yuǎn)從雙端隊(duì)列的頭部拿任務(wù)執(zhí)行,而竊取任務(wù)的線程永遠(yuǎn)從雙端隊(duì)列的尾部拿任務(wù)執(zhí)行。

工作竊取運(yùn)行流程圖

工作竊取(work-stealing)算法

  • 優(yōu)點(diǎn)是充分利用線程進(jìn)行并行計(jì)算,并減少了線程間的競(jìng)爭(zhēng)
  • 其缺點(diǎn)是在某些情況下還是存在競(jìng)爭(zhēng),比如雙端隊(duì)列里只有一個(gè)任務(wù)時(shí),并且消耗了更多的系統(tǒng)資源,比如創(chuàng)建多個(gè)線程和多個(gè)雙端隊(duì)列。

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è)簡(jiǎn)單的需求來使用Fork/Join框架,需求是:計(jì)算1+2+3+4的結(jié)果。

使用Fork/Join框架首先考慮到的就是如何分割任務(wù),如果希望每個(gè)子任務(wù)最多執(zhí)行兩個(gè)數(shù)的相加,那么我們將分割的閥值設(shè)為2.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)代碼如下。

import java.util.concurrent.ExecutionException;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.Future;
import java.util.concurrent.RecursiveTask;

public class CountTask extends RecursiveTask<Integer> {

    //閥值
    private static final int THRESHOLD = 2;
    private int start;
    private int end;

    public CountTask(int start, int end) {
        this.start = start;
        this.end = end;
    }

    @Override
    protected Integer compute() {
        int sum = 0;
        //如果任務(wù)足夠小就計(jì)算任務(wù)
        boolean canCompute = (end - start) <= THRESHOLD;
        if (canCompute) {
            for (int i = start; i <= end; i++ ) {
                sum += i;
            }
        }else {
            // 如果任務(wù)大于閾值,就分裂成兩個(gè)子任務(wù)計(jì)算
            int middle = (start + end) / 2;
            CountTask leftTask = new CountTask(start, middle);
            CountTask rightTask = new CountTask(middle + 1, end);
            // 執(zhí)行子任務(wù)
            leftTask.fork();
            rightTask.fork();
            // 等待子任務(wù)執(zhí)行完,并得到其結(jié)果
            int leftResult=leftTask.join();
            int rightResult=rightTask.join();
            // 合并子任務(wù)
            sum = leftResult + rightResult;
        }
        return sum;
    }

    public static void main(String[] args) {
        ForkJoinPool forkJoinPool = new ForkJoinPool();
        // 生成一個(gè)計(jì)算任務(wù),負(fù)責(zé)計(jì)算1+2+3+4
        CountTask task = new CountTask(1, 4);
        // 執(zhí)行一個(gè)任務(wù)
        Future<Integer> result = forkJoinPool.submit(task);
        try {
            System.out.println(result.get());
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
    }
}

ForkJoinTask與一般任務(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é)果。

Fork/Join框架的異常處理

ForkJoinTask在執(zhí)行任務(wù)可能會(huì)拋出異常,到那時(shí)無法直接在主線程中捕獲異常,所以ForkJoinTask提供了isCompletedAbnormally()方法來檢查任務(wù)是否已經(jīng)拋出異常或已經(jīng)被取消了,并且可以通過ForkJoinTask的getException方法獲取異常。


getException方法返回Throwable對(duì)象,如果任務(wù)被取消了則返回CancellationException。如果任務(wù)沒有完成或者沒有拋出異常則返回null。

Fork/Join框架的實(shí)現(xiàn)原理

ForkJoinPool = ForkJoinTask數(shù)組 + ForkJoinWorkerThread數(shù)組;

  • ForkJoinTask數(shù)組:負(fù)責(zé)將程序提交給ForkJoinPool的任務(wù);
  • ForkJoinWorkerThread:負(fù)責(zé)執(zhí)行ForkJoinTask提交的任務(wù)。

(1)ForkJoinTask的fork方法實(shí)現(xiàn)原理
當(dāng)我們調(diào)用ForkJoinTask的fork方法時(shí),程序會(huì)調(diào)用ForkJoinWorkerThread的pushTask方法異步執(zhí)行這個(gè)任務(wù).

ForkJoinTask的fork方法

pushTask方法把當(dāng)前任務(wù)存放在ForkJoinTask數(shù)組隊(duì)列里。然后再調(diào)用ForkJoinPool的signalWork()方法喚醒或創(chuàng)建一個(gè)工作線程來執(zhí)行任務(wù)。
pushTask方法

(2)ForkJoinTask的join方法實(shí)現(xiàn)原理
Join方法的主要作用是阻塞當(dāng)前線程并等待獲取結(jié)果。

ForkJoinTask的join

首先先調(diào)用doJoin(),通過doJoin()方法得到當(dāng)前任務(wù)的狀態(tài)來判斷返回什么結(jié)果,任務(wù)狀態(tài)有四種:

  • 已完成(NORMAL):直接返回任務(wù)結(jié)果。
  • 被取消(CANCELLED):直接拋出CancellationException。
  • 信號(hào)(SIGNAL)
  • 出現(xiàn)異常(EXCEPTIONAL):直接拋出對(duì)應(yīng)的異常。
doJoin()方法

在doJoin()方法里,首先通過查看任務(wù)的狀態(tài),看任務(wù)是否已經(jīng)執(zhí)行完成,如果執(zhí)行完成,則直接返回任務(wù)狀態(tài);如果沒有執(zhí)行完,則從任務(wù)數(shù)組里取出任務(wù)并執(zhí)行。如果任務(wù)順利執(zhí)行完成,則設(shè)置任務(wù)狀態(tài)為NORMAL,如果出現(xiàn)異常,則記錄異常,并將任務(wù)狀態(tài)設(shè)置為EXCEPTIONAL。

參考書籍:《Java并發(fā)編程的藝術(shù)》

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容