Java 并發(fā)包有很大一部分內(nèi)容都是關(guān)于并發(fā)容器的,因此學(xué)習(xí)和搞懂這部分的內(nèi)容很有必要。
Java 1.5 之前提供的同步容器雖然也能保證線程安全,但是性能很差,而 Java 1.5 版本之后提供的并發(fā)容器在性能方面則做了很多優(yōu)化,并且容器的類型也更加豐富了。下面我們就對(duì)比二者來(lái)學(xué)習(xí)這部分的內(nèi)容。
同步容器及其注意事項(xiàng)
Java 中的容器主要可以分為四個(gè)大類,分別是 List、Map、Set 和 Queue,但并不是所有的 Java 容器都是線程安全的。例如,我們常用的 ArrayList、HashMap 就不是線程安全的。在介紹線程安全的容器之前,我們先思考這樣一個(gè)問(wèn)題:如何將非線程安全的容器變成線程安全的容器?
在前面《12 | 如何用面向?qū)ο笏枷雽?xiě)好并發(fā)程序?》我們講過(guò)實(shí)現(xiàn)思路其實(shí)很簡(jiǎn)單,只要把非線程安全的容器封裝在對(duì)象內(nèi)部,然后控制好訪問(wèn)路徑就可以了。
我們?cè)?jīng)多次強(qiáng)調(diào),組合操作需要注意競(jìng)態(tài)條件問(wèn)題,例如上面提到的 addIfNotExist() 方法就包含組合操作。組合操作往往隱藏著競(jìng)態(tài)條件問(wèn)題,即便每個(gè)操作都能保證原子性,也并不能保證組合操作的原子性,這個(gè)一定要注意。
在容器領(lǐng)域一個(gè)容易被忽視的“坑”是用迭代器遍歷容器,例如在下面的代碼中,通過(guò)迭代器遍歷容器 list,對(duì)每個(gè)元素調(diào)用 foo() 方法,這就存在并發(fā)問(wèn)題,這些組合的操作不具備原子性。
List list = Collections.
synchronizedList(new ArrayList());
Iterator i = list.iterator();
while (i.hasNext())
foo(i.next());
而正確做法是下面這樣,鎖住 list 之后再執(zhí)行遍歷操作。如果你查看 Collections 內(nèi)部的包裝類源碼,你會(huì)發(fā)現(xiàn)包裝類的公共方法鎖的是對(duì)象的 this,其實(shí)就是我們這里的 list,所以鎖住 list 絕對(duì)是線程安全的。
List list = Collections.
synchronizedList(new ArrayList());
synchronized (list) {
Iterator i = list.iterator();
while (i.hasNext())
foo(i.next());
}
上面我們提到的這些經(jīng)過(guò)包裝后線程安全容器,都是基于 synchronized 這個(gè)同步關(guān)鍵字實(shí)現(xiàn)的,所以也被稱為同步容器。Java 提供的同步容器還有 Vector、Stack 和 Hashtable,這三個(gè)容器不是基于包裝類實(shí)現(xiàn)的,但同樣是基于 synchronized 實(shí)現(xiàn)的,對(duì)這三個(gè)容器的遍歷,同樣要加鎖保證互斥。
并發(fā)容器及其注意事項(xiàng)
Java 在 1.5 版本之前所謂的線程安全的容器,主要指的就是同步容器。不過(guò)同步容器有個(gè)最大的問(wèn)題,那就是性能差,所有方法都用 synchronized 來(lái)保證互斥,串行度太高了。因此 Java 在 1.5 及之后版本提供了性能更高的容器,我們一般稱為并發(fā)容器。
并發(fā)容器雖然數(shù)量非常多,但依然是前面我們提到的四大類:List、Map、Set 和 Queue,下面的并發(fā)容器關(guān)系圖,基本上把我們經(jīng)常用的容器都覆蓋到了。

鑒于并發(fā)容器的數(shù)量太多,再加上篇幅限制,所以我并不會(huì)一一詳細(xì)介紹它們的用法,只是把關(guān)鍵點(diǎn)介紹一下。
(一)List
List 里面只有一個(gè)實(shí)現(xiàn)類就是 CopyOnWriteArrayList。CopyOnWrite,顧名思義就是寫(xiě)的時(shí)候會(huì)將共享變量新復(fù)制一份出來(lái),這樣做的好處是讀操作完全無(wú)鎖。
那 CopyOnWriteArrayList 的實(shí)現(xiàn)原理是怎樣的呢?下面我們就來(lái)簡(jiǎn)單介紹一下
CopyOnWriteArrayList 內(nèi)部維護(hù)了一個(gè)數(shù)組,成員變量 array 就指向這個(gè)內(nèi)部數(shù)組,所有的讀操作都是基于 array 進(jìn)行的,如下圖所示,迭代器 Iterator 遍歷的就是 array 數(shù)組。

如果在遍歷 array 的同時(shí),還有一個(gè)寫(xiě)操作,例如增加元素,CopyOnWriteArrayList 是如何處理的呢?CopyOnWriteArrayList 會(huì)將 array 復(fù)制一份,然后在新復(fù)制處理的數(shù)組上執(zhí)行增加元素的操作,執(zhí)行完之后再將 array 指向這個(gè)新的數(shù)組。通過(guò)下圖你可以看到,讀寫(xiě)是可以并行的,遍歷操作一直都是基于原 array 執(zhí)行,而寫(xiě)操作則是基于新 array 進(jìn)行。

