java.util.concurrent.ConcurrentHashMap併發哈希表源碼解析

1.爲什麼使用ConcurrentHashMap

(1)HashMap是線程不安全的:我們知道HashMap實際上封裝了一個Entry單鏈表來維護衝突值,但是如果單線程訪問,那麼通過鍵找到索引,再通過索引計算hash值找到這個單向節點鏈,然後遍歷到節點的後繼爲null,就可以結束循環遍歷了。但是在多線程訪問的情況下,可能一個線程已經遍歷到節點的後繼爲null了,其他線程繼續往節點鏈裏面插入數據,導致一個線程遍歷節點的後繼永遠不爲null,造成死循環。導致最後的CPU利用率接近100%。

(2)HashTable效率太低:HashTable採用隱式鎖Synchronized來保證線程安全,但是如果一個線程往Hash表裏放入元素或者讀取元素,那麼其他線程的讀寫將會阻塞。

(3)ConcurrentHashMap的特點:採用分段鎖的方式,讓一個hash表裏面分爲多把鎖,將一個hash表裏面的數據分爲多塊,雖然一個線程佔用其中的一個鎖的一塊區域,但是其他鎖和其他區域,其他線程也可以訪問,提高了效率,這樣鎖的細粒度更高了。

2.ConcurrentHashMap所屬包

package java.util.concurrent;

3.ConcurrentHashMap繼承與實現關係

public class ConcurrentHashMap<K, V> extends AbstractMap<K, V>
        implements ConcurrentMap<K, V>, Serializable

4.ConcurrentHashMap的結構圖


5.HashEntry節點的數據結構


    /**
     * 節點鏈中節點的數據結構
     */
    static final class HashEntry<K,V> {
		//節點對應的hash值
        final int hash;
		//節點對應的鍵
        final K key;
		//節點對應的值
        volatile V value;
		//後繼結點
        volatile HashEntry<K,V> next;
		//初始化節點的構造器
        HashEntry(int hash, K key, V value, HashEntry<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }

        /**
         * 設置節點的後繼結點
         */
        final void setNext(HashEntry<K,V> n) {
            UNSAFE.putOrderedObject(this, nextOffset, n);
        }
    }

6.Segment片段

(1)片段類的屬性和構造方法:

static final class Segment<K,V> extends ReentrantLock implements Serializable {

        private static final long serialVersionUID = 2249069246763182397L;

        /**
         * 在準備鎖定段操作之前,可能阻塞之前嘗試使用預掃描的最大次數。 
		 * 在多處理器上,使用有限數量的重試來維護在定位節點時獲取的高速緩存。
         */
        static final int MAX_SCAN_RETRIES =
            Runtime.getRuntime().availableProcessors() > 1 ? 64 : 1;

        /**
         * 可以線程間訪問的table
         */
        transient volatile HashEntry<K,V>[] table;

        /**
         * 元素的數量
         */
        transient int count;

        /**
         * 修改片段的次數
         */
        transient int modCount;

        /**
         * 臨界值,如果實際大小超過了臨界值,就使用容量*加載因子重新計算來擴容 
         */
        transient int threshold;

        /**
         * 加載因子
         */
        final float loadFactor;
		//通過加載因子,臨界值,HashEntry節點來初始化片段
        Segment(float lf, int threshold, HashEntry<K,V>[] tab) {
            this.loadFactor = lf;
            this.threshold = threshold;
            this.table = tab;
        }
}

(2)片段類的方法:

