Java中的集合和線程安全

經過Java指南咱們知道Java集合框架(Collection Framework)如何爲併發服務,咱們應該如何在單線程和多線程中使用集合(Collection)。
話題有點高端,咱們不是很好理解。因此,我會盡量的描述的簡單點。經過這篇指南,你將會對Java集合由更深刻的瞭解,並且我敢保證,這會對你的平常編碼很是有用。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異常。

咱們來總結下今天咱們所學到的幾個點:

  1. 大部分在java.util包下的實現類都沒有保證線程安全爲了保證性能的優越,除了Vector和Hashtable之外。
  2. 經過Collection能夠建立線程安全類,可是他們的性能都比較差。
  3. 同步集合既保證線程安全也在給予不一樣的算法上保證了性能,他們都在java.util.concurrent包中。 

翻譯來自:
https://www.codejava.net/java-core/collections/understanding-collections-and-thread-safety-in-java

相關文章
相關標籤/搜索