使用 CopyOnWriteArrayList 需要注意的“坑”主要有兩個(gè)方面。一個(gè)是應(yīng)用場(chǎng)景,CopyOnWriteArrayList 僅適用于寫(xiě)操作非常少的場(chǎng)景,而且能夠容忍讀寫(xiě)的短暫不一致。例如上面的例子中,寫(xiě)入的新元素并不能立刻被遍歷到。另一個(gè)需要注意的是,CopyOnWriteArrayList 迭代器是只讀的,不支持增刪改。因?yàn)榈鞅闅v的僅僅是一個(gè)快照,而對(duì)快照進(jìn)行增刪改是沒(méi)有意義的。
(二)Map
Map 接口的兩個(gè)實(shí)現(xiàn)是 ConcurrentHashMap 和 ConcurrentSkipListMap,它們從應(yīng)用的角度來(lái)看,主要區(qū)別在于 ConcurrentHashMap 的 key 是無(wú)序的,而 ConcurrentSkipListMap 的 key 是有序的。所以如果你需要保證 key 的順序,就只能使用 ConcurrentSkipListMap。
使用 ConcurrentHashMap 和 ConcurrentSkipListMap 需要注意的地方是,它們的 key 和 value 都不能為空,否則會(huì)拋出NullPointerException這個(gè)運(yùn)行時(shí)異常。下面這個(gè)表格總結(jié)了 Map 相關(guān)的實(shí)現(xiàn)類對(duì)于 key 和 value 的要求,你可以對(duì)比學(xué)習(xí)。

ConcurrentSkipListMap 里面的 SkipList 本身就是一種數(shù)據(jù)結(jié)構(gòu),中文一般都翻譯為“跳表”。跳表插入、刪除、查詢操作平均的時(shí)間復(fù)雜度是 O(log n),理論上和并發(fā)線程數(shù)沒(méi)有關(guān)系,所以在并發(fā)度非常高的情況下,若你對(duì) ConcurrentHashMap 的性能還不滿意,可以嘗試一下 ConcurrentSkipListMap。
(三)Set
Set 接口的兩個(gè)實(shí)現(xiàn)是 CopyOnWriteArraySet 和 ConcurrentSkipListSet,使用場(chǎng)景可以參考前面講述的 CopyOnWriteArrayList 和 ConcurrentSkipListMap,它們的原理都是一樣的,這里就不再贅述了。
(四)Queue
Java 并發(fā)包里面 Queue 這類并發(fā)容器是最復(fù)雜的,你可以從以下兩個(gè)維度來(lái)分類。一個(gè)維度是阻塞與非阻塞,所謂阻塞指的是當(dāng)隊(duì)列已滿時(shí),入隊(duì)操作阻塞;當(dāng)隊(duì)列已空時(shí),出隊(duì)操作阻塞。另一個(gè)維度是單端與雙端,單端指的是只能隊(duì)尾入隊(duì),隊(duì)首出隊(duì);而雙端指的是隊(duì)首隊(duì)尾皆可入隊(duì)出隊(duì)。Java 并發(fā)包里阻塞隊(duì)列都用 Blocking 關(guān)鍵字標(biāo)識(shí),單端隊(duì)列使用 Queue 標(biāo)識(shí),雙端隊(duì)列使用 Deque 標(biāo)識(shí)。
這兩個(gè)維度組合后,可以將 Queue 細(xì)分為四大類,分別是:
1.單端阻塞隊(duì)列:其實(shí)現(xiàn)有 ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue、LinkedTransferQueue、PriorityBlockingQueue 和 DelayQueue。內(nèi)部一般會(huì)持有一個(gè)隊(duì)列,這個(gè)隊(duì)列可以是數(shù)組(其實(shí)現(xiàn)是 ArrayBlockingQueue)也可以是鏈表(其實(shí)現(xiàn)是 LinkedBlockingQueue);甚至還可以不持有隊(duì)列(其實(shí)現(xiàn)是 SynchronousQueue),此時(shí)生產(chǎn)者線程的入隊(duì)操作必須等待消費(fèi)者線程的出隊(duì)操作。而 LinkedTransferQueue 融合 LinkedBlockingQueue 和 SynchronousQueue 的功能,性能比 LinkedBlockingQueue 更好;PriorityBlockingQueue 支持按照優(yōu)先級(jí)出隊(duì);DelayQueue 支持延時(shí)出隊(duì)。

2.雙端阻塞隊(duì)列:其實(shí)現(xiàn)是 LinkedBlockingDeque。

3.單端非阻塞隊(duì)列:其實(shí)現(xiàn)是 ConcurrentLinkedQueue。
4.雙端非阻塞隊(duì)列:其實(shí)現(xiàn)是 ConcurrentLinkedDeque。
另外,使用隊(duì)列時(shí),需要格外注意隊(duì)列是否支持有界(所謂有界指的是內(nèi)部的隊(duì)列是否有容量限制)。實(shí)際工作中,一般都不建議使用無(wú)界的隊(duì)列,因?yàn)閿?shù)據(jù)量大了之后很容易導(dǎo)致 OOM。上面我們提到的這些 Queue 中,只有 ArrayBlockingQueue 和 LinkedBlockingQueue 是支持有界的,所以在使用其他無(wú)界隊(duì)列時(shí),一定要充分考慮是否存在導(dǎo)致 OOM 的隱患。
總結(jié)
Java 并發(fā)容器的內(nèi)容很多,但鑒于篇幅有限,我們只是對(duì)一些關(guān)鍵點(diǎn)進(jìn)行了梳理和介紹。
而在實(shí)際工作中,你不單要清楚每種容器的特性,還要能選對(duì)容器,這才是關(guān)鍵,至于每種容器的用法,用的時(shí)候看一下 API 說(shuō)明就可以了,這些容器的使用都不難。在文中,我們甚至都沒(méi)有介紹 Java 容器的快速失敗機(jī)制(Fail-Fast),原因就在于當(dāng)你選對(duì)容器的時(shí)候,根本不會(huì)觸發(fā)它。