rehash() 方法:

        /**
         *將表的大小擴增,重新計算hash值,然後將指定的節點添加到新表中
         */
        @SuppressWarnings("unchecked")
        private void rehash(HashEntry<K,V> node) {
            //獲取舊錶
            HashEntry<K,V>[] oldTable = table;
	    //獲取舊錶的長度
            int oldCapacity = oldTable.length;
	    //新表的容量爲舊錶的容量的2倍
            int newCapacity = oldCapacity << 1;
	    //獲取臨界值
            threshold = (int)(newCapacity * loadFactor);
	    //構造新的HashEntry數組
            HashEntry<K,V>[] newTable =
                (HashEntry<K,V>[]) new HashEntry[newCapacity];
	    //長度掩碼
            int sizeMask = newCapacity - 1;
	    //遍歷舊錶
            for (int i = 0; i < oldCapacity ; i++) {
		//獲取舊錶的節點HashEntry
                HashEntry<K,V> e = oldTable[i];
		//如果節點不爲null
                if (e != null) {
		    //獲取節點的後繼結點next
                    HashEntry<K,V> next = e.next;
		    //通過掩碼和hash值獲取下標索引
                    int idx = e.hash & sizeMask;
		    //如果後繼結點爲null
                    if (next == null)   
			//給新表賦值節點
                        newTable[idx] = e;
                    else { //如果後繼結點不爲null
			//新表的下標索引對應的HashEntry值
                        HashEntry<K,V> lastRun = e;
			//舊下標:記錄新表的下標索引
                        int lastIdx = idx;
			//從後繼結點開始遍歷
                        for (HashEntry<K,V> last = next;
                             last != null;
                             last = last.next) {
			    //計算後繼結點的下標索引
                            int k = last.hash & sizeMask;
			    //如果後繼結點對應的下標和之前節點的下標不同
                            if (k != lastIdx) {
				//將後繼結點的下標賦值給舊下標
                                lastIdx = k;
				//將後繼結點的下標對應的HashEntry節點值賦值給舊值
                                lastRun = last;
                            }
                        }
						//給新表賦值節點
                        newTable[lastIdx] = lastRun;
                        //遍歷新表,爲了獲取後繼結點,構造每個新節點
                        for (HashEntry<K,V> p = e; p != lastRun; p = p.next) {
			    //獲取value值
                            V v = p.value;
			    //獲取hash值
                            int h = p.hash;
			    //獲取下標索引
                            int k = h & sizeMask;
			    //獲取下標索引對應的HashEntry節點值
                            HashEntry<K,V> n = newTable[k];
			    //構造新的HashEntry節點並賦值新表
                            newTable[k] = new HashEntry<K,V>(h, p.key, v, n);
                        }
                    }
                }
            }
	    //獲取節點下標索引
            int nodeIndex = node.hash & sizeMask; // add the new node
	    //設置節點的後繼結點
            node.setNext(newTable[nodeIndex]);
	    //爲新表中的下標索引設置節點值
            newTable[nodeIndex] = node;
            table = newTable;
        }


scanAndLockForPut() 方法:

/**
         * 通過key查找節點鏈中對應的節點
         */
        private HashEntry<K,V> scanAndLockForPut(K key, int hash, V value) {
			//通過指定的片段和hash值來獲取HashEntry節點
            HashEntry<K,V> first = entryForHash(this, hash);
            HashEntry<K,V> e = first;
            HashEntry<K,V> node = null;
            int retries = -1;
			//鎖是否被當前線程持有
            while (!tryLock()) {
                HashEntry<K,V> f; // to recheck first below
                if (retries < 0) {
					//如果鏈表中不存在對應的HashEntry節點
                    if (e == null) {
                        if (node == null) 
							//創建HashEntry節點 
                            node = new HashEntry<K,V>(hash, key, value, null);
                        retries = 0;
                    }
                    else if (key.equals(e.key))//如果給定的鍵等於獲取節點的鍵
                        retries = 0;
                    else//如果都不滿足,那麼就找下一個節點
                        e = e.next;
                }
                else if (++retries > MAX_SCAN_RETRIES) {//如果重試次數大於最大掃描次數
					//給線程加鎖
                    lock();
					//退出循環
                    break;
                }
                else if ((retries & 1) == 0 &&
                         (f = entryForHash(this, hash)) != first) {//如果發現鏈表的表頭不等於最開始獲取的表頭,發生變化
					//更新表頭
                    e = first = f; // re-traverse if entry changed
					//重置值retries爲-1.重新開始
                    retries = -1;
                }
            }
            return node;
        }

put() 方法:

