ConcurrentHashMap JDK1.7中結構原理及源碼分析

注:本文根據網絡和部分書籍整理基於JDK1.7書寫,如有雷同敬請諒解  歡迎指正文中的錯誤之處。

數據結構

       ConcurrentHashMap是HashMap的一個線程安全的、支持高效併發的版本。在多線程併發場景下HashTable和由同步包裝器包裝的HashMap,都是通過使用一個全局的鎖來同步不同線程間的併發訪問,會帶來不可忽視的性能問題。

      ConcurrentHashMap採用分段鎖的設計Segment + HashEntry的方式進行實現,採用table數組+單向鏈表的數據結構結構如下:

重要屬性

      initialCapacity:初始容量(默認16),這個值指的是整個 ConcurrentHashMap 的初始容量,實際操作的時候需要平均分給每個 Segment。

      concurrencyLevel:併發數(Segment 數,默認16)。ConcurrentHashMap採用了分段鎖的設計,分段鎖個數即Segment[]的數組長度,只有在同一個分段內才存在競態關係,不同的分段鎖之間沒有鎖競爭,所以當它們分別操作不同的 Segment時最多可以同時支持16個線程併發。
      注:用戶可以在構造函數中初始設置爲其他值,當用戶設置併發度時,ConcurrentHashMap會使用大於等於該值的最小2冪指數作爲實際併發度(如用戶設置併發度爲17,實際併發度則爲32),但是一旦初始化以後,它是不可以擴容的。
不能超過規定的最大值:
final int MAX_SEGMENTS = 1 << 16

      segmentsSegment 通過繼承 ReentrantLock 來進行加鎖,每次需要加鎖的操作鎖住的是一個 segment,每個 Segment 對象用來守護其(成員對象 table 中)包含的若干個桶,這樣保證每個 Segment 是線程安全的,也就實現了全局的線程安全。
      注:JDK7中除了第一個Segment之外,剩餘的Segment採用的是延遲初始化的機制:每次put之前都需要檢查key對應的Segment是否爲null,如果是則調用ensureSegment()以確保對應的Segment被創建。Segment[i] 的默認大小爲 2,負載因子是 0.75

      table: 由 HashEntry 對象組成的數組。table 數組的每一個數組成員就是散列映射表的一個桶。

      count計數器,它表示每個 Segment 對象管理的 table 數組(若干個 HashEntry 組成的鏈表)包含的 HashEntry 對象的個數。每一個 Segment 對象都有一個 count 對象來表示本 Segment 中包含的 HashEntry 對象的總數。
      注:之所以在每個 Segment 對象中包含一個計數器,而不在 ConcurrentHashMap 中使用全局的計數器,是爲了避免出現“熱點域”而影響 ConcurrentHashMap 的併發性。需要更新計數器時,不用鎖定整個 ConcurrentHashMap。

      loadFactor:負載因子(默認0.75),之前我們說了,Segment 數組不可以擴容,所以這個負載因子是給每個 Segment 內部使用的。

put操作

      1、判斷value是否爲null,如果爲null,直接拋出異常。注:不允許key或者value爲null

      2、通過哈希算法定位到Segment(key通過一次hash運算得到一個hash值,將得到hash值向右按位移動segmentShift位,然後再與segmentMask做&運算得到segment的索引j)。

      3、使用Unsafe的方式從Segment數組中獲取該索引對應的Segment對象

      4、向這個Segment對象中put值

      注:對共享變量進行寫入操作爲了線程安全,在操作共享變量時必須得加鎖,持有段鎖(鎖定整個segment)的情況下執行的。修改數據是不能併發進行的

      判斷該值的插入是否會導致該 segment 的元素個數超過閾值,以確保容量不足時能夠rehash擴容,再插值

      注:rehash 擴容 segment 數組不能擴容,擴容的是 segment 數組某個位置內部的數組 HashEntry[] 擴容爲原來的 2 倍。先進行擴容,再插值

      查找是否存在同樣一個key的結點,存在直接替換這個結點的值。否則創建一個新的結點並添加到hash鏈的頭部,修改modCount和count的值,修改count的值一定要放在最後一步。

