線程池主要有兩種創(chuàng)建方式。
一種是選擇使用Executors線程池工具類,可以理解為線程池工廠類,通過(guò)該類設(shè)置好的一些靜態(tài)方法,創(chuàng)建指定類型的線程池(本質(zhì)上就是通過(guò)new創(chuàng)建線程池,但是該類為我們?cè)O(shè)置好了各類型線程池的默認(rèn)參數(shù))。
第二種是選擇自己創(chuàng)建線程池,通過(guò)new的方法,設(shè)置好創(chuàng)建線程池的七個(gè)參數(shù),自己手動(dòng)創(chuàng)建。但是網(wǎng)上包括阿里都是推薦我們使用Executors的默認(rèn)創(chuàng)建方式。
點(diǎn)開(kāi)Executors的創(chuàng)建線程池方法,我們發(fā)現(xiàn)它選擇使用的workQueue(參數(shù)類型是BlockingQueue)任務(wù)隊(duì)列,除了SynchronousQueue(比較特殊,這里不講)就是LinkedBlockingQueue,并且主要使用的就是LinkedBlockingQueue,那么為什么不使ArrayBlockingQueue呢?
因?yàn)槲覀冎饕vAQS這里我先寫(xiě)原因:
因?yàn)锳rrayBlockingQueue是基于數(shù)組實(shí)現(xiàn)的,需要在創(chuàng)建時(shí)給定長(zhǎng)度,所以在不會(huì)對(duì)阻塞隊(duì)列空間進(jìn)行判斷的線程池中使用它是需要仔細(xì)考慮這一細(xì)節(jié)的,如果設(shè)置的ArrayBlockingQueue長(zhǎng)度過(guò)長(zhǎng)會(huì)造成空間的多余浪費(fèi),設(shè)置的長(zhǎng)度過(guò)短,可能會(huì)導(dǎo)致本來(lái)不想拒絕的任務(wù)被執(zhí)行拒絕策略。而LinkedArrayQueue則不會(huì),它默認(rèn)長(zhǎng)度是Integer.MAX_VALUE,這個(gè)長(zhǎng)度不是代表它初始化的時(shí)候會(huì)創(chuàng)建這么多個(gè)節(jié)點(diǎn),而是說(shuō)最多可以添加這么多個(gè)節(jié)點(diǎn),每次執(zhí)行put,它都會(huì)判斷當(dāng)前隊(duì)列長(zhǎng)度是否大于Integer.MAX_VALUE,如果不大于則添加新節(jié)點(diǎn),當(dāng)然也可以給LinkedBlockingQueue默認(rèn)長(zhǎng)度來(lái)限制隊(duì)列里的任務(wù)數(shù)量,但作為默認(rèn)創(chuàng)建的方式,顯然LinkedBlockingQueue更為安全(在程序員沒(méi)有考慮任務(wù)隊(duì)列長(zhǎng)度的情況下)。但是ArrayBlockingQueue的效率是要高于LinkedArrayQueue的,因?yàn)長(zhǎng)inkedBlockingQueue是基于鏈表實(shí)現(xiàn)的,無(wú)論是出隊(duì)還是入隊(duì)操作,都需要操作Node節(jié)點(diǎn),會(huì)稍微比ArrayBlockingQueue更耗費(fèi)內(nèi)存一丟丟。在程序員可以確定隊(duì)列長(zhǎng)度的情況下,使用ArrayBlockingQueue可能更為合適。
ArrayBlcokingQueue原理
參考
http://www.itdecent.cn/p/584631fcd9bf
LinkedBlockingQueue原理
前面也講過(guò)了LinkedBlockingQueue是基于鏈表實(shí)現(xiàn)的,那么接下來(lái)是它的具體實(shí)現(xiàn)細(xì)節(jié)。

LinkedBlockingQueue主要實(shí)現(xiàn)了BlockingQueue接口(該接口定義了阻塞隊(duì)列的一些必要方法如take,put)和繼承了AbstractQueue抽象類(該類定義并實(shí)現(xiàn)了部分LinkQueue的必要方法,可以將它理解為以鏈表為原理實(shí)現(xiàn)的一系列隊(duì)列的抽象父類,包括ConcurrentLinkedQueue等非阻塞鏈表隊(duì)列也繼承了它,這個(gè)類的最頂層父類就是Collection,這里我們不細(xì)講)

