通過Java指南我們知道Java集合框架(Collection Framework)如何為並發服務,我們應該如何在單線程和多線程中使用集合(Collection)。
話題有點高端,我們不是很好理解。所以,我會盡可能的描述的簡單點。通過這篇指南,你將會對Java集合由更深入的了解,而且我敢保證,這會對你的日常編碼非常有用。
1. 為什么大多數的集合類不是線程安全的?
你注意到了嗎?為什么多數基本集合實現類都不是線程安全的?比如:ArrayList, LinkedList, HashMap, HashSet, TreeMap, TreeSet等等。事實上,所有的集合類(除了Vector和HashTable以外)在java.util包中都不是線程安全的,只遺留了兩個實現類(Vector和HashTable)是線程安全的為什么?
原因是:線程安全消耗十分昂貴!
你應該知道,Vector和HashTable在Java歷史中,很早就出現了,最初的時候他們是為線程安全設計的。(如果你看了源碼,你會發現這些實現類的方法都被synchronized修飾)而且很快的他們在多線程中性能表現的非常差。如你所知的,同步就需要鎖,有鎖就需要時間來監控,所以就降低了性能。
這就是為什么新的集合類沒有提供並發控制,為了保證在單線程中提供最大的性能。
下面測試的程序驗證了Vector和ArrayList的性能,兩個相似的集合類(Vector是線程安全,ArrayList非線程安全)
import java.util.*;
/**
* This test program compares performance of Vector versus ArrayList
* @author www.codejava.net
*
*/
public class CollectionsThreadSafeTest {
public void testVector() {
long startTime = System.currentTimeMillis();
Vector<Integer> vector = new Vector<>();
for (int i = 0; i < 10_000_000; i++) {
vector.addElement(i);
}
long endTime = System.currentTimeMillis();
long totalTime = endTime - startTime;
System.out.println("Test Vector: " + totalTime + " ms");
}
public void testArrayList() {
long startTime = System.currentTimeMillis();
List<Integer> list = new ArrayList<>();
for (int i = 0; i < 10_000_000; i++) {
list.add(i);
}
long endTime = System.currentTimeMillis();
long totalTime = endTime - startTime;
System.out.println("Test ArrayList: " + totalTime + " ms");
}
public static void main(String[] args) {
CollectionsThreadSafeTest tester = new CollectionsThreadSafeTest();
tester.testVector();
tester.testArrayList();
}
}
通過為每個集合添加1000萬個元素來測試性能,結果如下:
Test Vector: 9266 ms
Test ArrayList: 4588 ms
如你所看到的,在相當大的數據操作下,ArrayList速度差不多是Vector的2倍。你也拷貝上述代碼自己感受下。
2.快速失敗迭代器(Fail-Fast Iterators)
在使用集合的時候,你也要了解到迭代器的並發策略:Fail-Fast Iterators
看下以后代碼片段,遍歷一個String類型的集合:
List<String> listNames = Arrays.asList("Tom", "Joe", "Bill", "Dave", "John");
Iterator<String> iterator = listNames.iterator();
while (iterator.hasNext()) {
String nextName = iterator.next();
System.out.println(nextName);
}
這里我們使用了Iterator來遍歷list中的元素,試想下listNames被兩個線程共享:一個線程執行遍歷操作,在還沒有遍歷完成的時候,第二線程進行修改集合操作(添加或者刪除元素),你猜測下這時候會發生什么?
遍歷集合的線程會立刻拋出異常“ConcurrentModificationException”,所以稱之為:快速失敗迭代器(隨便翻的哈,沒那么重要,理解就OK)
為什么迭代器會如此迅速的拋出異常?
因為當一個線程在遍歷集合的時候,另一個在修改遍歷集合的數據會非常的危險:集合可能在修改后,有更多元素了,或者減少了元素又或者一個元素都沒有了。所以在考慮結果的時候,選擇拋出異常。而且這應該盡可能早的被發現,這就是原因。(反正這個答案不是我想要的~)
下面這段代碼演示了拋出:ConcurrentModificationException
import java.util.*;
/**
* This test program illustrates how a collection's iterator fails fast
* and throw ConcurrentModificationException
* @author www.codejava.net
*
*/
public class IteratorFailFastTest {
private List<Integer> list = new ArrayList<>();
public IteratorFailFastTest() {
for (int i = 0; i < 10_000; i++) {
list.add(i);
}
}
public void runUpdateThread() {
Thread thread1 = new Thread(new Runnable() {
public void run() {
for (int i = 10_000; i < 20_000; i++) {
list.add(i);
}
}
});
thread1.start();
}
public void runIteratorThread() {
Thread thread2 = new Thread(new Runnable() {
public void run() {
ListIterator<Integer> iterator = list.listIterator();
while (iterator.hasNext()) {
Integer number = iterator.next();
System.out.println(number);
}
}
});
thread2.start();
}
public static void main(String[] args) {
IteratorFailFastTest tester = new IteratorFailFastTest();
tester.runIteratorThread();
tester.runUpdateThread();
}
}
如你所見,在thread1遍歷list的時候,thread2執行了添加元素的操作,這時候異常被拋出。
需要注意的是,使用iterator遍歷list,快速失敗的行為是為了讓我更早的定位問題所在。我們不應該依賴這個來捕獲異常,因為快速失敗的行為是沒有保障的。這意味着如果拋出異常了,程序應該立刻終止行為而不是繼續執行。
現在你應該了解到了ConcurrentModificationException是如何工作的,而且最好是避免它。
同步封裝器
至此我們明白了,為了確保在單線程環境下的性能最大化,所以基礎的集合實現類都沒有保證線程安全。那么如果我們在多線程環境下如何使用集合呢?
當然我們不能使用線程不安全的集合在多線程環境下,這樣做會導致出現我們期望的結果。我們可以手動自己添加synchronized代碼塊來確保安全,但是使用自動線程安全的線程比我們手動更為明智。
你應該已經知道,Java集合框架提供了工廠方法創建線程安全的集合,這些方法的格式如下:
Collections.synchronizedXXX(collection)
這個工廠方法封裝了指定的集合並返回了一個線程安全的集合。XXX可以是Collection、List、Map、Set、SortedMap和SortedSet的實現類。比如下面這段代碼創建了一個線程安全的列表:
List<String> safeList = Collections.synchronizedList(new ArrayList<>());
如果我們已經擁有了一個線程不安全的集合,我們可以通過以下方法來封裝成線程安全的集合:
Map<Integer, String> unsafeMap = new HashMap<>();
Map<Integer, String> safeMap = Collections.synchronizedMap(unsafeMap);
如你鎖看到的,工廠方法封裝指定的集合,返回一個線程安全的結合。事實上接口基本都一直,只是實現上添加了synchronized來實現。所以被稱之為:同步封裝器。后面集合的工作都是由這個封裝類來實現。
提示:
在我們使用iterator來遍歷線程安全的集合對象的時候,我們還是需要添加synchronized字段來確保線程安全,因為Iterator本身並不是線程安全的,請看代碼如下:
List<String> safeList = Collections.synchronizedList(new ArrayList<>());
// adds some elements to the list
Iterator<String> iterator = safeList.iterator();
while (iterator.hasNext()) {
String next = iterator.next();
System.out.println(next);
}
事實上我們應該這樣來操作:
synchronized (safeList) {
while (iterator.hasNext()) {
String next = iterator.next();
System.out.println(next);
}
}
同時提醒下,Iterators也是支持快速失敗的。
盡管經過類的封裝可保證線程安全,但是他們依然有着自己的缺點,具體見下面部分。
並發集合
一個關於同步集合的缺點是,用集合的本身作為鎖的對象。這意味着,在你遍歷對象的時候,這個對象的其他方法已經被鎖住,導致其他的線程必須等待。其他的線程無法操作當前這個被鎖的集合,只有當執行的線程釋放了鎖。這會導致開銷和性能較低。
這就是為什么jdk1.5+以后提供了並發集合的原因,因為這樣的集合性能更高。並發集合類並放在java.util.concurrent包下,根據三種安全機制被放在三個組中。
-
第一種為:寫時復制集合:這種集合將數據放在一成不變的數組中;任何數據的改變,都會重新創建一個新的數組來記錄值。這種集合被設計用在,讀的操作遠遠大於寫操作的情景下。有兩個如下的實現類:CopyOnWriteArrayList 和 CopyOnWriteArraySet.
需要注意的是,寫時復制集合不會拋出ConcurrentModificationException異常。因為這些集合是由不可變數組支持的,Iterator遍歷值是從不可變數組中出來的,不用擔心被其他線程修改了數據。 -
第二種為:比對交換集合也稱之為CAS(Compare-And-Swap)集合:這組線程安全的集合是通過CAS算法實現的。CAS的算法可以這樣理解:
為了執行計算和更新變量,在本地拷貝一份變量,然后不通過獲取訪問來執行計算。當准備好去更新變量的時候,他會跟他之前的開始的值進行比較,如果一樣,則更新值。
如果不一樣,則說明應該有其他的線程已經修改了數據。在這種情況下,CAS線程可以重新執行下計算的值,更新或者放棄。使用CAS算法的集合有:ConcurrentLinkedQueue and ConcurrentSkipListMap.
需要注意的是,CAS集合具有不連貫的iterators,這意味着自他們創建之后並不是所有的改變都是從新的數組中來。同時他也不會拋出ConcurrentModificationException異常。 -
第三種為:這種集合采用了特殊的對象鎖(java.util.concurrent.lock.Lock):這種機制相對於傳統的來說更為靈活,可以如下理解:
這種鎖和經典鎖一樣具有基本的功能,但還可以再特殊的情況下獲取:如果當前沒有被鎖、超時、線程沒有被打斷。
不同於synchronization的代碼,當方法在執行,Lock鎖一直會被持有,直到調用unlock方法。有些實現通過這種機制把集合分為好幾個部分來提供並發性能。比如:LinkedBlockingQueue,在隊列的開后和結尾,所以在添加和刪除的時候可以同時進行。
其他使用了這種機制的集合有:ConcurrentHashMap 和絕多數實現了BlockingQueue的實現類
同樣的這一類的集合也具有不連貫的iterators,也不會拋出ConcurrentModificationException異常。
我們來總結下今天我們所學到的幾個點:
- 大部分在java.util包下的實現類都沒有保證線程安全為了保證性能的優越,除了Vector和Hashtable以外。
- 通過Collection可以創建線程安全類,但是他們的性能都比較差。
- 同步集合既保證線程安全也在給予不同的算法上保證了性能,他們都在java.util.concurrent包中。
翻譯來自:
https://www.codejava.net/java-core/collections/understanding-collections-and-thread-safety-in-java
