Java并發(fā)編程之ConcurrentLinkedQueue隊(duì)列詳情
ConcurrentLinkedQueue
JDK中提供了一系列場(chǎng)景的并發(fā)安全隊(duì)列??偟膩?lái)說(shuō),按照實(shí)現(xiàn)方式的不同可分為阻塞隊(duì)列和非阻塞隊(duì)列,前者使用鎖實(shí)現(xiàn),而后則使用CAS非阻塞算法實(shí)現(xiàn)。
ConcurrentLinkedQueue
內(nèi)部的隊(duì)列使用單向鏈表方式實(shí)現(xiàn),其中有兩個(gè)volatile
類型的 Node 節(jié)點(diǎn)分別用來(lái)存放隊(duì)列的首、尾節(jié)點(diǎn)。從下面的無(wú)參構(gòu)造函數(shù)可知,默認(rèn)頭、尾節(jié)點(diǎn)都是指向 item 為null 的哨兵節(jié)點(diǎn)。新元素會(huì)被插入隊(duì)列末尾,出隊(duì)時(shí)從隊(duì)列頭部獲取一個(gè)元素。
public ConcurrentLinkedQueue() { head = tail = new Node<E>(null); }
在 Node 節(jié)點(diǎn)內(nèi)部則維護(hù)一個(gè)使用volatile 修飾的變量 item,用來(lái)存放節(jié)點(diǎn)的值;next用來(lái)存放鏈表的下一個(gè)節(jié)點(diǎn),從而鏈接為一個(gè)單向無(wú)界鏈表。其內(nèi)部則使用 UNSafe 工具類提供的CAS 算法來(lái)保證出入隊(duì)時(shí)操作鏈表的原子性。
下面通過(guò)介紹ConcurrentLinkedQueue
的幾個(gè)方法來(lái)介紹其實(shí)現(xiàn)原理。
offer操作: offer操作是在隊(duì)列末尾添加一個(gè)元素,如果傳遞的參數(shù)是null則拋出NPE異常,否則由于ConcurrentLinkedQueue
是無(wú)界隊(duì)列,該方法一直會(huì)返回true。另外,由于使用CAS無(wú)阻塞算法,因此該方法不會(huì)阻塞掛起調(diào)用線程。下面具體看下實(shí)現(xiàn)原理。
public boolean offer(E e) { //(1)e為null這拋出空指針異常 checkNotNull(e); //(2)構(gòu)造Node節(jié)點(diǎn),在構(gòu)造函數(shù)內(nèi)部調(diào)用unsafe.putObject final Node<E> newNode = new Node<E>(e); //(3) 從尾節(jié)點(diǎn)插入 for (Node<E> t = tail, p = t;;) { Node<E> q = p.next; //(4) 如果q==null說(shuō)明p是尾節(jié)點(diǎn),則執(zhí)行插入 if (q == null) { // p is last node //(5)使用CAS設(shè)置p節(jié)點(diǎn)的next節(jié)點(diǎn) if (p.casNext(null, newNode)) { // Successful CAS is the linearization point // for e to become an element of this queue, // and for newNode to become "live". //(6)CAS成功,則說(shuō)明新增節(jié)點(diǎn)已經(jīng)放入鏈表,然后設(shè)置當(dāng)前尾巴節(jié)點(diǎn) if (p != t) // hop two nodes at a time casTail(t, newNode); // Failure is OK. return true; } // Lost CAS race to another thread; re-read next } else if (p == q) // We have fallen off list. If tail is unchanged, it // will also be off-list, in which case we need to // jump to head, from which all live nodes are always // reachable. Else the new tail is a better bet. p = (t != (t = tail)) ? t : head; else // Check for tail updates after two hops. p = (p != t && t != (t = tail)) ? t : q; } }
- 首先看當(dāng)一個(gè)線程調(diào)用
offer
(item)時(shí)的情況。首先代碼(1)對(duì)傳參進(jìn)行空檢查, 由于使用 如果為null 則拋出NPE 異常,否則執(zhí)行代碼(2)并使用item作為構(gòu)造函數(shù)參數(shù)創(chuàng)建一 個(gè)新的節(jié)點(diǎn),然后代碼(3)從隊(duì)列尾部節(jié)點(diǎn)開(kāi)始循環(huán),打算從隊(duì)列尾部添加元素。這時(shí)候節(jié)點(diǎn)p、t、head、tail同時(shí)指向了item為null的哨兵節(jié)點(diǎn),由于哨兵節(jié)點(diǎn)的next 節(jié)點(diǎn)為null,所以這里q也指向null。代碼(4)發(fā)現(xiàn)q->null則執(zhí)行代碼(5),通過(guò)CAS 原子操作判斷p節(jié)點(diǎn)的next節(jié)點(diǎn)是否為null,如果為null 則使用節(jié)點(diǎn)newNode替換p的next節(jié)點(diǎn),然后執(zhí)行代碼(6),這里由于p=t所以沒(méi)有設(shè)置尾部節(jié)點(diǎn),然后退出 offer方法。 - 上面是一個(gè)線程調(diào)用offer方法的情況,如果多個(gè)線程同時(shí)調(diào)用,就會(huì)存在多個(gè)線程同時(shí)執(zhí)行到代碼(5)的情況。假設(shè)線程A調(diào)用offer(item1),線程B調(diào)用 ofer(item2),同時(shí)執(zhí)行到代碼(5)p.casNext(null, newNode)。由于CAS的比較設(shè)置操作是原子性的,所以這里假設(shè)線程A先執(zhí)行了比較設(shè)置操作,發(fā)現(xiàn)當(dāng)前p的 next 節(jié)點(diǎn)確實(shí)是null,則會(huì)原子性地更新next節(jié)點(diǎn)為iteml,這時(shí)候線程B也會(huì)判斷p的next節(jié)點(diǎn)是否為null,結(jié)果發(fā)現(xiàn)不是null(因?yàn)榫€程A已經(jīng)設(shè)置了p的next節(jié)點(diǎn)為iteml),則會(huì)跳到代碼(3),然后執(zhí)行到代碼(4)。
可見(jiàn),offer 操作中的關(guān)鍵步驟是代碼(5),通過(guò)原子CAS 操作來(lái)控制某時(shí)只有一個(gè)線程可以追加元素到隊(duì)列末尾。進(jìn)行CAS 競(jìng)爭(zhēng)失敗的線程會(huì)通過(guò)循環(huán)一次次嘗試進(jìn)行 CAS操作,直到CAS 成功才會(huì)返回,也就是通過(guò)使用無(wú)限循環(huán)不斷進(jìn)行 CAS 嘗試方式來(lái)替代阻塞算法掛起調(diào)用線程。相比阻塞算法,這是使用CPU資源換取阻塞所帶來(lái)的開(kāi)銷。
add操作:
add操作是在鏈表尾部添加一個(gè)元素,其實(shí)在內(nèi)部調(diào)用的還是offer操作。
public boolean add(E e) { return offer(e); }
poll操作:
poll操作是在隊(duì)列頭部獲取并移除一個(gè)元素,如果隊(duì)列為空則返回null。
public E poll() { restartFromHead: for (;;) { for (Node<E> h = head, p = h, q;;) { E item = p.item; if (item != null && p.casItem(item, null)) { // Successful CAS is the linearization point // for item to be removed from this queue. if (p != h) // hop two nodes at a time updateHead(h, ((q = p.next) != null) ? q : p); return item; } else if ((q = p.next) == null) { updateHead(h, p); return null; } else if (p == q) continue restartFromHead; else p = q; } } }
poll方法在移除一個(gè)元素時(shí),只是簡(jiǎn)單地使用 CAS操作把當(dāng)前節(jié)點(diǎn)的item值設(shè)置為null,然后通過(guò)重新設(shè)置頭節(jié)點(diǎn)將該元素從隊(duì)列里面移除,被移除的節(jié)點(diǎn)就成了孤立節(jié)點(diǎn),這個(gè)節(jié)點(diǎn)會(huì)在垃圾回收時(shí)被回收掉。另外,如果在執(zhí)行分支中發(fā)現(xiàn)頭節(jié)點(diǎn)被修改了,要跳到外層循環(huán)重新獲取新的頭節(jié)點(diǎn)。
peak操作:
peak操作是獲取隊(duì)列頭部獲一個(gè)元素,如果隊(duì)列為空則返回null。
public E peek() { restartFromHead: for (;;) { for (Node<E> h = head, p = h, q;;) { E item = p.item; //注釋 if (item != null || (q = p.next) == null) { updateHead(h, p); return item; } else if (p == q) continue restartFromHead; else p = q; } } }
Peek操作的代碼結(jié)構(gòu)與poll操作類似,不同之處在于我們?cè)诖a中標(biāo)記注釋的地方中少了castItem操作。其實(shí)這很正常,因?yàn)閜eek只是獲取隊(duì)列頭元素值,并不清空其值。根據(jù)前面的介紹我們知道第一次執(zhí)行offer后head指向的是哨兵節(jié)點(diǎn)(也就是item為null的節(jié)點(diǎn)),那么第一次執(zhí)行peek時(shí)在注釋處會(huì)發(fā)現(xiàn)item==null,然后執(zhí)行q=p.next,這時(shí)候q節(jié)點(diǎn)指向的才是隊(duì)列里面第一個(gè)真正的元素,或者如果隊(duì)列為 null 則 q 指向 null。
到此這篇關(guān)于Java并發(fā)編程之ConcurrentLinkedQueue隊(duì)列詳情的文章就介紹到這了,更多相關(guān)Java并發(fā)編程 ConcurrentLinkedQueue 內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
深入分析Android系統(tǒng)中SparseArray的源碼
這篇文章主要介紹了深入分析Android系統(tǒng)中SparseArray的源碼,SparseArray為Java實(shí)現(xiàn),需要的朋友可以參考下2015-07-07啟用springboot security后登錄web頁(yè)面需要用戶名和密碼的解決方法
這篇文章主要介紹了啟用springboot security后登錄web頁(yè)面需要用戶名和密碼的解決方法,也就是使用默認(rèn)用戶和密碼登錄的操作方法,本文結(jié)合實(shí)例代碼給大家介紹的非常詳細(xì),需要的朋友可以參考下2023-02-02Java通過(guò)百度API實(shí)現(xiàn)圖片車牌號(hào)識(shí)別
這段時(shí)間做項(xiàng)目需要用java程序進(jìn)行車牌識(shí)別,因此嘗試做了下這個(gè)程序,本代碼功能是通過(guò)調(diào)用百度API實(shí)現(xiàn)的,感興趣的可以了解一下2021-06-06idea輸入sout無(wú)法自動(dòng)補(bǔ)全System.out.println()的問(wèn)題
這篇文章主要介紹了idea輸入sout無(wú)法自動(dòng)補(bǔ)全System.out.println()的問(wèn)題,本文給大家分享解決方案,供大家參考,需要的朋友可以參考下2020-07-07SpringBoot根據(jù)各地區(qū)時(shí)間設(shè)置接口有效時(shí)間的實(shí)現(xiàn)方式
這篇文章給大家介紹了SpringBoot根據(jù)各地區(qū)時(shí)間設(shè)置接口有效時(shí)間的實(shí)現(xiàn)方式,文中通過(guò)代碼示例給大家講解的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作有一定的幫助,需要的朋友可以參考下2024-01-01深入了解Maven Settings.xml文件的結(jié)構(gòu)和功能
這篇文章主要為大家介紹了Maven Settings.xml文件基本結(jié)構(gòu)和功能詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-11-11springboot基于Redis發(fā)布訂閱集群下WebSocket的解決方案
這篇文章主要介紹了springboot基于Redis發(fā)布訂閱集群下WebSocket的解決方案,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2021-01-01