這里定義的所有變量都很關(guān)鍵從上往下開(kāi)始講
1.靜態(tài)內(nèi)部類Node,鏈表節(jié)點(diǎn)的實(shí)現(xiàn)類,只包含一個(gè)item用于存值,與一個(gè)next指向下一個(gè)節(jié)點(diǎn),說(shuō)明這是一個(gè)單向鏈表。
2.final int capacity,這個(gè)capacity用于限制鏈表的最大長(zhǎng)度,在用構(gòu)造方法new LinkedBlockingQueue的時(shí)候可以初始化該值,如果沒(méi)有通過(guò)new設(shè)置,capacity的默認(rèn)值將會(huì)是Interger.MAX_VALUE,上面也說(shuō)過(guò)了,因?yàn)長(zhǎng)inkedBlockingQueue是基于鏈表而非數(shù)組,所以capacity并不是說(shuō)在創(chuàng)建鏈表的時(shí)候就初始化這么多個(gè)節(jié)點(diǎn),而是通過(guò)put方法添加節(jié)點(diǎn)的時(shí)候會(huì)判斷是否當(dāng)前節(jié)點(diǎn)數(shù)已達(dá)到capacity,如果沒(méi)達(dá)到則創(chuàng)建成功,所以如果沒(méi)有特殊要求,使用默認(rèn)值就好,不必?fù)?dān)心因?yàn)閏apacity過(guò)大而占用大量?jī)?nèi)存。
3.AtomicInteger count,這個(gè)類在我的ArrayBlockingQueue的文章中有寫(xiě)到,本質(zhì)上底層就是通過(guò)unsafe的cas操作在該類中維護(hù)了一個(gè)volite int類型的變量value,但是因?yàn)樵擃悡碛性S多unsafe的cas操作方法,所以在修改該變量?jī)?nèi)的int時(shí)可以很方便的使用這些cas方法以保證該類對(duì)value操作的原子性,并由于維護(hù)的value也用volite修飾,同時(shí)也保證了可見(jiàn)性。 count 用該類封裝來(lái)存放當(dāng)前的當(dāng)前隊(duì)列中的節(jié)點(diǎn)個(gè)數(shù),保證了無(wú)論從put還是take各種需要讀取或操作count操作的方法中對(duì)count讀取的正確性與對(duì)count操作的原子性。
4.Node head 與 Node last,重頭戲來(lái)了,如果看過(guò)ReentranLock源碼的人應(yīng)該已經(jīng)發(fā)現(xiàn)了,這與AQS的數(shù)據(jù)結(jié)構(gòu)特別相似,由head指向隊(duì)列頭節(jié)點(diǎn),last指向隊(duì)列尾節(jié)點(diǎn),我們直到AQS維護(hù)的數(shù)據(jù)結(jié)構(gòu)也是由類似的head指向頭節(jié)點(diǎn),tail指向尾節(jié)點(diǎn),并且隊(duì)列中會(huì)維護(hù)一個(gè)不存放數(shù)據(jù)的空節(jié)點(diǎn)。LinkedBlockingQueue也會(huì)一樣維護(hù)一個(gè)空節(jié)點(diǎn)嗎?這里我可以說(shuō)是的,等下我們看構(gòu)造方法的時(shí)候就可以證明。

5.Condition notEmpty和Condition notFull,兩個(gè)阻塞隊(duì)列,分別用于阻塞在線程為空時(shí)調(diào)用出隊(duì)方法的線程與線程已滿時(shí)調(diào)用入隊(duì)操作的線程。(具體可以看ArrayBlockingQueue原理)
6.ReentrantLock takeLock和putLock,我們知道ArrayBlockingQueue中只有一個(gè)ReentrantLock lock用于實(shí)現(xiàn)隊(duì)列的同步,在ArrayBlockingQueue中無(wú)論是入隊(duì)方法還是出隊(duì)方法都要使用該鎖,而LinkedBlockingQueue選擇使用兩個(gè)鎖,分別用于對(duì)入隊(duì)和出隊(duì)操作進(jìn)行加鎖。我們可以將其理解為分段鎖,這樣它的讀寫(xiě)效率是要高于ArrayBlockingQueue的。
但同時(shí)又產(chǎn)生了一個(gè)問(wèn)題:用兩把鎖分別進(jìn)行讀寫(xiě),如何保證鏈表結(jié)構(gòu)的線程安全?
這就是AQS數(shù)據(jù)結(jié)構(gòu)的神奇之處了。


