【併發容器精講一、】ConcurrentHashMap

1. 磨刀不誤砍柴功 :Map簡介

Map 是個接口 他會有許多實現如下:

在這裏插入圖片描述

  • HashMap

基本介紹:

1. 用於存儲Key-Value鍵值對的集合(每一個鍵值對也叫做一個Entry)(無順序)。

2. 根據鍵的hashCode值存儲數據,大多數情況下可以直接定位到它的值。

3. 鍵key爲null的記錄至多隻允許一條,值value爲null的記錄可以有多條。

4. 非線程安全。

5. HashMap是由數組+鏈表+紅黑樹(JDK1.8後增加了紅黑樹部分,鏈表長度超過閾值(8)時會將鏈表轉換爲紅黑樹)實現的。

6. HashMap與Hashtable區別:
        Hashtable是synchronized的。
        Hashtable不可以接受爲null的鍵值(key)和值(value)

簡單例子:

public static void main(String[] args) {
        Map<String,String>  map = new HashMap();
        map.put("1","1");
        System.out.println(map.isEmpty());
        System.out.println(map.keySet());
    }
JDK版本	實現方式	節點數>=8	節點數<=6
1.8以前	數組+單向鏈表	 數組+單向鏈表	數組+單向鏈表
1.8以後	數組+單向鏈表+紅黑樹	數組+紅黑樹	數組+單向鏈表
  • Hastable
1. 和HashMap一樣,Hashtable 也是一個散列表,它存儲的內容是鍵值對(key-value)映射。
2. Hashtable 繼承於Dictionary,實現了Map、Cloneable、java.io.Serializable接口。
3. Hashtable 的函數都是同步的,這意味着它是線程安全的。它的key、value都不可以爲null。此外,Hashtable中的映射不是有序的。

很多功能他和HashMap是一致的,但是他是線程安全的

  • LinkedHashMap

基礎介紹

1. LinkedHashMap是HashMap的一個子類,它保留插入的順序,如果需要輸出的順序和輸入時的相同,那麼就選用LinkedHashMap。

2. LinkedHashMap是Map接口的哈希表和鏈接列表實現,具有可預知的迭代順序。此實現提供所有可選的映射操作,並允許使用null值和null鍵。此類不保證映射的順序,特別是它不保證該順序恆久不變。
3. LinkedHashMap實現與HashMap的不同之處在於,後者維護着一個運行於所有條目的雙重鏈接列表。此鏈接列表定義了迭代順序,該迭代順序可以是插入順序或者是訪問順序。
   注意,此實現不是同步的。如果多個線程同時訪問鏈接的哈希映射,而其中至少一個線程從結構上修改了該映射,則它必須保持外部同步。

 
  • TreeMap

基礎介紹


1. TreeMap 是一個有序的key-value集合,它是通過紅黑樹實現的。
2. TreeMap 繼承於AbstractMap,所以它是一個Map,即一個key-value集合。
3. TreeMap 實現了NavigableMap接口,意味着它支持一系列的導航方法。比如返回有序的key集合。
4. TreeMap 實現了Cloneable接口,意味着它能被克隆。
5. TreeMap 實現了java.io.Serializable接口,意味着它支持序列化。

6. TreeMap基於紅黑樹(Red-Black tree)實現。該映射根據其鍵的自然順序進行排序,或者根據創建映射時提供的 Comparator 進行排序,具體取決於使用的構造方法。
7.  TreeMap的基本操作 containsKey、get、put 和 remove 的時間複雜度是 log(n) 。
另外,TreeMap是非同步的。 它的iterator 方法返回的迭代器是fail-fastl的。

2. 爲什麼需要ConcurrentHashMap

1.我們先思考一個問題,我們爲什麼要用ConcurrentHashMap,而不用Collections.synchronizedMap() 或者 Hashtable
加了鎖對我們性能造成很大的影響
2. 爲什麼HashMap是線程不安全的呢?
同時put碰撞導致數據丟失
同時put擴容導致數據丟失
死循環造成CPU100%

3. 九層之臺,起於累土,羅馬不是一天建成的:HashMap分析

  • HashMap是應用更廣泛的哈希表實現,而且大部分情況下,都能在常數時間性能的情況下進行put和get操作。要掌握HashMap,主要從如下幾點來把握:

  • jdk1.7中底層是由數組(也有叫做“位桶”的)+鏈表實現;jdk1.8中底層是由數組+鏈表/紅黑樹實現

  • 可以存儲null鍵和null值,線程不安全 初始size爲16,擴容:newsize = oldsize*2,size一定爲2的n次冪

  • 擴容針對整個Map,每次擴容時,原來數組中的元素依次重新計算存放位置,並重新插入

  • 插入元素後才判斷該不該擴容,有可能無效擴容(插入後如果擴容,如果沒有再次插入,就會產生無效擴容)

  • 當Map中元素總數超過Entry數組的75%,觸發擴容操作,爲了減少鏈表長度,元素分配更均勻

1.7 實現圖
在這裏插入圖片描述
1.8實現圖

在這裏插入圖片描述

延伸:紅黑樹

  • 他是一個二叉查找數,一種平衡策略
    在這裏插入圖片描述
  • 左邊的值要比這個節點要小,右邊則大
  • 會自動平衡,防止極端不平衡從而影響查找效率的情況發生
  • 每個節點要們是紅色,要麼是黑色,但跟節點永遠是黑色
  • 紅色節點不能連續
  • 從任一節點到其子數中每個葉子節點的路徑都包含相同數量的黑色節點
  • 所有的葉節點都是黑色的