//存入元素的操作
        final V put(K key, int hash, V value, boolean onlyIfAbsent) {
			/* 如果當前片段的鎖已經被其他線程持有了,
			 * 那麼獲取的節點node爲null
			 * 如果沒有被其他線程持有,那麼就直接獲取節點鏈中對應的節點
			 */
            HashEntry<K,V> node = tryLock() ? null :
                scanAndLockForPut(key, hash, value);
            V oldValue;
            try {
				//獲取舊錶
                HashEntry<K,V>[] tab = table;
				//通過與位運算計算出下標索引index
                int index = (tab.length - 1) & hash;
				//獲取給定表tab中指定下標index對應的頭節點first
                HashEntry<K,V> first = entryAt(tab, index);
				//遍歷節點鏈表
                for (HashEntry<K,V> e = first;;) {
					//如果節點不爲空
                    if (e != null) {
                        K k;
						//如果存在鍵相同或者存在hash值相同並且鍵也相同
                        if ((k = e.key) == key ||
                            (e.hash == hash && key.equals(k))) {
							//獲取鍵對應的節點的值
                            oldValue = e.value;
                            if (!onlyIfAbsent) {
                                e.value = value;
								//修改次數加1
                                ++modCount;
                            }
                            break;
                        }
						//找下一個節點
                        e = e.next;
                    }
                    else {
						//如果node不爲空
                        if (node != null)
							//設置node節點的後繼結點爲first
                            node.setNext(first);
                        else
                            node = new HashEntry<K,V>(hash, key, value, first);
						//節點鏈中的節點數量加1
                        int c = count + 1;
						//如果片段的容量大於了臨界值並且表的長度小於最大容量
                        if (c > threshold && tab.length < MAXIMUM_CAPACITY)
							//重新計算hash值
                            rehash(node);
                        else
							//爲指定表的指定下標設置節點值node
                            setEntryAt(tab, index, node);
						//修改的次數加1
                        ++modCount;
						//重新設置鏈表中的節點數量
                        count = c;
						//釋放對象
                        oldValue = null;
                        break;
                    }
                }
            } finally {
				//將當前線程獲取的鎖釋放
                unlock();
            }
            return oldValue;
        }

7.ConcurrentHashMap屬性字段

/**
     * 默認初始化容量爲16
     */
    static final int DEFAULT_INITIAL_CAPACITY = 16;

    /**
     * 默認的加載因子爲0.75
     */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    /**
     * 默認併發級別爲16(鎖的個數)
     */
    static final int DEFAULT_CONCURRENCY_LEVEL = 16;

    /**
     * 最大容量爲2^30次冪
     */
    static final int MAXIMUM_CAPACITY = 1 << 30;

    /**
     * 最小的分段容量爲2(需要爲2的n次冪)
     */
    static final int MIN_SEGMENT_TABLE_CAPACITY = 2;

    /**
     * 最大分成2^16段
     */
    static final int MAX_SEGMENTS = 1 << 16; // slightly conservative

    /**
     * 同步重試的次數
     */
    static final int RETRIES_BEFORE_LOCK = 2;

8.ConcurrentHashMap的構造方法

構造方法1

/**
     * 構造一個帶有默認初始化容量、默認加載因子、默認併發級別的空映射
     */
    public ConcurrentHashMap() {
        this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR, DEFAULT_CONCURRENCY_LEVEL);
    }

構造方法2

/**
     * 構造一個帶有指定容量、默認加載因子、默認併發級別的空映射
     */
    public ConcurrentHashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR, DEFAULT_CONCURRENCY_LEVEL);
    }

構造方法3

/**
     * 構造一個帶有指定容量、指定加載因子、默認併發級別的空映射
     */
    public ConcurrentHashMap(int initialCapacity, float loadFactor) {
        this(initialCapacity, loadFactor, DEFAULT_CONCURRENCY_LEVEL);
    }

構造方法4

/**
     * 通過一個指定的映射關係Map來構造一個映射
     */
    public ConcurrentHashMap(Map<? extends K, ? extends V> m) {
        this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
                      DEFAULT_INITIAL_CAPACITY),
             DEFAULT_LOAD_FACTOR, DEFAULT_CONCURRENCY_LEVEL);
        putAll(m);
    }

構造方法5

    /**
     * 構造一個指定容量、指定加載因子、指定併發級別的新映射
     */
    @SuppressWarnings("unchecked")
    public ConcurrentHashMap(int initialCapacity,
                             float loadFactor, int concurrencyLevel) {
		/* 如果加載因子小於0或者初始化容量小於0或者併發級別小於0
		 * 就拋出非法參數異常
		 */
        if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
            throw new IllegalArgumentException();
		/* 如果鎖的數量大於最大分段數
		 * 就將鎖的數量設置爲最大分段數
		 */
        if (concurrencyLevel > MAX_SEGMENTS)
            concurrencyLevel = MAX_SEGMENTS;
        // 變量sshift用於記錄ssize增加的次數
        int sshift = 0;
		//變量ssize用於記錄小於鎖的數量的最大偶數
        int ssize = 1;
        while (ssize < concurrencyLevel) {
			//sshift增加1,
            ++sshift;
			//ssize擴大爲原來的2倍
            ssize <<= 1;
        }
		//散列運算的位數
        this.segmentShift = 32 - sshift;
		//散列運算的掩碼
        this.segmentMask = ssize - 1;
		/* 如果初始化容量大於最大容量
		 * 那麼就設置初始化容量爲最大容量
		 */
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
		//初始化容量除以鎖的數量得到分段數量
        int c = initialCapacity / ssize;
		/* 如果initialCapacity/ssize能整除
		 * 那麼c*ssize =initialCapacity
		 * 如果initialCapacity/ssize不能整除有餘數
		 * 那麼就將分段數+1
		 */
        if (c * ssize < initialCapacity)
            ++c;
		//獲取最小分段數量
        int cap = MIN_SEGMENT_TABLE_CAPACITY;
        while (cap < c)
			//最小分段數量擴大爲原來的2倍且小於c
            cap <<= 1;
        //初始化數組中每個元素對應的片段Segment對象
        Segment<K,V> s0 =
            new Segment<K,V>(loadFactor, (int)(cap * loadFactor),
                             (HashEntry<K,V>[])new HashEntry[cap]);
		//初始化Segment數組
        Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize];
        UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0]
        this.segments = ss;
    }

