Java并发编程实战~并发容器
在容器領域一個容易被忽視的“坑”是用迭代器遍歷容器,例如在下面的代碼中,通過迭代器遍歷容器 list,對每個元素調用 foo() 方法,這就存在并發問題,這些組合的操作不具備原子性。
List list = Collections.synchronizedList(new ArrayList()); Iterator i = list.iterator(); while (i.hasNext())foo(i.next());正確做法是下面這樣,鎖住 list 之后再執行遍歷操作
List list = Collections.synchronizedList(new ArrayList()); synchronized (list) { Iterator i = list.iterator(); while (i.hasNext())foo(i.next()); }發容器及其注意事項
Java 在 1.5 版本之前所謂的線程安全的容器,主要指的就是同步容器。不過同步容器有個最大的問題,那就是性能差,所有方法都用 synchronized 來保證互斥,串行度太高了。因此 Java 在 1.5 及之后版本提供了性能更高的容器,我們一般稱為并發容器。
并發容器雖然數量非常多,但依然是前面我們提到的四大類:List、Map、Set 和 Queue,下面的并發容器關系圖,基本上把我們經常用的容器都覆蓋到了。
?鑒于并發容器的數量太多,再加上篇幅限制,所以我并不會一一詳細介紹它們的用法,只是把關鍵點介紹一下。
(一)List
List 里面只有一個實現類就是CopyOnWriteArrayList。CopyOnWrite,顧名思義就是寫的時候會將共享變量新復制一份出來,這樣做的好處是讀操作完全無鎖。
那 CopyOnWriteArrayList 的實現原理是怎樣的呢?下面我們就來簡單介紹一下
CopyOnWriteArrayList 內部維護了一個數組,成員變量 array 就指向這個內部數組,所有的讀操作都是基于 array 進行的,如下圖所示,迭代器 Iterator 遍歷的就是 array 數組。
?如果在遍歷 array 的同時,還有一個寫操作,例如增加元素,CopyOnWriteArrayList 是如何處理的呢?CopyOnWriteArrayList 會將 array 復制一份,然后在新復制處理的數組上執行增加元素的操作,執行完之后再將 array 指向這個新的數組。通過下圖你可以看到,讀寫是可以并行的,遍歷操作一直都是基于原 array 執行,而寫操作則是基于新 array 進行。
?使用 CopyOnWriteArrayList 需要注意的“坑”主要有兩個方面。一個是應用場景,CopyOnWriteArrayList 僅適用于寫操作非常少的場景,而且能夠容忍讀寫的短暫不一致。例如上面的例子中,寫入的新元素并不能立刻被遍歷到。另一個需要注意的是,CopyOnWriteArrayList 迭代器是只讀的,不支持增刪改。因為迭代器遍歷的僅僅是一個快照,而對快照進行增刪改是沒有意義的。
(二)Map
Map 接口的兩個實現是 ConcurrentHashMap 和 ConcurrentSkipListMap,它們從應用的角度來看,主要區別在于ConcurrentHashMap 的 key 是無序的,而 ConcurrentSkipListMap 的 key 是有序的。所以如果你需要保證 key 的順序,就只能使用 ConcurrentSkipListMap。
使用 ConcurrentHashMap 和 ConcurrentSkipListMap 需要注意的地方是,它們的 key 和 value 都不能為空,否則會拋出NullPointerException這個運行時異常。下面這個表格總結了 Map 相關的實現類對于 key 和 value 的要求,你可以對比學習。
?ConcurrentSkipListMap 里面的 SkipList 本身就是一種數據結構,中文一般都翻譯為“跳表”。跳表插入、刪除、查詢操作平均的時間復雜度是 O(log n),理論上和并發線程數沒有關系,所以在并發度非常高的情況下,若你對 ConcurrentHashMap 的性能還不滿意,可以嘗試一下 ConcurrentSkipListMap。
(三)Set
Set 接口的兩個實現是 CopyOnWriteArraySet 和 ConcurrentSkipListSet,使用場景可以參考前面講述的 CopyOnWriteArrayList 和 ConcurrentSkipListMap,它們的原理都是一樣的,這里就不再贅述了。
(四)Queue
Java 并發包里面 Queue 這類并發容器是最復雜的,你可以從以下兩個維度來分類。一個維度是阻塞與非阻塞,所謂阻塞指的是當隊列已滿時,入隊操作阻塞;當隊列已空時,出隊操作阻塞。另一個維度是單端與雙端,單端指的是只能隊尾入隊,隊首出隊;而雙端指的是隊首隊尾皆可入隊出隊。Java 并發包里阻塞隊列都用 Blocking 關鍵字標識,單端隊列使用 Queue 標識,雙端隊列使用 Deque 標識。
這兩個維度組合后,可以將 Queue 細分為四大類,分別是:
1.單端阻塞隊列:其實現有 ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue、LinkedTransferQueue、PriorityBlockingQueue 和 DelayQueue。內部一般會持有一個隊列,這個隊列可以是數組(其實現是 ArrayBlockingQueue)也可以是鏈表(其實現是 LinkedBlockingQueue);甚至還可以不持有隊列(其實現是 SynchronousQueue),此時生產者線程的入隊操作必須等待消費者線程的出隊操作。而 LinkedTransferQueue 融合 LinkedBlockingQueue 和 SynchronousQueue 的功能,性能比 LinkedBlockingQueue 更好;PriorityBlockingQueue 支持按照優先級出隊;DelayQueue 支持延時出隊。
?2.雙端阻塞隊列:其實現是 LinkedBlockingDeque。
3.單端非阻塞隊列:其實現是 ConcurrentLinkedQueue。
4.雙端非阻塞隊列:其實現是 ConcurrentLinkedDeque。
另外,使用隊列時,需要格外注意隊列是否支持有界(所謂有界指的是內部的隊列是否有容量限制)。實際工作中,一般都不建議使用無界的隊列,因為數據量大了之后很容易導致 OOM。上面我們提到的這些 Queue 中,只有 ArrayBlockingQueue 和 LinkedBlockingQueue 是支持有界的,所以在使用其他無界隊列時,一定要充分考慮是否存在導致 OOM 的隱患。
?
總結
以上是生活随笔為你收集整理的Java并发编程实战~并发容器的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Effective Java~26. 不
- 下一篇: 联想linux笔记本评测,联想(leno