前面的文章中詳細介紹了Java的容器框架,在此基礎上,本文對Java中的同步容器與併發容器做一些介紹。
fail-fast機制
快速報錯機制(fail-fast)能夠防止多個進程同時修改同一個容器的內容。如果在你迭代遍歷某個容器的過程中,另一個進程接入其中,並且插入、刪除或者修改此容器內的某個對象,就會出現問題:也許迭代過程已經處理過容器中的該元素了,也許還沒處理,也許在調用size()之後尺寸縮小了等等。fail-fast機制會探查容器上的任何除了你的進程所進行的操作以外的所有變化,一旦它發現其他進程修改了容器,立刻拋出ConcurrentModificationException異常,即快速報錯——不適用複雜的算法在時候進行檢查。
同步容器
同步容器可以簡單地理解爲通過synchronized來實現同步的容器,如果有多個線程調用同步容器的方法,它們將會串行執行。
同步容器將它們的狀態封裝起來,並對每一個公有方法進行同步。主要包括:
- Vector
- Stack
- HashTable
- Collections.synchronized方法生成,例如:
- Collectinons.synchronizedList()
- Collections.synchronizedSet()
- Collections.synchronizedMap()
- Collections.synchronizedSortedSet()
- Collections.synchronizedSortedMap()
其中Vector(同步的ArrayList)和Stack(繼承自Vector,先進後出)、HashTable(繼承自Dictionary,實現了Map接口)是比較老的容器,Thinking in Java中明確指出,這些容器現在仍然存在於JDK中是爲了向以前老版本的程序兼容,在新的程序中不應該在使用。Collections的方法時將非同步的容器包裹生成對應的同步容器。
同步容器在單線程的環境下能夠保證線程安全,但是通過synchronized同步方法將訪問操作串行化,導致併發環境下效率低下。而且同步容器在多線程環境下的複合操作(迭代、條件運算如沒有則添加等)是非線程安全,需要客戶端代碼來實現加鎖。
代碼示例:
public static Object getLast(Vector list) {
int lastIndex = list.size() - 1;
return list.get(lastIndex);
}
public static void deleteLast(Vector list) {
int lastIndex = list.size() - 1;
list.remove(lastIndex);
}
上面的代碼取最後一個元素或者刪除最後一個元素,使用了同步容器Vector。如果有兩個線程A,B同時調用上面的兩個方法,假設list的大小爲10,這裏計算得到的lastIndex爲9,線程B首先執行了刪除操作(多線程之間操作執行的不確定性導致),而後線程A調用了list.get方法,這時就會發生數組越界異常,導致問題的原因就是上面的複合操作不是原子操作,這裏可以通過在方法內部額外的使用list對象鎖來實現原子操作。
在多線程中使用同步容器,如果使用Iterator迭代容器或使用使用for-each遍歷容器,在迭代過程中修改容器會拋出ConcurrentModificationException異常。想要避免出現ConcurrentModificationException,就必須在迭代過程持有容器的鎖。但是若容器較大,則迭代的時間也會較長。那麼需要訪問該容器的其他線程將會長時間等待。從而會極大降低性能。
此外,隱式迭代的情況,如toString,hashCode,equalse,containsAll,removeAll,retainAll等方法都會隱式的Iterate,也可能拋出ConcurrentModificationException。
併發容器
由上面的分析我們知道,同步容器並不能保證多線程安全,而併發容器是針對多個線程併發訪問而設計的,在jdk5.0引入了concurrent包,其中提供了很多併發容器,極大的提升同步容器類的性能。
ConcurrentHashMap
- 對應的非併發容器:HashMap
- 目標:代替Hashtable、synchronizedMap,支持複合操作
- 原理:JDK6中採用一種更加細粒度的加鎖機制Segment“分段鎖”,JDK8中採用CAS無鎖算法,詳細分析推薦閱讀【JDK】:ConcurrentHashMap高併發機制——【轉載】。
CopyOnWriteArrayList
- 對應的非併發容器:ArrayList
- 目標:代替Vector、synchronizedList
- 原理:利用高併發往往是讀多寫少的特性,對讀操作不加鎖,對寫操作,先複製一份新的集合,在新的集合上面修改,然後將新集合賦值給舊的引用,並通過volatile 保證其可見性,當然寫操作的鎖是必不可少的了。
關於這一部分可參考【JDK】:CopyOnWriteArrayList、CopyOnWriteArraySet 源碼解析
CopyOnWriteArraySet
- 對應的費併發容器:HashSet
- 目標:代替synchronizedSet
- 原理:基於CopyOnWriteArrayList實現,其唯一的不同是在add時調用的是CopyOnWriteArrayList的addIfAbsent方法,其遍歷當前Object數組,如Object數組中已有了當前元素,則直接返回,如果沒有則放入Object數組的尾部,並返回。
關於這一部分可參考【JDK】:CopyOnWriteArrayList、CopyOnWriteArraySet 源碼解析
ConcurrentSkipListMap
- 對應的非併發容器:TreeMap
- 目標:代替synchronizedSortedMap(TreeMap)
- 原理:Skip list(跳錶)是一種可以代替平衡樹的數據結構,默認是按照Key值升序的。Skip list讓已排序的數據分佈在多層鏈表中,以0-1隨機數決定一個數據的向上攀升與否,通過”空間來換取時間”的一個算法。ConcurrentSkipListMap提供了一種線程安全的併發訪問的排序映射表。內部是SkipList(跳錶)結構實現,在理論上能夠在O(log(n))時間內完成查找、插入、刪除操作。
ConcurrentSkipListSet
- 對應的非併發容器:TreeSet
- 目標:代替synchronizedSortedSet
- 原理:內部基於ConcurrentSkipListMap實現
ConcurrentLinkedQueue
不會阻塞的隊列
- 對應的非併發容器:Queue
- 原理:基於鏈表實現的FIFO隊列(LinkedList的併發版本)
LinkedBlockingQueue、ArrayBlockingQueue、PriorityBlockingQueue
- 對應的非併發容器:BlockingQueue
- 特點:拓展了Queue,增加了可阻塞的插入和獲取等操作
- 原理:通過ReentrantLock實現線程安全,通過Condition實現阻塞和喚醒
- 實現類:
- LinkedBlockingQueue:基於鏈表實現的可阻塞的FIFO隊列
- ArrayBlockingQueue:基於數組實現的可阻塞的FIFO隊列
- PriorityBlockingQueue:按優先級排序的隊列
參考:
1、Java併發:同步容器&併發容器
2、Java併發:線程安全的容器-同步和併發