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的精华,主要是了解其本质,学习它的设计思路,这也是我们读源码的目的之一。

猜你喜欢

转载自blog.csdn.net/cool_summer_moon/article/details/106171346