我們先看它的構(gòu)造方法,無(wú)參構(gòu)造方法里首先證明了我之前說(shuō)的capacity默認(rèn)值為Integer.MAX_VALUE的問(wèn)題,接著看有參構(gòu)造方法,它確實(shí)初始化了一個(gè)值為null的空Node,并講last和head都指向它。LinkedBlockingQueue確實(shí)在內(nèi)部維護(hù)了一個(gè)迷你版的AQS。
如果不使用AQS的哨兵節(jié)點(diǎn)可能存在哪些問(wèn)題?
我們這里把這個(gè)AQS中空值的頭節(jié)點(diǎn)稱為哨兵節(jié)點(diǎn)。
以下為無(wú)哨兵節(jié)點(diǎn)的情況:
我們要知道ArrayBlockingQueue和LinkedBlockingQueue都是阻塞隊(duì)列,當(dāng)隊(duì)列為空嘗試出隊(duì)操作的線程和隊(duì)列已滿嘗試入隊(duì)操作的線程是都會(huì)被阻塞的。且由于他們都是隊(duì)列的數(shù)據(jù)結(jié)構(gòu),隊(duì)頭入隊(duì),隊(duì)尾出隊(duì),所以他們的競(jìng)爭(zhēng)情況極小,如果隊(duì)列中有很多節(jié)點(diǎn)(未滿的情況)入隊(duì)和出隊(duì)操作都是對(duì)分別對(duì)頭尾的節(jié)點(diǎn)進(jìn)行操作,中間隔著好幾個(gè)節(jié)點(diǎn),它們互不干擾。當(dāng)隊(duì)列無(wú)節(jié)點(diǎn)的時(shí)候,由于阻塞隊(duì)列的特性,執(zhí)行出隊(duì)操作的線程被阻塞,會(huì)保證執(zhí)行入隊(duì)操作先執(zhí)行,也不存在競(jìng)爭(zhēng),當(dāng)隊(duì)列滿節(jié)點(diǎn)的時(shí)候同理。
我們發(fā)現(xiàn)阻塞隊(duì)列想要存在競(jìng)爭(zhēng)關(guān)系,只有一種情況,當(dāng)隊(duì)列里只有一個(gè)節(jié)點(diǎn)的時(shí)候(這時(shí)出隊(duì)操作要?jiǎng)h除的節(jié)點(diǎn)是這個(gè)節(jié)點(diǎn)入隊(duì)操作也是需要修改這個(gè)節(jié)點(diǎn)的next指向新的節(jié)點(diǎn)),假設(shè)無(wú)哨兵節(jié)點(diǎn),
那么可能存在這樣一種情況:
出隊(duì)的線程在執(zhí)行dequeue方法之前(該方法為執(zhí)行出隊(duì)的具體操作,即修改head指向下一個(gè)節(jié),將頭節(jié)點(diǎn)的引用指向自己,實(shí)現(xiàn)頭節(jié)點(diǎn)的GC回收),判斷隊(duì)列里存在一個(gè)頭節(jié)點(diǎn),但是在具體執(zhí)行dequeue方法的時(shí)候由于線程的并發(fā)性,執(zhí)行入隊(duì)節(jié)點(diǎn)的線程并不知道頭節(jié)點(diǎn)會(huì)被刪除,還是會(huì)調(diào)用enqueue方法(該方法為執(zhí)行入隊(duì)的具體操作,即修改上一個(gè)節(jié)點(diǎn)的next指向入隊(duì)節(jié)點(diǎn)并修改last節(jié)點(diǎn)指向入隊(duì)節(jié)點(diǎn),實(shí)現(xiàn)入隊(duì)),將頭節(jié)點(diǎn)的next指向入隊(duì)節(jié)點(diǎn)(這里有點(diǎn)繞這里的情況是head和last都指向隊(duì)列中唯一一個(gè)節(jié)點(diǎn),雖然是通過(guò)last.next=new Node來(lái)添加新節(jié)點(diǎn),但是不可能實(shí)時(shí)判斷l(xiāng)ast指向是否為空,可能上一秒判斷l(xiāng)ast指向頭節(jié)點(diǎn)不為空,開(kāi)始執(zhí)行入隊(duì)操作的時(shí)候last卻已經(jīng)為空,那么使用last.next就會(huì)拋空指針異常),這是很危險(xiǎn)的,無(wú)法保證判斷l(xiāng)ast不為空,使用last的時(shí)候也不為空(last指向的頭節(jié)點(diǎn)隨時(shí)可能出隊(duì),last和head就會(huì)指向null)。

