java多線程之ConcurrentHashMap源碼(JDK1.8)解析

前言

ConcurrentHashMap是線程安全的HashMap,本篇將和大家一起來分析它在JDK1.8源碼(在JDK1.6、JDK1.7、JDK1.8中,每個版本的源碼都不同)。

1、簡介

ConcurrentHashMap內部數據結構是:數組+鏈表+紅黑樹。數組中存放元素地址的空間,稱之爲bucket。每個bucket存放的是一個Node節點,下面是它的內部類,實現了Map.Entry<K,V>接口:

static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    volatile V val;
    volatile Node<K,V> next;
    //以下省略部分代碼……
}

2、構造方法

/**
 * Table initialization and resizing control.  When negative,the
 * table is being initialized or resized: -1 for initialization,
 * else -(1 + the number of active resizing threads). Otherwise,
 * when table is null, holds the initial table size to use upon
 * creation, or 0 for default. After initialization, holds the
 * next element count value upon which to resize the table.
 * Hash表的初始化和調整大小的控制標誌。
 * 小於0表示Hash表正在初始化或者擴容;
 * -1表示正在初始化,-(1+N)表示有N個線程在進行擴容
 * 否則,當表爲null時,保存創建時使用的初始化大小或者默認0;
 * 初始化後,保存的是下一次擴容的大小。
 */
private transient volatile int sizeCtl;

//默認構造方法
public ConcurrentHashMap() {}
//初始化容量
public ConcurrentHashMap(int initialCapacity) {
	if (initialCapacity < 0)
		throw new IllegalArgumentException();
	int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
			   MAXIMUM_CAPACITY :
			   tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
	//sizeCtl爲initialCapacity的兩倍
	this.sizeCtl = cap;
}
//將一個map轉換爲ConcurrentHashMap
public ConcurrentHashMap(Map<? extends K, ? extends V> m) {
	this.sizeCtl = DEFAULT_CAPACITY;
	putAll(m);
}
//使用指定的容量和負載因子初始化
public ConcurrentHashMap(int initialCapacity, float loadFactor) {
	this(initialCapacity, loadFactor, 1);
}
//使用指定的容量、負載因子、併發線程數量初始化
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;
}

3、put方法

public V put(K key, V value) {
	return putVal(key, value, false);
}

final V putVal(K key, V value, boolean onlyIfAbsent) {
	//key 或者 value爲null ,則拋出異常
	if (key == null || value == null) throw new NullPointerException();
	//計算hash值
	int hash = spread(key.hashCode());
	//bucket中保存的Node節點的長度
	int binCount = 0;
	for (Node<K,V>[] tab = table;;) {
		Node<K,V> f; int n, i, fh;
		//如果數組爲空或length長度爲0,則初始化
		if (tab == null || (n = tab.length) == 0)
			tab = initTable();
		//如果要插入的元素所在的bucket中還沒有元素
		else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
			//插入Node到當前bucket
			if (casTabAt(tab, i, null,
						 new Node<K,V>(hash, key, value, null)))
				//退出當前死循環
				break;                   // no lock when adding to empty bin
		}
		//如果hash值爲-1,則表示table數組正在擴容,則加入幫忙擴容
		else if ((fh = f.hash) == MOVED)
			tab = helpTransfer(tab, f);
		else {
			V oldVal = null;
			//使用synchronized對當前節點加鎖(鎖住的是一個bucket,體現了分段鎖思想)
			synchronized (f) {
				//再次檢測數組中當前節點是否更改,如更改,則退出當前循環再來走一遍
				if (tabAt(tab, i) == f) {
					//當前hash值大於0,則證明不是擴容,也不是紅黑樹,是鏈表
					if (fh >= 0) {
						//設置鏈表的節點數爲1
						binCount = 1;
						//遍歷整個鏈表
						for (Node<K,V> e = f;; ++binCount) {
							K ek;
							//插入的key在其中存在(hash相等、equals也相等)
							if (e.hash == hash &&
								((ek = e.key) == key ||
								 (ek != null && key.equals(ek)))) {
								oldVal = e.val;
								if (!onlyIfAbsent)
									//替換當前key的value
									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;
						//節點個數設置爲2
						binCount = 2;
						//調用紅黑樹的插入方法
						if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,value)) != null) {
							//如果找到了這個元素,則賦值了新值(onlyIfAbsent默認false)						   
							oldVal = p.val;
							if (!onlyIfAbsent)
								p.val = value;
						}
					}
				}
			}
			//如果binCount不爲0,說明成功插入了元素或者替換了元素
			if (binCount != 0) {
				// 如果鏈表元素個數達到了8,則嘗試樹化(樹的binCount設置了爲2,不會再次樹化)
				if (binCount >= TREEIFY_THRESHOLD)
					treeifyBin(tab, i);
				// 如果要插入的元素已經存在
				if (oldVal != null)
					//返回舊值,退出循環
					return oldVal;
				break;
			}
		}
	}
	//插入新值,元素個數加1(再次檢查是否要擴容)
	addCount(1L, binCount);
	//成功則返回null
	return null;
}