4. JDK1.7 中 ConcurrentHashMap 實現和分析

1.7 數據結構
在這裏插入圖片描述
1.7 的特點

  • Java 1.7 中ConcurrentHashMap最外層是多個segment,每個segment底層數據結構與HashMap類似,ren仍然是數組+鏈表的 拉鍊法
  • 每個segment獨立ReentrantLock,每個segment 互不影響,提高鏈併發效率
  • ConcurrentHashMap 有16個 segment,所以同時支持 16個線程併發寫,這個默認值可以在初始化的時候設置爲其他值,但是一旦初始化後,是不可以擴容的

5. JDK1.8 中 ConcurrentHashMap 實現和源碼分析

數據結構
在這裏插入圖片描述
ConcurrentHashMap 借鑑鏈 1.8 HashMap 實現

源碼分析

  1. put
 /**
     * Maps the specified key to the specified value in this table.
     * Neither the key nor the value can be null.
     *
     * <p>The value can be retrieved by calling the {@code get} method
     * with a key that is equal to the original key.
     *
     * @param key key with which the specified value is to be associated
     * @param value value to be associated with the specified key
     * @return the previous value associated with {@code key}, or
     *         {@code null} if there was no mapping for {@code key}
     * @throws NullPointerException if the specified key or value is null
     */
    public V put(K key, V value) {
        return putVal(key, value, false);
    }

putVal 是 put 方法和核心

/** Implementation for put and putIfAbsent */
    final V putVal(K key, V value, boolean onlyIfAbsent) {
    // 不允許key value 出現 空否則 拋出空指針異常
        if (key == null || value == null) throw new NullPointerException();
        //計算hashcode值
        int hash = spread(key.hashCode());
        int binCount = 0;
        // 用for循環進行處理  完成值的插入工作
        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh;
            //判斷 tab 是否初始化
            if (tab == null || (n = tab.length) == 0)
                tab = initTable();
                //已經被初始化 並且在這個位置是空的  執行cas操作直接放進去
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            	//cas操作把值放進去
                if (casTabAt(tab, i, null,
                             new Node<K,V>(hash, key, value, null)))
                    break;                   // no lock when adding to empty bin
            }
            // 判斷hash值是否  MOVED  MOVED代表擴容
            else if ((fh = f.hash) == MOVED)
            //幫助進行擴容
                tab = helpTransfer(tab, f);
            else {
                V oldVal = null;
                //否則使用synchronized保證值線程的安全
                synchronized (f) {
                    if (tabAt(tab, i) == f) {
                        if (fh >= 0) {
                            binCount = 1;
                            //進行鏈表的操作
                            for (Node<K,V> e = f;; ++binCount) {
                                K ek;
                                //判斷當前是否存在key
                                if (e.hash == hash &&
                                    ((ek = e.key) == key ||
                                     (ek != null && key.equals(ek)))) {
                                    oldVal = e.val;
                                    if (!onlyIfAbsent)
                                        e.val = value;
                                    break;
                                }
                                //創建一個節點 放到鏈表的最後
                                Node<K,V> pred = e;
                                if ((e = e.next) == null) {
                                    pred.next = new Node<K,V>(hash, key,
                                                              value, null);
                                    break;
                                }
                            }
                        }
                        //如果走到這裏 說明是一個 紅黑樹 進行紅黑樹的操作
                        else if (f instanceof TreeBin) {
                            Node<K,V> p;
                            binCount = 2;
                            if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                           value)) != null) {
                                oldVal = p.val;
                                if (!onlyIfAbsent)
                                    p.val = value;
                            }
                        }
                    }
                }
                // 判斷添加是否完成
                if (binCount != 0) {
                // 判斷鏈表 是否滿足條件變成紅黑樹
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        addCount(1L, binCount);
        return null;
    }

在這裏插入圖片描述

  1. get
/**
     * Returns the value to which the specified key is mapped,
     * or {@code null} if this map contains no mapping for the key.
     *
     * <p>More formally, if this map contains a mapping from a key
     * {@code k} to a value {@code v} such that {@code key.equals(k)},
     * then this method returns {@code v}; otherwise it returns
     * {@code null}.  (There can be at most one such mapping.)
     *
     * @throws NullPointerException if the specified key is null
     */
    public V get(Object key) {
        Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
        //算出hash值
        int h = spread(key.hashCode());
        //判斷值
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (e = tabAt(tab, (n - 1) & h)) != null) {
            //如果槽點的hash值符合 返回val  找到值了
            if ((eh = e.hash) == h) {
                if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                    return e.val;
            }
            // 如果是負數,說明是紅黑樹節點,
            else if (eh < 0)
             //用find方法找到對應的value 
                return (p = e.find(h, key)) != null ? p.val : null;
                // 遍歷鏈表 找到值返回
            while ((e = e.next) != null) {
                if (e.hash == h &&
                    ((ek = e.key) == key || (ek != null && key.equals(ek))))
                    return e.val;
            }
        }
        return null;
    }

在這裏插入圖片描述

6. 對比1.7 與 1.8 ,爲什麼要把1.7的結構改成1.8的結構

  1. 數據結構的區別
  • Java 1.7 中ConcurrentHashMap最外層是多個segment,每個segment底層數據結構與HashMap類似 最多 16個
  • Java 1.8 使用 數組+鏈表+紅黑樹的結構 每個 Node,提高了併發性
  1. hash碰撞
  • 轉變爲 數組 + 鏈表 + 紅黑樹
  1. 保證併發安全
  • java1.8 通過 cas + synchronized 實現

個人博客地址:http://www.yxl520.cn/

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章