9.ConcurrentHashMap的方法

put() 方法:

/**
     * 將指定的鍵映射到該表中指定的值
     */
    @SuppressWarnings("unchecked")
    public V put(K key, V value) {
        Segment<K,V> s;
		//如果值爲null,就拋出空指針異常
        if (value == null)
            throw new NullPointerException();
		//通過鍵生成hash值
        int hash = hash(key);
		//hash值右移片段偏移量個位,然後按位與片段掩碼
        int j = (hash >>> segmentShift) & segmentMask;
        if ((s = (Segment<K,V>)UNSAFE.getObject          // nonvolatile; recheck
             (segments, (j << SSHIFT) + SBASE)) == null) //  in ensureSegment
			 //通過索引獲取對應的片段
            s = ensureSegment(j);
		//將元素存入到片段
        return s.put(key, hash, value, false);
    }
ensureSegment() 方法:

/**
     * 通過給定的索引獲取片段
     */
    @SuppressWarnings("unchecked")
    private Segment<K,V> ensureSegment(int k) {
        final Segment<K,V>[] ss = this.segments;
        long u = (k << SSHIFT) + SBASE; // raw offset
        Segment<K,V> seg;
		//通過u獲取對應的片段,如果片段不爲null
        if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) {
			//獲取第一個片段
            Segment<K,V> proto = ss[0]; // use segment 0 as prototype
			//獲取第一個片段中表的長度
            int cap = proto.table.length;
			//獲取第一個片段中的加載因子
            float lf = proto.loadFactor;
			//獲取臨界值
            int threshold = (int)(cap * lf);
			//初始化HashEntry數組
            HashEntry<K,V>[] tab = (HashEntry<K,V>[])new HashEntry[cap];
			//通過u獲取對應的片段,如果獲取的片段爲null(第二次檢查)
            if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
                == null) { // recheck
				//構造表中的片段
                Segment<K,V> s = new Segment<K,V>(lf, threshold, tab);
                while ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
                       == null) {
                    if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s))
                        break;
                }
            }
        }
        return seg;
    }

get() 方法:

/**
     * 如果映射不包含鍵的映射,則返回指定鍵被映射到的值
     */
    public V get(Object key) {
        Segment<K,V> s;
        HashEntry<K,V>[] tab;
		//通過鍵獲取hash值
        int h = hash(key);
		/**
		 * 將hash值右移segmentShift個位
		 * 然後進行和segmentMask的按位與運算
		 */
        long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
		//通過上面計算的值u獲取片段值,如果不爲null,並且片段值對應的表也不爲null
        if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&
            (tab = s.table) != null) {
			//遍歷片段中的節點鏈HashEntry
            for (HashEntry<K,V> e = (HashEntry<K,V>) UNSAFE.getObjectVolatile
                     (tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE);
                 e != null; e = e.next) {
                K k;
				//判斷鍵存在對應表中的鍵
                if ((k = e.key) == key || (e.hash == h && key.equals(k)))
                    return e.value;
            }
        }
        return null;
    }

10.閱讀總結

(1)ConcurrentHashMap是一種採用分段鎖的方式來實現的。

(2)ConcurrentHashMap默認初始化容量爲16,默認加載因子爲0.75。

(3)ConcurrentHashMap如果初始化空間不夠,將進行擴容,擴容爲原來的2倍,重新計算hash值,並將舊元素移動到新空間。

(4)ConcurrentHashMap中的節點HashEntry本身採用單鏈表的數據結構進行實現的。






-----------------------------該源碼爲jdk1.7版本的





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