final V put(K key, int hash, V value, boolean onlyIfAbsent) {
	HashEntry<K,V> node = tryLock() ? null : scanAndLockForPut(key, hash, value);
	V oldValue;
	try {
		HashEntry<K,V>[] tab = table;
		int index = (tab.length - 1) & hash;
		HashEntry<K,V> first = entryAt(tab, index);
		for (HashEntry<K,V> e = first;;) {
			if (e != null) {
				K k;
				if ((k = e.key) == key ||
					(e.hash == hash && key.equals(k))) {
					oldValue = e.value;
					if (!onlyIfAbsent) {
						e.value = value;
						++modCount;
					}
					break;
				}
				e = e.next;
			}
			else {
				if (node != null)
					node.setNext(first);
				else
					node = new HashEntry<K,V>(hash, key, value, first);
				int c = count + 1;
				if (c > threshold && tab.length < MAXIMUM_CAPACITY)
					rehash(node);
				else
					setEntryAt(tab, index, node);
				++modCount;
				count = c;
				oldValue = null;
				break;
			}
		}
	} finally {
		unlock();
	}
	return oldValue;
}

get操作

    1、計算 hash 值,找到 segment 數組中的具體位置,使用Unsafe獲取對應的Segment

    2、根據 hash 找到數組中具體的位置

    3、從鏈表頭開始遍歷整個鏈表(因爲Hash可能會有碰撞,所以用一個鏈表保存),如果找到對應的key,則返回對應的value值,否則返回null。

    注:get操作不需要鎖由於其中涉及到的共享變量都使用volatile修飾,volatile可以保證內存可見性,所以不會讀取到過期數據。

/**
 * 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) {
	Segment<K,V> s; // manually integrate access methods to reduce overhead
	HashEntry<K,V>[] tab;
	int h = hash(key);
	long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
	if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&
		(tab = s.table) != null) {
		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;
}

size()操作

     size操作需要遍歷所有的Segment才能算出整個Map的大小。先採用不加鎖的方式,循環所有的Segment(通過Unsafe的getObjectVolatile()以保證原子讀語義)連續計算元素的個數,最多計算3次:

      1、如果前後兩次計算結果相同,則說明計算出來的元素個數是準確的;
     
2、如果前後兩次計算結果都不同,則給每個Segment進行加鎖,再計算一次元素的個數;
     注:在put , remove和clean方法裏操作元素前都會將變量modCount進行加1,那麼在統計size前後比較modCount是否發生變化,從而得知容器的大小是否發生變化。

/**
 * Returns the number of key-value mappings in this map.  If the
 * map contains more than <tt>Integer.MAX_VALUE</tt> elements, returns
 * <tt>Integer.MAX_VALUE</tt>.
 *
 * @return the number of key-value mappings in this map
 */
public int size() {
	// Try a few times to get accurate count. On failure due to
	// continuous async changes in table, resort to locking.
	final Segment<K,V>[] segments = this.segments;
	int size;
	boolean overflow; // true if size overflows 32 bits
	long sum;         // sum of modCounts
	long last = 0L;   // previous sum
	int retries = -1; // first iteration isn't retry
	try {
		for (;;) {
			if (retries++ == RETRIES_BEFORE_LOCK) {
				for (int j = 0; j < segments.length; ++j)
					ensureSegment(j).lock(); // force creation
			}
			sum = 0L;
			size = 0;
			overflow = false;
			for (int j = 0; j < segments.length; ++j) {
				Segment<K,V> seg = segmentAt(segments, j);
				if (seg != null) {
					sum += seg.modCount;
					int c = seg.count;
					if (c < 0 || (size += c) < 0)
						overflow = true;
				}
			}
			if (sum == last)
				break;
			last = sum;
		}
	} finally {
		if (retries > RETRIES_BEFORE_LOCK) {
			for (int j = 0; j < segments.length; ++j)
				segmentAt(segments, j).unlock();
		}
	}
	return overflow ? Integer.MAX_VALUE : size;
}

 

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