整體流程如下:

  1. 檢查key_value是否爲空,任意爲null,則拋出異常;
  2. 檢查數組是否初始化,如果否,則進行初始化;
  3. 檢查數組中當前元素對應的bucket有無Node節點,如果無,則嘗試插入當前元素作爲第一個Node到對應的bucket中;
  4. 如果正在擴容,則加入進去幫忙擴容;
  5. 如果數組當前bucket中有Node節點,且沒有在擴容,則鎖住當前bucket;
  6. 如果當前元素對應的bucket存儲的是鏈表,則在鏈表中進行更新或插入;
  7. 如果當前元素對應的bucket存儲的是紅黑樹,則在紅黑樹中進行更新或插入;
  8. 如果元素存在,返回舊值;
  9. 如果元素不存在,則返回null,將map的元素個數加1,並檢查是否需要再次擴容。

3.1、初始化數組

private final Node<K,V>[] initTable() {
	Node<K,V>[] tab; int sc;
	while ((tab = table) == null || tab.length == 0) {
		//如果sizeCtl<0說明正在初始化或者擴容
		if ((sc = sizeCtl) < 0)
			//讓出cpu
			Thread.yield(); // lost initialization race; just spin
		else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
			// 如果把sizeCtl原子更新爲-1成功,則當前線程進入初始化
			try {
				// 再次檢查table是否爲空,防止ABA問題
				if ((tab = table) == null || tab.length == 0) {
					//默認容量爲16
					int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
					@SuppressWarnings("unchecked")
					Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
					table = tab = nt;
					// 設置sc爲數組長度的0.75倍(寫死了)
					sc = n - (n >>> 2);
				}
			} finally {
				sizeCtl = sc;
			}
			break;
		}
	}
	return tab;
}

3.2、協助擴容

final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {
	Node<K,V>[] nextTab; int sc;
	// 如果數組不爲空,並且當前bucket第一個元素爲ForwardingNode類型(表示當前bucket已經遷移完)
	// 擴容時會把舊bucket的第一個元素置爲ForwardingNode,並讓其nextTab指向新bucket數組
	if (tab != null && (f instanceof ForwardingNode) &&
		(nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) {
		int rs = resizeStamp(tab.length);
		while (nextTab == nextTable && table == tab &&
			   (sc = sizeCtl) < 0) {
			//正在擴容
			if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
				sc == rs + MAX_RESIZERS || transferIndex <= 0)
				break;
			if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
				//遷移元素
				//新桶數組大小是舊桶數組的兩倍
				//遷移元素會鎖住當前bucket,也是分段鎖
				transfer(tab, nextTab);
				break;
			}
		}
		return nextTab;
	}
	return table;
}

4、總結

1、ConcurrentHashMap採用的鎖有分段鎖,synchronized,CAS,自旋鎖,volatile等;
2、JDK1.7中使用的是ReentrantLock,此處使用的是synchronized,說明JDK1.8中對synchronized進行了極大的優化;
3、ConcurrentHashMap中沒有threshold和loadFactor這兩個字段,而是用sizeCtl字段來控制擴容;
4、插入操作採用分段鎖的思想,使用synchronized鎖住當前bucket的第一個Node;
5、擴容過程是採用CAS控制sizeCtl這個字段來進行的。

結束語

限於篇幅,本篇只介紹了ConcurrentHashMap的put方法,此方法包含了ConcurrentHashMap的精華,主要是瞭解其本質,學習它的設計思路,這也是我們讀源碼的目的之一。

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