java容器-ConcurrentHashMap

ConcurrentHashMap

本文章源碼來自 Java 8,重點是put和get方法及涉及到相關方法

重要常量


    // map容器的最大容量
    private static final int MAXIMUM_CAPACITY = 1 << 30;

    // 默認的初始容量
    private static final int DEFAULT_CAPACITY = 16;

    // toArry 方法轉化的最大容量,超過報oom異常
    static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

    // 併發級別
    private static final int DEFAULT_CONCURRENCY_LEVEL = 16;

    // 加載因子,常說的閥值不過現在基本沒用了都是用 n-(n >> 2) 表示
    private static final float LOAD_FACTOR = 0.75f;

    // 轉化爲紅黑樹的節點數,在concurrentHashMap裏面容量必須大於64才能進行紅黑樹轉化
    static final int TREEIFY_THRESHOLD = 8;

    // 紅黑樹轉化爲鏈表的節點數
    static final int UNTREEIFY_THRESHOLD = 6;

    // 上面說的轉化樹所需最小容量
    static final int MIN_TREEIFY_CAPACITY = 64;

    // 擴容時最小轉移容量
    private static final int MIN_TRANSFER_STRIDE = 16;

    // resize 校驗碼
    private static int RESIZE_STAMP_BITS = 16;
	
	/**
     * 主要用於表初始化和擴容時的控制,各種數值有不同的含義
     * -1:      表正在初始化或者擴容
     * 其他負數: 絕對值減一,就是正在擴容的線程數
     * 正數:  表示下次擴容時的閾值,超過該值後進行擴容	  
     */
	private transient volatile int sizeCtl;

四個節點

Node:table數組中的存儲元素,即一個Node對象就代表一個鍵值對(key,value)存儲在table中
TreeNode:看名字就是知道,這裏是紅黑的節點,但是並不是直接通過TreeNode組成樹,而是包裝成TreeBins然後組裝成樹
TreeBins:用於封裝維護TreeNode,是紅黑樹的真正存放節點
ForwardingNode:僅僅用於擴容時的臨時節點

構造方法

    // 無參構造器,用默認的值進行初始化
    public ConcurrentHashMap() {
    }

    // 指定初始表長的構造器,tableSizeFor 方法總是返回2的冪次方,將在後面分析這個算法
    public ConcurrentHashMap(int initialCapacity) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException();
        int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY :
                   tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
        this.sizeCtl = cap;
    }
		
	//  指定初始容量和負載因子
    public ConcurrentHashMap(int initialCapacity, float loadFactor) {
        this(initialCapacity, loadFactor, 1);
    }

    // concurrencyLevel主要是爲了兼容1.7及之前版本,它並不是實際的併發級別
    public ConcurrentHashMap(int initialCapacity,
                             float loadFactor, int concurrencyLevel) {
        if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0)
            throw new IllegalArgumentException();
        if (initialCapacity < concurrencyLevel)   // Use at least as many bins
            initialCapacity = concurrencyLevel;   // as estimated threads
        long size = (long)(1.0 + (long)initialCapacity / loadFactor);
        int cap = (size >= (long)MAXIMUM_CAPACITY) ?
            MAXIMUM_CAPACITY : tableSizeFor((int)size);
        this.sizeCtl = cap;
    }

tableSizeFor

簡單來說 這個算法的目標是得到 2的n次方 的一個初始容量,運算過程是通過或運算讓每一位的數值都變成1然後通過加1的方式變成2的冪次方

    private static final int tableSizeFor(int c) {
		// 這裏的目的是爲了不讓 c 翻倍,假設c=8(剛好爲2的N次冪的時候不減的下面算法會讓數值翻倍)
		// c 二進制表現爲 1000,減一就變成了111,後面n+1的時候會變回1000
		// 如果1000經過運算會變成1111,加一就會變成1000 0,這樣會讓最後的數值翻倍
        int n = c - 1;
		// 爲了方便我們假設減一後的值爲1000 0001 那麼下面的運算會變成
		// 1000 0001
		// 0100 0000 =1100 0001
        n |= n >>> 1;
		// 1100 0001
		// 0011 0000 =1111 0001
        n |= n >>> 2;
		// 1111 0001
		// 0000 1111 =1111 1111
        n |= n >>> 4;
		// 因爲舉例的數字沒那麼大,下面就省略了,效果是一樣的就是讓所有位數都變成1
        n |= n >>> 8;
        n |= n >>> 16;
		// 最後返回的就是 1111 1111 + 1 = 1 0000 0000 正好是2的九次方
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }

put

主要做的事情:

  1. 檢查key/value是否爲空,如果爲空,則拋異常,否則進行2
  2. 兩次hash得到key的hash值,進行3
  3. 進入for死循環,進行4
  4. 檢查table是否初始化了,如果沒有,則調用initTable()進行初始化然後進行3,否則進行5
  5. 賦值並通過tabAt這個CAS方法判斷節點是否已經存在,爲空則插入元素break跳出進行8,否則進行6
  6. 判斷此節點是否處於擴容狀態,是則加入幫助擴容,然後進行8,否則進行7
  7. 產生hash碰撞鎖住頭結點,進行equals判斷相同則替換,不同判斷鏈表還是紅黑樹然後進行插入,進行8
  8. 插入成功後判斷是否需要轉化成樹,如果是調用treeifyBin()方法嘗試進行轉化

casTabAt、tabAt、setTabAt 均爲CAS操作不在這裏細講,其他方法將在後面分析

   public V put(K key, V value) { return putVal(key, value, false);  }
   final V putVal(K key, V value, boolean onlyIfAbsent) {
		// 驗證 key value 不爲空
        if (key == null || value == null) throw new NullPointerException();
		// 兩次hash  spread方法本身也是一次hash
        int hash = spread(key.hashCode());
        int binCount = 0;
        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh;
			// 判斷容器本身有無初始化
            if (tab == null || (n = tab.length) == 0)
                tab = initTable();
			/* 1. i 賦值爲 (n-1)& hash 可以理解爲 hash % n
			   2. tabAt 尋找tab[i]
			   3. 複製給 f
			   4. 判斷這個節點是否爲空
			 */
            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
            }
			// 判斷此節點是否處於擴容狀態,是則加入幫助擴容
            else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);
			// 如果節點非空並且是正常狀態則加鎖然後插入數據
            else {
                V oldVal = null;
				// 鎖住這個節點
                synchronized (f) {
					// 再次判斷節點是否相同(因爲多線程存在被鎖之前就被更改的可能性)
                    if (tabAt(tab, i) == f) {
                        if (fh >= 0) {
                            binCount = 1;
                            for (Node<K,V> e = f;; ++binCount) {
                                K ek;
								// hash 相等 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;
    }

initTable

private final Node<K,V>[] initTable() {
        Node<K,V>[] tab; int sc;
        while ((tab = table) == null || tab.length == 0) {
            if ((sc = sizeCtl) < 0) // 判斷當前狀態,如果正在擴容,讓出資源
                Thread.yield(); // lost initialization race; just spin
             // 否則通過CAS方式將SIZECTL設爲-1也就是擴容狀態
            else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
                try {
                    if ((tab = table) == null || tab.length == 0) {
                        int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                        @SuppressWarnings("unchecked")
                        Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                        table = tab = nt;
                        sc = n - (n >>> 2);// 閥值 0.75
                    }
                } finally {
                    sizeCtl = sc; // 設置sizeCtl 爲當前閥值
                }
                break;
            }
        }
        return tab;
    }

helpTransfer transfer等擴容機制

請允許我偷個懶,這裏寫的很不錯
https://www.jianshu.com/p/487d00afe6ca

get

主要做的事情:
1、根據key調用spread計算hash值;並根據計算出來的hash值計算出該key在table出現的位置i.
2、檢查table是否爲空;如果爲空,返回null,否則進行3
3、檢查table[i]處桶位不爲空;如果爲空,則返回null,否則進行4
4、先檢查table[i]的頭結點的key是否滿足條件,是則返回頭結點的value;否則分別根據樹、鏈表查詢。

 public V get(Object key) {
        Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
        int h = spread(key.hashCode());//兩次hash計算出hash值
        if ((tab = table) != null && (n = tab.length) > 0 &&//table不能爲null,是吧
            (e = tabAt(tab, (n - 1) & h)) != null) {//table[i]不能爲空,是吧
            if ((eh = e.hash) == h) { //檢查頭結點
                if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                    return e.val;
            }
            else if (eh < 0) //table[i]爲一顆樹
                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;
    }

小結

本文主要是對put 和 get 相關方法進行分析,目的是理解一點點大神的思路和實現方式,能學會一點點東西,有興趣的小夥伴可以自行查看此容器內的其他方法。

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