為什么AQS能實(shí)現(xiàn)隊(duì)列線程安全?
點(diǎn)開(kāi)enqueue方法,和dequeue方法,我們逐行分析,看看AQS是如何實(shí)現(xiàn)隊(duì)列線程安全的。

很簡(jiǎn)單的實(shí)現(xiàn),就是把last指向的尾節(jié)指向新入隊(duì)的節(jié)點(diǎn),并把last也指向新入隊(duì)的節(jié)點(diǎn),實(shí)現(xiàn)節(jié)點(diǎn)的添加。

1.先用h拿到head指向的頭節(jié)點(diǎn)(AQS中的哨兵節(jié)點(diǎn))。
2.接著用first拿到哨兵節(jié)點(diǎn)后的第一個(gè)節(jié)點(diǎn)(第一個(gè)存值的節(jié)點(diǎn))。
3.讓哨兵節(jié)點(diǎn)的next指向哨兵節(jié)點(diǎn)自己(實(shí)現(xiàn)GC回收)。
4.把head指向頭first(first為此時(shí)隊(duì)列中實(shí)際的頭節(jié)點(diǎn),并失去head對(duì)前哨兵節(jié)點(diǎn)的引用,此時(shí)前哨兵節(jié)點(diǎn)已可以被回收)
5-8.獲取頭節(jié)點(diǎn)的值并返回該值,且返回前把頭節(jié)點(diǎn)的值置為null。(此時(shí)頭節(jié)點(diǎn)成為新的哨兵節(jié)點(diǎn))
我們可以看到enqueue和dequeue分別操作的是head和頭節(jié)點(diǎn)及l(fā)ast和尾節(jié)點(diǎn),如果使用AQS的哨兵模式,保證隊(duì)列里至少有兩個(gè)節(jié)點(diǎn)(一個(gè)節(jié)點(diǎn)的時(shí)候就是隊(duì)列中除了哨兵節(jié)點(diǎn)無(wú)其他節(jié)點(diǎn)的情況,此時(shí)會(huì)認(rèn)為隊(duì)列中無(wú)元素,那么嘗試出隊(duì)操作的線程會(huì)被阻塞,不存在競(jìng)爭(zhēng)),當(dāng)隊(duì)列里有兩個(gè)節(jié)點(diǎn)的時(shí)候頭尾節(jié)點(diǎn)分別執(zhí)行入隊(duì)出隊(duì)操作,它們并不會(huì)對(duì)頭節(jié)點(diǎn)指向尾節(jié)點(diǎn)的next進(jìn)行操作(不會(huì)破壞兩個(gè)節(jié)點(diǎn)之間的關(guān)系而是操作各自的節(jié)點(diǎn)及各自的head/last),也就是本質(zhì)上是互不干擾的,也就自然而然不存在競(jìng)爭(zhēng)問(wèn)題。
AQS為什么必須有哨兵節(jié)點(diǎn)
1.如果沒(méi)有哨兵節(jié)點(diǎn),那么每次執(zhí)行入隊(duì)操作,都需要判斷head是否為空,如果為空則head=new Node如果不為空則head.next=new Node,而有哨兵節(jié)點(diǎn)則可以大膽的head.next=new Node.
2.如果沒(méi)有哨兵節(jié)點(diǎn),可能存在之前所說(shuō)的安全性問(wèn)題,當(dāng)只有一個(gè)節(jié)點(diǎn)的時(shí)候執(zhí)行入隊(duì)方法,無(wú)法保證last和head不為空。哪怕執(zhí)行enqueue入隊(duì)之前l(fā)ast和head還指向一個(gè)節(jié)點(diǎn),可能由于并發(fā)性在具體調(diào)用enqueue方法操作last的時(shí)候head和last共同指向的頭節(jié)點(diǎn)已經(jīng)完成出隊(duì),此時(shí)last和head都為null,所以enqueue方法中的last.next=new node會(huì)拋空指針異常,且由于線程并發(fā)性的問(wèn)題,last始終可能隨時(shí)為空的問(wèn)題不使用哨兵節(jié)點(diǎn)是無(wú)法解決的。