慕課網實戰·高併發探索(九):併發容器 J.U.C - 線程安全的集合與Map

特別感謝:慕課網jimin老師的《Java併發編程與高併發解決方案》課程,以下知識點多數來自老師的課程內容。
jimin老師課程地址:Java併發編程與高併發解決方案


概述

Java併發容器JUC是三個單詞的縮寫。是JDK下面的一個包名。即Java.util.concurrency。
上一節我們介紹了ArrayList、HashMap、HashSet對應的同步容器保證其線程安全,這節我們介紹一下其對應的併發容器。

ArrayList –> CopyOnWriteArrayList

CopyOnWriteArrayList 寫操作時複製,當有新元素添加到集合中時,從原有的數組中拷貝一份出來,然後在新的數組上作寫操作,將原來的數組指向新的數組。整個數組的add操作都是在鎖的保護下進行的,防止併發時複製多份副本。讀操作是在原數組中進行,不需要加鎖

  • 缺點:
    1.寫操作時複製消耗內存,如果元素比較多時候,容易導致young gc 和full gc。
    2.不能用於實時讀的場景.由於複製和add操作等需要時間,故讀取時可能讀到舊值。
    能做到最終一致性,但無法滿足實時性的要求,更適合讀多寫少的場景。
    如果無法知道數組有多大,或者add,set操作有多少,慎用此類,在大量的複製副本的過程中很容易出錯。

  • 設計思想:
    1.讀寫分離
    2.最終一致性
    3.使用時另外開闢空間,防止併發衝突

  • 源碼分析

//構造方法
public CopyOnWriteArrayList(Collection<? extends E> c) {
    Object[] elements;//使用對象數組來承載數據
    if (c.getClass() == CopyOnWriteArrayList.class)
        elements = ((CopyOnWriteArrayList<?>)c).getArray();
    else {
        elements = c.toArray();
        // c.toArray might (incorrectly) not return Object[] (see 6260652)
        if (elements.getClass() != Object[].class)
            elements = Arrays.copyOf(elements, elements.length, Object[].class);
    }
    setArray(elements);
}

//添加數據方法
public boolean add(E e) {
    final ReentrantLock lock = this.lock;//使用重入鎖,保證線程安全
    lock.lock();
    try {
        Object[] elements = getArray();//獲取當前數組數據
        int len = elements.length;
        Object[] newElements = Arrays.copyOf(elements, len + 1);//複製當前數組並且擴容+1
        newElements[len] = e;//將要添加的數據放入新數組
        setArray(newElements);//將原來的數組指向新的數組
        return true;
    } finally {
        lock.unlock();
    }
}

//獲取數據方法,與普通的get沒什麼差別
private E get(Object[] a, int index) {
    return (E) a[index];
}
HashSet –> CopyOnWriteArraySet
  • 它是線程安全的,底層實現使用的是CopyOnWriteArrayList,因此它也適用於大小很小的set集合,只讀操作遠大於可變操作。因爲他需要copy整個數組,所以包括add、remove、set它的開銷相對於大一些。
  • 迭代器不支持可變的remove操作。使用迭代器遍歷的時候速度很快,而且不會與其他線程發生衝突。
  • 源碼分析:
//構造方法
public CopyOnWriteArraySet() {
    al = new CopyOnWriteArrayList<E>();//底層使用CopyOnWriteArrayList
}

//添加元素方法,基本實現原理與CopyOnWriteArrayList相同
private boolean addIfAbsent(E e, Object[] snapshot) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] current = getArray();
        int len = current.length;
        if (snapshot != current) {//添加了元素去重操作
            // Optimize for lost race to another addXXX operation
            int common = Math.min(snapshot.length, len);
            for (int i = 0; i < common; i++)
                if (current[i] != snapshot[i] && eq(e, current[i]))
                    return false;
            if (indexOf(e, current, common, len) >= 0)
                    return false;
        }
        Object[] newElements = Arrays.copyOf(current, len + 1);
        newElements[len] = e;
        setArray(newElements);
        return true;
    } finally {
        lock.unlock();
    }
}
TreeSet –> ConcurrentSkipListSet

它是JDK6新增的類,同TreeSet一樣支持自然排序,並且可以在構造的時候自己定義比較器。

  • 同其他set集合,是基於map集合的(基於ConcurrentSkipListMap),在多線程環境下,裏面的contains、add、remove操作都是線程安全的。
  • 多個線程可以安全的併發的執行插入、移除、和訪問操作。但是對於批量操作addAll、removeAll、retainAll和containsAll並不能保證以原子方式執行,原因是addAll、removeAll、retainAll底層調用的還是contains、add、remove方法,只能保證每一次的執行是原子性的,代表在單一執行操縱時不會被打斷,但是不能保證每一次批量操作都不會被打斷。在使用批量操作時,還是需要手動加上同步操作的。
  • 不允許使用null元素的,它無法可靠的將參數及返回值與不存在的元素區分開來。
  • 源碼分析:
//構造方法
public ConcurrentSkipListSet() {
    m = new ConcurrentSkipListMap<E,Object>();//使用ConcurrentSkipListMap實現
}
HashMap –> ConcurrentHashMap
  • 不允許空值,在實際的應用中除了少數的插入操作和刪除操作外,絕大多數我們使用map都是讀取操作。而且讀操作大多數都是成功的。基於這個前提,它針對讀操作做了大量的優化。因此這個類在高併發環境下有特別好的表現。
  • ConcurrentHashMap作爲Concurrent一族,其有着高效地併發操作,相比Hashtable的笨重,ConcurrentHashMap則更勝一籌了。
  • 在1.8版本以前,ConcurrentHashMap採用分段鎖的概念,使鎖更加細化,但是1.8已經改變了這種思路,而是利用CAS+Synchronized來保證併發更新的安全,當然底層採用數組+鏈表+紅黑樹的存儲結構。
  • 源碼分析:推薦參考chenssy的博文:J.U.C之Java併發容器:ConcurrentHashMap
TreeMap –> ConcurrentSkipListMap
  • 底層實現採用SkipList跳錶
  • 曾經有人用ConcurrentHashMap與ConcurrentSkipListMap做性能測試,在4個線程1.6W的數據條件下,前者的數據存取速度是後者的4倍左右。但是後者有幾個前者不能比擬的優點:
    1、Key是有序的
    2、支持更高的併發,存儲時間與線程數無關
安全共享對象策略
  • 線程限制:一個被線程限制的對象,由線程獨佔,並且只能被佔有它的線程修改
  • 共享只讀:一個共享只讀的U帝鄉,在沒有額外同步的情況下,可以被多個線程併發訪問,但是任何線程都不能修改它
  • 線程安全對象:一個線程安全的對象或者容器,在內部通過同步機制來保障線程安全,多以其他線程無需額外的同步就可以通過公共接口隨意訪問他
  • 被守護對象:被守護對象只能通過獲取特定的鎖來訪問。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章