經過Java指南咱們知道Java集合框架(Collection Framework)如何爲併發服務,咱們應該如何在單線程和多線程中使用集合(Collection)。
話題有點高端,咱們不是很好理解。因此,我會盡量的描述的簡單點。經過這篇指南,你將會對Java集合由更深刻的瞭解,並且我敢保證,這會對你的平常編碼很是有用。java
你注意到了嗎?爲何多數基本集合實現類都不是線程安全的?好比: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倍。你也拷貝上述代碼本身感覺下。安全
在使用集合的時候,你也要了解到迭代器的併發策略: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異常。
咱們來總結下今天咱們所學到的幾個點:
翻譯來自:
https://www.codejava.net/java-core/collections/understanding-collections-and-thread-safety-in-java