上一篇介紹了ArrayBlockingQueue的源碼,這節(jié)我們介紹它的兄弟,基于鏈表的實(shí)現(xiàn),直接開看
head見名知意:這里必須要說的是一個好的名字,真的能夠給閱讀者帶來不一樣的便利。我讀過的源碼,是深有體會的。也就是我們鏈表的頭
tail同樣我們可以知道是尾巴節(jié)點(diǎn),二者都是一個Node類型,我們看看這個類的定義
只有2個屬性,一個是泛型,一個是指向下一個節(jié)點(diǎn)的next。
下面是構(gòu)造函數(shù),一個帶集合對象參數(shù)的構(gòu)造函數(shù),一個無參構(gòu)造函數(shù)
如果是無參的直接把頭尾都初始化為null,
如果是有參數(shù)的,則如下圖,遍歷我們傳進(jìn)來的集合,然后取出每一個對象,并且包裝為node對象,然后遍歷并且調(diào)用node的lazySetNext執(zhí)行鏈條指針設(shè)置,通過名字可以理解為是一個延遲設(shè)置,并且把t永遠(yuǎn)指向最新加入的一個。最后然后把head和tail指向了h和t。
我們下面看看lazySetNext的方法實(shí)現(xiàn)
這里直接調(diào)用了UNSAFE對象的putorderedObject方法,三個參數(shù)分別是當(dāng)前node,偏移量,下一個node對象。從下圖中可知,node在初始化的時候根據(jù)node類的類對象通過反射獲取了next這個屬性,我們node類的next屬性就是指向的下一個node,然后通過UNSAFE的objectFieldOffset設(shè)置為偏移量。這里可以理解為,每次我創(chuàng)建node對象的時候,通過反射機(jī)制,返回一個新的node,然后設(shè)置為屬性偏移量。有興趣的同學(xué)可以去讀下反射的這些源碼,反射返回的是一個復(fù)制的新對象。也就是說我們每次初始化的時候已經(jīng)對node里面的nextNode通過反射也進(jìn)行了初始化。
這里不知道大家注意到?jīng)],我們并沒有設(shè)置邊界,也就是說很可能鏈表隊(duì)列是一個無邊界的。
這里提出個問題:隊(duì)列的大小怎么維護(hù)的,目前為止我們還沒有看到過隊(duì)列大小的屬性值。
下面我們看add方法,
通過解讀可以知道,是沒有邊界的,所以add方法永遠(yuǎn)會成功
這里調(diào)用了offer方法,我們來看看offer實(shí)現(xiàn)
這里假設(shè)我們第一次添加一個節(jié)點(diǎn)1,然后此時tail為null,然后p.next為null,然后用cas改變head為節(jié)點(diǎn)1,tail的next改變?yōu)橹赶蜃约?/p>
接下來我們添加第二個節(jié)點(diǎn)2,此時tail為null,t為null,p為null,p.next為自身,顯然q不為null,然后q==p,然后通過三目運(yùn)算符把p指向了節(jié)點(diǎn)1.
過程如下圖:
后面的繼續(xù)追加基本就是這個模式了,這里用了個for循環(huán),在并發(fā)下保證能追加進(jìn)去。
這里有一個問題我也搞不清楚,從后面的節(jié)點(diǎn)debug查看,CAS算法的確是把next指向了新入的節(jié)點(diǎn),但是當(dāng)?shù)谝粋€節(jié)點(diǎn)null的時候,用CAS產(chǎn)生的效果是,head指向了新加入節(jié)點(diǎn),tail的next指向了自身。我一直也沒搞明白,然后在加入節(jié)點(diǎn)后把這個Null的節(jié)點(diǎn)丟棄掉。至此,無鎖的linkedqueue的追加就完成了。
下面我們來看poll方法,poll方法總是彈出第一個,也就是先進(jìn)先出。從這里可以看得出在并發(fā)下,如果產(chǎn)生競爭,也是通過不停的修改head的for循環(huán)來進(jìn)行彈出的。這里p.casitem后同時會把下一個節(jié)點(diǎn)升級為head。這里如果多線程環(huán)境下,當(dāng)一個線程拿到了某一個節(jié)點(diǎn)a,第二個線程也拿到了a,但是搶先去拿走了值,此時a節(jié)點(diǎn)的下一個節(jié)點(diǎn)為null,這時候head也為Null,然后p.next為null,h為null,所以直接返回了null值。如果P.next指向了自身,則滿足第三個判斷,然后這時候跳過本次循環(huán)從最上面開始再次進(jìn)入循環(huán)。如果q=p.next不為null,p也不和q相等,則直接把p指向next,然后再次循環(huán)。這樣就基本解決了所有的可能性:
? ? ? ? ? ? ?需要彈出的p節(jié)點(diǎn)不為Null
? ? ? ? ? ? ?需要彈出的p節(jié)點(diǎn)為null,但是下個節(jié)點(diǎn)不為null,但指向自己
? ? ? ? ? ? ?需要彈出的p節(jié)點(diǎn)為null,但是下個節(jié)點(diǎn)不為null,但也不指向自己
? ? ? ? ? ? ?需要彈出的節(jié)點(diǎn)為null,下一個也為null,然后返回null。
peek操作總返回head,但是不從鏈接上刪除,這里的判斷條件為,當(dāng)前節(jié)點(diǎn)的item如果不為null,則直接對自身進(jìn)行一個uphead,如果當(dāng)前節(jié)點(diǎn)的item為null,則繼續(xù)比價當(dāng)前節(jié)點(diǎn)的下一個節(jié)點(diǎn)是不是null,如果是null,則進(jìn)行一個偽head替換,然后返回Null,如果不是則比較當(dāng)前節(jié)點(diǎn)與當(dāng)前節(jié)點(diǎn)的下一個節(jié)點(diǎn)是不是相等,如果相等則跳過本次循環(huán)繼續(xù)從頭開始,如果不相等則把當(dāng)前節(jié)點(diǎn)指向下個節(jié)點(diǎn),然后再次循環(huán)。
isempty方法是擁來查看是否為空
調(diào)用的是first,方法,這里面進(jìn)行了比較判斷,也是基于兩個for嵌套循環(huán),這個給我們對于并發(fā)下實(shí)現(xiàn)無鎖模型算法提供了非常好的思路。如果當(dāng)前節(jié)點(diǎn)的item不為null,進(jìn)行一個偽head替換,然后返回當(dāng)前節(jié)點(diǎn),如果為null,則查看下一個節(jié)點(diǎn)是不是null,如果是null,則進(jìn)行一個偽升級,然后返回null,如果下一個節(jié)點(diǎn)不為null,則考慮是不是指向自身,如果是則重新開始循環(huán),如果不是,則把當(dāng)前節(jié)點(diǎn)指向下一個然后繼續(xù)循環(huán)。
size方法,前面我們注意到了,在這個queue中并未維護(hù)關(guān)于大小的數(shù)字,我們接下來看size怎么實(shí)現(xiàn),這里是在size方法里面進(jìn)行統(tǒng)計的,在原文注釋中解釋這個返回值并不準(zhǔn)確,并發(fā)條件下的追加和刪除都可能使得返回的size不正確。
contains(o)方法返回是否包含此對象,由方法可以看出,通過遍歷這個鏈條來比較,直到當(dāng)前的p為null為止。并發(fā)條件下僅僅可以作為判斷是否曾經(jīng)存在過,也許在返回true的那一刻,就被消費(fèi)掉了。這種狀態(tài)僅僅保證存在過,不保證下一刻存在。
remove方法需要指定刪除,也是需要遍歷整個鏈條,如果找到了則立馬把當(dāng)前的item修改為null,然后獲取到下一個節(jié)點(diǎn),然后用前一個指向下一個完成刪除。這里的刪除分2步,第一步首先把item修改為Null,第二步然后修改前一個鏈條和下一個鏈條的關(guān)系完成刪除。
toArray返回一個Object數(shù)組,但是對鏈條中的節(jié)點(diǎn)不進(jìn)行刪除操作
toArray數(shù)組返回到奧指定的數(shù)組中,從源碼中可以看到,能返回指定的數(shù)組
addAll方法可以直接批量添加一個集合,首先對集合遍歷,然后把每一個對象包裝為node,然后先將這個集合進(jìn)行自身鏈條化,而不是一個個先追加到原來的鏈條上,這樣既能保證我們集合的連續(xù)性,同時也避免了并發(fā)條件下帶來的復(fù)雜性。
當(dāng)上面的鏈條完成后,我們開始往我們真正的鏈條上追加,可以看到如果tail的next為null,則直接把我們前面鏈條的第一個然后連接到next即可,然后把tail指向最后一個,如果失敗后先把t指向tail然后把最后一個的next指向null,再進(jìn)行一次tail的改變。
至此我們整個源碼都結(jié)束了,由此引出來的2個問題
? ? ? ? ?第一:如果CAS時,需要修改的對象為null的node,則我們會把目標(biāo)對象變?yōu)槲覀兊膆ead,然后把null的node的next指向自身
? ? ? ? 第二:在我們poll的時候,也可以看出這一點(diǎn)來,如果當(dāng)前節(jié)點(diǎn)為null,則直接把目標(biāo)對象變?yōu)槲覀兊膆ead.這里不知道是CAS的算法,還是java自身對CAS的進(jìn)一步優(yōu)化呢?
整個LinkedBlockQueue是無鎖的,全部都基于for循環(huán)和CAS算法,而且也是無界的。
關(guān)于這個的優(yōu)勢就是真的快,無鎖并發(fā)下,通過不停的循環(huán)來追加,會很快,因此也會比較耗內(nèi)存和CPU,因?yàn)槿绻粚φ埱缶€程限制,同一時間會有很多線程在不停的搶cpu資源,他們不存在線程終結(jié)的概念,只有線程執(zhí)行完。