前言
面试中常常问到Hashtable、HashMap和ConcurrentHashMap的区别。大家都知道HashMap是线程不安全的,Hashtable和ConcurrentHashMap是线程安全的。Hashtable保证线程安全的方法,基本都是在操作集合的方法上加synchronized关键字,我们有必要知道ConcurrentHashMap底层实现和如何保证线程安全性。ConcurrentHashMap引入了分段锁的概念,对于不同Bucket的操作不需要全局锁来保证线程安全。
建议学习ConcurrentHashMap之前,先学习Hashtable和HashMap,并且要有一些并发的基础。
在看源码的时候一定要注意,本文讲的是JDK1.8的源码,ConcurrentHashMap在JDK1.8相比之前的版本有很大变化,1.8版本的源码达到6000+行,而1.6的源码只有一千多行。
ConcurrentHashMap相比HashMap而言,是多线程安全的,其底层数据与HashMap的数据结构相同,数据结构如下:
在JDK1.6的实现中,使用的是一个segments数组来存储。
final Segment<K,V>[] segments;
JDK1.8中是使用Node数组来存储。
transient volatile Node<K,V>[] table;
JDK1.8中,Segment类只有在序列化和反序列化时才会被用到。
ConcurrentHashMap数据结构
其底层数据结构实现如下。
可以看到,桶中的结构可能是链表,也可能是红黑树,红黑树是为了提高查找效率。
ConcurrentHashMap的继承关系
public class ConcurrentHashMap<K,V> extends AbstractMap<K,V>
implements ConcurrentMap<K,V>, Serializable {}
其内部框架图如下。
1. Node类
Node类主要用于存储具体键值对,其子类有ForwardingNode、ReservationNode、TreeNode和TreeBin四个子类。四个子类具体的代码在之后的具体例子中进行分析讲解。
2. Traverser类
Traverser类主要用于遍历操作,其子类有BaseIterator、KeySpliterator、ValueSpliterator、EntrySpliterator四个类,BaseIterator用于遍历操作。KeySplitertor、ValueSpliterator、EntrySpliterator则用于键、值、键值对的划分。
3. CollectionView类
CollectionView抽象类主要定义了视图操作,其子类KeySetView、ValueSetView、EntrySetView分别表示键视图、值视图、键值对视图。对视图均可以进行操作。
4. Segment类
Segment类在JDK1.8中与之前的版本的JDK作用存在很大的差别,JDK1.8下,其在普通的ConcurrentHashMap操作中已经没有失效,其在序列化与反序列化的时候会发挥作用。
5. CounterCell
CounterCell类主要用于对baseCount的计数。
ConcurrentHashMap为什么高效?
Hashtable低效主要是因为所有访问Hashtable的线程都争夺一把锁。如果容器有很多把锁,每一把锁控制容器中的一部分数据,那么当多个线程访问容器里的不同部分的数据时,线程之前就不会存在锁的竞争,这样就可以有效的提高并发的访问效率。
这也正是ConcurrentHashMap使用的分段锁技术。将ConcurrentHashMap容器的数据分段存储,每一段数据分配一个Segment(锁),当线程占用其中一个Segment时,其他线程可正常访问其他段数据。
/**
* 在先前版本中使用的精简版辅助类,为了序列化兼容性而声明
*/
static class Segment<K,V> extends ReentrantLock implements Serializable {
private static final long serialVersionUID = 2249069246763182397L;
final float loadFactor;
Segment(float lf) { this.loadFactor = lf; }
}
类的属性
public class ConcurrentHashMap<K,V> extends AbstractMap<K,V>
implements ConcurrentMap<K,V>, Serializable {
private static final long serialVersionUID = 7249069246763182397L;
// 表的最大容量
private static final int MAXIMUM_CAPACITY = 1 << 30;
// 默认表的大小
private static final int DEFAULT_CAPACITY = 16;
// 最大数组大小
static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
// 默认并发数
private static final int DEFAULT_CONCURRENCY_LEVEL = 16;
// 装载因子
private static final float LOAD_FACTOR = 0.75f;
// 转化为红黑树的阈值
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;
// 生成sizeCtl所使用的bit位数
private static int RESIZE_STAMP_BITS = 16;
// 进行扩容所允许的最大线程数
private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;
// 记录sizeCtl中的大小所需要进行的偏移位数
private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;
// 一系列的标识
static final int MOVED = -1; // hash for forwarding nodes
static final int TREEBIN = -2; // hash for roots of trees
static final int RESERVED = -3; // hash for transient reservations
static final int HASH_BITS = 0x7fffffff; // usable bits of normal node hash
//
/** Number of CPUS, to place bounds on some sizings */
// 获取可用的CPU个数
static final int NCPU = Runtime.getRuntime().availableProcessors();
//
/** For serialization compatibility. */
// 进行序列化的属性
private static final ObjectStreamField[] serialPersistentFields = {
new ObjectStreamField("segments", Segment[].class),
new ObjectStreamField("segmentMask", Integer.TYPE),
new ObjectStreamField("segmentShift", Integer.TYPE)
};
// 表
transient volatile Node<K,V>[] table;
// 下一个表
private transient volatile Node<K,V>[] nextTable;
//
/**
* Base counter value, used mainly when there is no contention,
* but also as a fallback during table initialization
* races. Updated via CAS.
*/
// 基本计数
private transient volatile long baseCount;
//
/**
* 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.
*/
// 对表初始化和扩容控制
private transient volatile int sizeCtl;
/**
* The next table index (plus one) to split while resizing.
*/
// 扩容下另一个表的索引
private transient volatile int transferIndex;
/**
* Spinlock (locked via CAS) used when resizing and/or creating CounterCells.
*/
// 旋转锁
private transient volatile int cellsBusy;
/**
* Table of counter cells. When non-null, size is a power of 2.
*/
// counterCell表
private transient volatile CounterCell[] counterCells;
// views
// 视图
private transient KeySetView<K,V> keySet;
private transient ValuesView<K,V> values;
private transient EntrySetView<K,V> entrySet;
// Unsafe mechanics 说明:ConcurrentHashMap的属性很多,其中不少属性在HashMap中就已经介绍过,而对于ConcurrentHashMap而言,添加了Unsafe实例,主要用于反射获取对象相应的字段。
private static final sun.misc.Unsafe U;
private static final long SIZECTL;
private static final long TRANSFERINDEX;
private static final long BASECOUNT;
private static final long CELLSBUSY;
private static final long CELLVALUE;
private static final long ABASE;
private static final int ASHIFT;
static {
try {
U = sun.misc.Unsafe.getUnsafe();
Class<?> k = ConcurrentHashMap.class;
SIZECTL = U.objectFieldOffset
(k.getDeclaredField("sizeCtl"));
TRANSFERINDEX = U.objectFieldOffset
(k.getDeclaredField("transferIndex"));
BASECOUNT = U.objectFieldOffset
(k.getDeclaredField("baseCount"));
CELLSBUSY = U.objectFieldOffset
(k.getDeclaredField("cellsBusy"));
Class<?> ck = CounterCell.class;
CELLVALUE = U.objectFieldOffset
(ck.getDeclaredField("value"));
Class<?> ak = Node[].class;
ABASE = U.arrayBaseOffset(ak);
int scale = U.arrayIndexScale(ak);
if ((scale & (scale - 1)) != 0)
throw new Error("data type scale not a power of two");
ASHIFT = 31 - Integer.numberOfLeadingZeros(scale);
} catch (Exception e) {
throw new Error(e);
}
}
}
ConcurrentHashMap的用到的重要内部类
Node类
该类是此集合最重要的一个内部类。只要是插入ConcurrentHashMap的结点,都会被包装成Node
/**
* Key-value entry. This class is never exported out as a
* user-mutable Map.Entry (i.e., one supporting setValue; see
* MapEntry below), but can be used for read-only traversals used
* in bulk tasks. Subclasses of Node with a negative hash field
* are special, and contain null keys and values (but are never
* exported). Otherwise, keys and vals are never null.
* 键值输入。 该类永远不会作为用户可变的Map.Entry导出(即,一个支持setValue;请参阅下面的MapEntry),但可以用于批量任务中使用的只读遍历。 具有负散列字段的Node的子类是特殊的,并且包含空键和值(但永远不会导出)。 否则,键和val永远不会为空。
*/
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
volatile V val;
volatile Node<K,V> next;
Node(int hash, K key, V val) {
this.hash = hash;
this.key = key;
this.val = val;
}
Node(int hash, K key, V val, Node<K,V> next) {
this(hash, key, val);
this.next = next;
}
public final K getKey() { return key; }
public final V getValue() { return val; }
public final int hashCode() { return key.hashCode() ^ val.hashCode(); }
public final String toString() {
return Helpers.mapEntryToString(key, val);
}
public final V setValue(V value) {
throw new UnsupportedOperationException(); //不允许被修改val
}
public final boolean equals(Object o) {
Object k, v, u; Map.Entry<?,?> e;
return ((o instanceof Map.Entry) &&
(k = (e = (Map.Entry<?,?>)o).getKey()) != null &&
(v = e.getValue()) != null &&
(k == key || k.equals(key)) &&
(v == (u = val) || v.equals(u)));
}
/**
* Virtualized support for map.get(); overridden in subclasses.支持map的get方法,通过hashcode和Key来获取一个node结点
*/
Node<K,V> find(int h, Object k) {
Node<K,V> e = this;
if (k != null) {
do {
K ek;
if (e.hash == h &&
((ek = e.key) == k || (ek != null && k.equals(ek))))
return e;
} while ((e = e.next) != null);
}
return null;
}
}
TreeNode类
/**
* Nodes for use in TreeBins.
*/
static final class TreeNode<K,V> extends Node<K,V> {
TreeNode<K,V> parent; // red-black tree links
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev; // needed to unlink next upon deletion
boolean red;
TreeNode(int hash, K key, V val, Node<K,V> next,
TreeNode<K,V> parent) {
super(hash, key, val, next);
this.parent = parent;
}
Node<K,V> find(int h, Object k) {
return findTreeNode(h, k, null);
}
/**
* Returns the TreeNode (or null if not found) for the given key
* starting at given root.
*/
final TreeNode<K,V> findTreeNode(int h, Object k, Class<?> kc) {
if (k != null) {
TreeNode<K,V> p = this;
do {
int ph, dir; K pk; TreeNode<K,V> q;
TreeNode<K,V> pl = p.left, pr = p.right;
if ((ph = p.hash) > h)
p = pl;
else if (ph < h)
p = pr;
else if ((pk = p.key) == k || (pk != null && k.equals(pk)))
return p;
else if (pl == null)
p = pr;
else if (pr == null)
p = pl;
else if ((kc != null ||
(kc = comparableClassFor(k)) != null) &&
(dir = compareComparables(kc, k, pk)) != 0)
p = (dir < 0) ? pl : pr;
else if ((q = pr.findTreeNode(h, k, kc)) != null)
return q;
else
p = pl;
} while (p != null);
}
return null;
}
}
如果链表的数据过长是会转换为红黑树来处理。当它并不是直接转换,而是将这些链表的节点包装成TreeNode放在TreeBin对象中,然后由TreeBin完成红黑树的转换。TreeBin类的代码很长,其实TreeBin的构造方法就是一个建立红黑树的过程。
TreeNode 用于构建红黑树节点,但是ConcurrentHashMap 中的TreeNode和HashMap中的TreeNode用途有点差别,HashMap中hash 表的部分位置上存储的是一颗树,具体存储的就是TreeNode型的树根节点,而ConcurrentHashMap 则不同,其hash 表是存储的被TreeBin 包装过的树,也就是存放的是TreeBin对象,而不是TreeNode对象,同时TreeBin 带有读写锁,当需要调整树时,为了保证线程的安全,必须上锁。
TreeBin 对象
/**
* TreeNodes used at the heads of bins. TreeBins do not hold user
* keys or values, but instead point to list of TreeNodes and
* their root. They also maintain a parasitic read-write lock
* forcing writers (who hold bin lock) to wait for readers (who do
* not) to complete before tree restructuring operations.
*/
static final class TreeBin<K,V> extends Node<K,V> {
TreeNode<K,V> root; // 树根
volatile TreeNode<K,V> first; // 树的链式结构
volatile Thread waiter; // 等待者
volatile int lockState; // 锁状态
// values for lockState
static final int WRITER = 1; // set while holding write lock
static final int WAITER = 2; // set when waiting for write lock
static final int READER = 4; // increment value for setting read lock
...
}
过渡节点–ForwardingNode
/**
* A node inserted at head of bins during transfer operations.
*/
static final class ForwardingNode<K,V> extends Node<K,V> {
final Node<K,V>[] nextTable;
ForwardingNode(Node<K,V>[] tab) {
super(MOVED, null, null, null); // hash 值为MOVED 进行标识
this.nextTable = tab;
}
ForwardingNode 用于在hash 表扩容过程中的过渡节点,当hash 表进行扩容进行数据转移的时候,其它线程如果还不断的往原hash 表中添加数据,这个肯定是不好的,因此就引入了ForwardingNode 节点,当对原hash 表进行数据转移时,如果hash 表中的位置还没有被占据,那么就存放ForwardingNode 节点,表明现在hash 表正在进行扩容转移数据阶段,这样,其它线程在操作的时候,遇到ForwardingNode 节点,就知道hash 现在的状态了,就可以协助参与hash 表的扩容过程。
到这里,ConcurrentHashMap 中的重要的数据结构基本都了解了,一个是hash 表(table),一个是链表节点Node,其实呢就是红黑树节点TreeNode.
构造方法
1、无参构造
/**
* Creates a new, empty map with the default initial table size (16).
*/
public ConcurrentHashMap() {
}
里面什么都没有做,hash 表的初始化,是在第一次put 数据的时候初始化的。
2、指定hash 表大小
/**
* Creates a new, empty map with an initial table size
* accommodating the specified number of elements without the need
* to dynamically resize.
*
* @param initialCapacity The implementation performs internal
* sizing to accommodate this many elements.
* @throws IllegalArgumentException if the initial capacity of
* elements is negative
*/
public ConcurrentHashMap(int initialCapacity) {
this(initialCapacity, LOAD_FACTOR, 1);
}
3.指定hash表大小,负载因子,并发更新线程的估计量
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
//initialCapacity / loadFactor可能考虑到当initialCapacity超过MAXIMUM_CAPACITY* loadFactor时此后进一步的扩容问题。
因为容量即将达到上限,若是这样的话,则使用tableSizeFor()找到比size大的又最接近size的一个值,值须是2的幂次方
long size = (long)(1.0 + (long)initialCapacity / loadFactor);
int cap = (size >= (long)MAXIMUM_CAPACITY) ?
MAXIMUM_CAPACITY : tableSizeFor((int)size);
this.sizeCtl = cap;
}
注意,ConcurrentHashMap在构造函数中只会初始化sizeCtl值,并不会直接初始化table,而是延缓到第一次put操作
table初始化
前面已经提到过,table初始化操作会延缓到第一次put行为。但是put是可以并发执行的,Doug Lea是如何实现table只初始化一次的?让我们来看看源码的实现。
/**
* Initializes table, using the size recorded in sizeCtl.
*/
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
while ((tab = table) == null || tab.length == 0) {
// 如果sizeCtl <0 则说明其它线程在初始化table,自己等待初始化完成即可
if ((sc = sizeCtl) < 0)
// 让出cpu
Thread.yield(); // lost initialization race; just spin
//将要执行table初始化,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*n
}
} finally {
//这里无需cas ,前面已经保证了只有一个线程能执行初始化工作
sizeCtl = sc; // sizeCtl 设置为0.75*n
}
break;
}
}
return tab;
}
put操作
假设table已经初始化完成,put操作采用CAS+synchronized实现并发插入或更新操作,具体实现如下。
/**
* Maps the specified key to the specified value in this table.
* Neither the key nor the value can be null.
*
* <p>The value can be retrieved by calling the {@code get} method
* with a key that is equal to the original key.
*
* @param key key with which the specified value is to be associated
* @param value value to be associated with the specified key
* @return the previous value associated with {@code key}, or
* {@code null} if there was no mapping for {@code key}
* @throws NullPointerException if the specified key or value is null
*/
public V put(K key, V value) {
return putVal(key, value, false);
}
/** Implementation for put and putIfAbsent */
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
//计算hash值
int hash = spread(key.hashCode());
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh; K fk; V fv;
//将要初始化table
if (tab == null || (n = tab.length) == 0)
tab = initTable();
//通过hash 值计算table 中的索引,如果该位置没有数据,则可以put
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
// cas 将数据设置到table 中,如果设置成功,则本次put 基本完成
if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value)))
break; // no lock when adding to empty bin
}
//如果table位置上的节点状态时MOVE,则表明hash 正在进行扩容搬移数据的过程中
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f); //协助扩容
else if (onlyIfAbsent //检查第一个节点而不获取锁
&& fh == hash
&& ((fk = f.key) == key || (fk != null && key.equals(fk)))
&& (fv = f.val) != null)
return fv;
else {
// hash 表该位置上有数据,可能是链表,也可能是一颗树
V oldVal = null;
synchronized (f) {//将hash 表该位置进行上锁,保证线程安全
//在节点f上进行同步,节点插入之前,再次利用tabAt(tab, i) == f判断,防止被其它线程修改
if (tabAt(tab, i) == f) {
// hash 值>=0 表明这是一个链表结构
if (fh >= 0) {
binCount = 1; //标识链表长度
// 遍历链表
for (Node<K,V> e = f;; ++binCount) {
K ek;
// 存在相同的key,则覆盖其value
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;
// 不存在该key,将新数据添加到链表尾
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key, value);
break;
}
}
}
else if (f instanceof TreeBin) {// 该位置是红黑树,是TreeBin对象(注意是TreeBin,而不是TreeNode)
Node<K,V> p;
binCount = 2;
//通过TreeBin 中的方法,将数据添加到红黑树中
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
else if (f instanceof ReservationNode)
throw new IllegalStateException("Recursive update");
}
}
//添加了数据,需要进行检查
if (binCount != 0) {
//if 成立,说明遍历的是链表结构,并且超过了阀值,需要将链表转换为树
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i); //将table 索引i 的位置上的链表转换为红黑树
if (oldVal != null)
return oldVal;
break;
}
}
}
// ConcurrentHashMap 容量增加1,检查是否需要扩容
addCount(1L, binCount);
return null;
}
先来梳理一下大致逻辑:
1、计算key的hash 值
int hash = spread(key.hashCode());
//散布(XOR)较高位的散列值降低并强制顶部位为0.因为该表使用2次幂掩蔽,仅在当前掩码之上的位变化的散列集将始终发生冲突。 (在已知的例子中是一组Float键,在小表中保存连续的整数。)因此我们应用一个向下传播高位比特影响的变换。 速度,效用和比特扩展质量之间存在权衡。 因为许多常见的哈希集合已经合理分布(因此不会受益于传播),并且因为我们使用树来处理容器中的大量冲突,所以我们只是以最便宜的方式对一些移位的位进行异或,以减少系统损失, 以及由于表格边界而包含最高位的影响,否则这些位将永远不会用于索引计算。
static final int spread(int h) {
return (h ^ (h >>> 16)) & HASH_BITS;
}
2、如果table(hash 表)没有被初始化,则执行table的初始化过程。
3、通过hash 值得到table 中的索引index(n为table的大小),如果该位置没有数据,则直接将该数据存放在该位置。具体为:
获取table中对应索引的元素f。
Doug Lea采用Unsafe.getObjectVolatile来获取,也许有人质疑,直接table[index]不可以么,为什么要这么复杂?
在java内存模型中,我们已经知道每个线程都有一个工作内存,里面存储着table的副本,虽然table是volatile修饰的,但不能 保证线程每次都拿到table中的最新元素,Unsafe.getObjectVolatile可以直接获取指定内存的数据,保证了每次拿到数据都是最新的。
如果f为null,说明table中这个位置第一次插入元素,利用Unsafe.compareAndSwapObject方法插入Node节点。
- 如果CAS成功,说明Node节点已经插入,随后addCount(1L, binCount)方法会检查当前容量是否需要进行扩容。
- 如果CAS失败,说明有其它线程提前插入了节点,自旋重新尝试在这个位置插入节点。
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value)))
break; // no lock when adding to empty bin
}
4、如果table 表中该位置有数据,如果数据的hash 值为MOVED,则表明在进行table表的扩容工作,则辅助进行table的扩容和数据搬移工作。
5、如果table 表中该位置上的数据有效(存储的真正的数据),锁住该位置,然后执行后面的操作。
6、如果f.hash >= 0,说明f是链表结构的头结点,遍历链表.如果存在该key,则根据onlyIfAbsent 决定是否覆盖该value,如果不存在该key,则添加到链表的末尾。
7、如果f是TreeBin类型节点,说明f是红黑树根节点,则在树结构上遍历元素,更新或增加节点
8、如果数据添加到链表中,则需要检查链表的长度是否超过了阀值(链表中节点数binCount >= TREEIFY_THRESHOLD,默认是8),如果是则需要将该链表转换为红黑树。
9、如果上面过程在多线程中,执行失败(提前被其它线程改变),则需要从步骤2 重新开始。
10、递增map的容量,并检查是否需要扩容(addCount)。
添加数据到链表或树
对于 putVal中synchronized 包裹的内容,就是将数据添加到链表或者红黑树中,具体的代码这里就不再贴了,可以看前面的代码。
在前面的属性定义中有这样的定义:如果是一颗红黑树,那么其根(也就是table中的数据)的hash 值为TREEBIN
/*
* Encodings for Node hash fields. See above for explanation.
*/
static final int MOVED = -1; // hash for forwarding nodes
static final int TREEBIN = -2; // hash for roots of trees
static final int RESERVED = -3; // hash for transient reservations
static final int HASH_BITS = 0x7fffffff; // usable bits of normal node hash
将链表转换为红黑树
当添加数据到链表中后,如果链表的长度超过了阀值,那么会将链表转换为红黑树。
/**
* Replaces all linked nodes in bin at given index unless table is
* too small, in which case resizes instead.
*/
private final void treeifyBin(Node<K,V>[] tab, int index) {
Node<K,V> b; int n;
if (tab != null) {
// 如果table的容量不满足链表转换为红黑树的阀值要求,则需要对table 进行扩容
if ((n = tab.length) < MIN_TREEIFY_CAPACITY)
tryPresize(n << 1);
else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {
// 将table 中该位置 锁住
synchronized (b) {
if (tabAt(tab, index) == b) {
// 遍历链表,构造TreeNode链表
TreeNode<K,V> hd = null, tl = null;
for (Node<K,V> e = b; e != null; e = e.next) {
TreeNode<K,V> p =
new TreeNode<K,V>(e.hash, e.key, e.val,
null, null);
if ((p.prev = tl) == null)
hd = p;
else
tl.next = p;
tl = p;
}
//new TreeBin<K,V>(hd) 是将TreeNode 链表构造成一颗红黑树
//将table原位置上的链表TreeNode 对象更改为TreeBin对象
setTabAt(tab, index, new TreeBin<K,V>(hd));
}
}
}
}
}
大致过程:
1、如果table的容量不满足链表转换为红黑树的阀值要求,则需要对table 进行扩容
2、锁住table 表该位置上的数据,遍历链表,将Node 链表转换为TreeNode 链表
3、通过TreeBin对象通过TreeNode 链表构造红黑树结构,树根存储在TreeBin对象中
4、将TreeBin 对象存储在table 原链表Node的位置上,至此链表转红黑树完成。
将链表转换为红黑树之前会构造TreeNode 链表,这样在构造成红黑树后,不仅可以通过树的方式遍历该结构,同时TreeNode 之间也存在一种链式结构,该结构就是最初的链表转换为红黑树时构造的关系,在HashMap也存在这种结构,同时里面有相关的图示(在最后部分)
从整体来看整个过程还是很清楚的,和HashMap有着大致相同的逻辑,因为ConcurrentHashMap 要保证线程安全,因此在存在竞争的操作上采用了cas 或者加锁的方式进行,当执行失败时,则需要重新开始,有了大致的认识后,接下来我们在挨着分析具体的步骤。
扩容
当table容量不足的时候,即table的元素数量达到容量阈值sizeCtl,需要对table进行扩容。
整个扩容分为两部分:
1.构建一个nextTable,大小为table的两倍
2.把table的数据复制到nextTable中
扩容在HashMap和ConcurrentHashMap 中都是重头戏,ConcurrentHashMap是支持并发插入的,扩容操作可以有两种方式,一种是如同初始化table那样,整个过程都控制只有一个线程进行操作,这样肯定实现比较容易,但是这样会影响到性能,当数据量比较大时,搬移数据将是一个费事操作,追求完美的jdk 当然不是那样实现的,构建nextTable 这个肯定只有一个线程来执行,但是将table 中的数据复制到nextTable 中,这个可以进行并发复制,这样的话,实现就比较复杂了,在无锁的线程安全的算法中,都用到了一种思想:辅助
在分析SynchronousQueue 中阐述过下面的一段话:
不使用锁来保证数据结构的完整性,要确保其他线程不仅能够判断出第一个线程已经完成了更新还是处在更新的中途,还能够判断出如果第一个线程在操作状态,完成更新还需要什么操作。如果线程发现了处在更新中途的数据结构,它就可以 “帮助” 正在执行更新的线程完成更新,然后再进行自己的操作。当第一个线程回来试图完成自己的更新时,会发现不再需要了,返回即可。
因此在table 复制数据的过程中,其它线程是可以参与一同进行复制的,这样可以极大的提高效率,同时也必须要保证数据结构不被破坏,下面我们一步一步来看看是如何实现这一过程的。
分析扩容,我们先从addCount 这个方法入手,当添加数据后,会递增map中的size的计数,同时会检查table 是否需要扩容
private final void addCount(long x, int check) {
long s;
... //中间省略 map 数量增加x 的过程
if (check >= 0) {
Node<K,V>[] tab, nt; int n, sc;
while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
(n = tab.length) < MAXIMUM_CAPACITY) {
// 标记位(这里理解不是很清楚)
int rs = resizeStamp(n);
if (sc < 0) {
// 这个分支 属于协助扩容部分
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
transferIndex <= 0)
break;
// 协助扩容,递增SIZECTL,表明有新线程参与扩容了
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
transfer(tab, nt);
}
//设置SIZECTL 标记(rs << RESIZE_STAMP_SHIFT 为负数)
else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
//扩容,最先进行扩容的线程执行这里
transfer(tab, null);
s = sumCount();
}
}
}
注意看注释,将会保证其结果左移RESIZE_STAMP_SHIFT 为负数。
最新进行扩容的线程,其sizeCtl 为不为负数,因此将会执行后面的分支,这个时候,会设置SIZECTL的值为(rs << RESIZE_STAMP_SHIFT) + 2,而rs << RESIZE_STAMP_SHIFT 必定是一个负数。
竞争的线程判断如果不需要辅助扩容,则会跳出,否则将会进行辅助扩容,同时递增SIZECTL,在条件判断的地方,个人未完全参透,但是其意图是可以知道的。
/**
* 返回用于调整大小为n的表的标记位。当由RESIZE_STAMP_SHIFT向左移位时,必须为负。
* Returns the stamp bits for resizing a table of size n.
* Must be negative when shifted left by RESIZE_STAMP_SHIFT.
*/
static final int resizeStamp(int n) {
return Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1));
}
辅助扩容
transfer 这个方法便是扩容的核心方法了,在看这个方法前,我们需要先看helpTransfer 方法:
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
/**
* Helps transfer if a resize is in progress.
*/
final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {
Node<K,V>[] nextTab; int sc;
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;
// 辅助扩容,递增SIZECTL
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
transfer(tab, nextTab);
break;
}
}
return nextTab;
}
return table;
}
可以看到这个helpTransfer 方法,和前面我们展示addCount 中的部分是差不多的逻辑,判断是否需要进行辅助扩容,如果参与扩容,则需要递增SIZECTL。
因此现在我们的重点就是transfer 方法了,鉴于transfer 代码有点长,这里先简单的阐述一下过程:
1、获取遍历table的步长
2、最先进行扩容的线程,会初始化nextTable(正常情况下,nextTable 是table的两倍大小)
3、计算table某个位置索引 i,该位置上的数据将会被转移到nextTable 中。
4、如果索引i 所对应的table的位置上没有存放数据,则放在ForwardingNode 数据,表明该table 正在进行扩容处理。(如果有添加数据的线程添加数据到该位置上,将会发现table的状态)
5、将索引i 位置上的数据进行转移,数据分成两部分,一部分就是数据 在nextTable 中索引没有变(仍然是i),另一部分则是其索引变成i+n的,将这两部分分别添加到nextTable 中。
transfer 中的核心思想大致就是这样,这里有一个细节,就是步骤5 将数据分成两部分,这个其实如果了解jdk 1.8 的HashMap,那么也就知道这里为什么这样了,这个得益于table的大小必须是2的n次幂,这个在HashMap中有详细阐述,这里再次简单描述一下吧:
在ConcurrentHashMap中,扩容后的大小是原来的2倍(这个我们在后面会看到),所以,元素的位置要么是在原位置,要么是在原位置+n的位置(n 为原table的大小)。看下图可以明白这句话的意思,n为table的长度,图(a)表示扩容前的key1和key2两种key确定索引位置的示例,图(b)表示扩容后key1和key2两种key确定索引位置的示例,其中hash1是key1对应的哈希与高位运算结果。
有了这种结果,就可以避免将所有数据再次hash,可以判断hash 值的某个位置上的二进制位,来确定其索引是没有变,还是变成了i+n了。
有了上面的基础,再来看代码,应该就好理解多了
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
int n = tab.length, stride;
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
// 步长
stride = MIN_TRANSFER_STRIDE; // subdivide range
// 初始化 nextTab
if (nextTab == null) { // initiating
try {
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
nextTab = nt;
} catch (Throwable ex) { // try to cope with OOME
sizeCtl = Integer.MAX_VALUE;
return;
}
nextTable = nextTab;
// 设置transferIndex
transferIndex = n;
}
int nextn = nextTab.length;
// 转移标识节点
ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
boolean advance = true;
boolean finishing = false; // to ensure sweep before committing nextTab
// 计算和控制对table 表中位置i 的数据进行转移
for (int i = 0, bound = 0;;) {
Node<K,V> f; int fh;
while (advance) {
int nextIndex, nextBound;
if (--i >= bound || finishing)
advance = false;
// 本轮转移完毕
else if ((nextIndex = transferIndex) <= 0) {
i = -1;
advance = false;
}
// 更新TRANSFERINDEX(stride 为步长)
else if (U.compareAndSwapInt
(this, TRANSFERINDEX, nextIndex,
nextBound = (nextIndex > stride ?
nextIndex - stride : 0))) {
bound = nextBound;
i = nextIndex - 1;
advance = false;
}
}
//扫描table 本轮完成
if (i < 0 || i >= n || i + n >= nextn) {
int sc;
//如果finished,则设置新tabl,sizeCtl ,返回
if (finishing) {
nextTable = null;
table = nextTab;
sizeCtl = (n << 1) - (n >>> 1);
return;
}
//利用CAS方法更新这个扩容阈值,sizectl值减一,即将退出扩容
if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
//有其它线程在辅助,直接退出
if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
return;
//扩容完成,辅助的线程也退出了,设置完成标识,最后再recheck 一下
finishing = advance = true;
i = n; // recheck before commit
}
}
// table 索引i 的位置上没有数据
else if ((f = tabAt(tab, i)) == null)
// 设置 为ForwardingNode 节点,这样其它线程可以感知到table 状态
advance = casTabAt(tab, i, null, fwd);
//已经设置过了
else if ((fh = f.hash) == MOVED)
advance = true; // already processed
else {
// 位置上是数据,需要锁住该位置,进行数据转移
synchronized (f) {
if (tabAt(tab, i) == f) {
Node<K,V> ln, hn;
// 链表数据
if (fh >= 0) {
// 得到hash 值 高位的二进制值(高位:n的二进制位数)
int runBit = fh & n;
// 最后添加的数据
Node<K,V> lastRun = f;
// 遍历链表
for (Node<K,V> p = f.next; p != null; p = p.next) {
// 得到hash 值 高位的二进制值
int b = p.hash & n;
//和前个节点不一致,则更新
if (b != runBit) {
runBit = b;
lastRun = p;
}
}
// 更加runBit 设置相应值
if (runBit == 0) {
ln = lastRun;
hn = null;
}
else {
hn = lastRun;
ln = null;
}
// 遍历链表,注意调节p != lastRun
// lastRun 之后的数据其高位也是一致
for (Node<K,V> p = f; p != lastRun; p = p.next) {
int ph = p.hash; K pk = p.key; V pv = p.val;
// 高位是0,则索引没有变,添加到ln 链表中
if ((ph & n) == 0)
ln = new Node<K,V>(ph, pk, pv, ln);
else
// 索引变为i+n的数据添加到hn 链表中
hn = new Node<K,V>(ph, pk, pv, hn);
}
// 设置nextTab 索引i 的位置上的数据为链表ln
setTabAt(nextTab, i, ln);
// 设置nextTab 索引i 的位置上的数据为链表hn
setTabAt(nextTab, i + n, hn);
//原table 索引i 的位置上的数据转移完成,用ForwardingNode节点标识
setTabAt(tab, i, fwd);
advance = true;
}
// 是红黑树
else if (f instanceof TreeBin) {
TreeBin<K,V> t = (TreeBin<K,V>)f;
TreeNode<K,V> lo = null, loTail = null;
TreeNode<K,V> hi = null, hiTail = null;
int lc = 0, hc = 0;
//这里是链式遍历,而不是树的方式遍历
// 这和我们前面说的一致:树中保存了链式关系,在构造树的时候构建的。
for (Node<K,V> e = t.first; e != null; e = e.next) {
// 下面也是分成两部分,重新构建TreeNode 链表
int h = e.hash;
TreeNode<K,V> p = new TreeNode<K,V>
(h, e.key, e.val, null, null);
if ((h & n) == 0) {
if ((p.prev = loTail) == null)
lo = p;
else
loTail.next = p;
loTail = p;
++lc;
}
else {
if ((p.prev = hiTail) == null)
hi = p;
else
hiTail.next = p;
hiTail = p;
++hc;
}
}
// 如果不满足树要求,则转化为链表,否则转换为TreeBin 对象(内部建树)
ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) :
(hc != 0) ? new TreeBin<K,V>(lo) : t;
hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) :
(lc != 0) ? new TreeBin<K,V>(hi) : t;
// 设置nextTab 索引i 的位置上的数据为ln(可能是链表,也可能是树)
setTabAt(nextTab, i, ln);
// 设置nextTab 索引i+n 的位置上的数据为hn
setTabAt(nextTab, i + n, hn);
setTabAt(tab, i, fwd);
// 重设标识,继续循环,查找下次需要转移数据的索引i
advance = true;
}
}
}
}
}
}
这代码够长的,但是结合前面的叙述和代码中的注释,我相信掌握其脉络应该不成问题,难理解的应该是table 索引i的控制和计算了,并没有顺序遍历table 进行转移数据,因为多线程下,如果多个线程辅助转移数据,会导致冲突频繁,因此设置了一个步长,相当于不同的线程转移table 中不同 位置段 上的数据,这样可以减小冲突。
在转移数据过程中,如果是链表结构,则需要遍历两次链表,第一次的意义在于减少数据操作,如果链表中后半部分的数据,有着相同的高位,那么第二次遍历就可以缩短,如果不幸,数据的hash值高位情况分布的很乱,那么第一次的意义就不大了,将链表分成两部分,一部分是索引没有变的,一部分是索引值变为i+n,然后设置在新table 中即可。
如果是红黑树节点,同样的操作方式,分成两部分,在遍历树时,采用的是链式遍历,当重新构建树时,也会再次构建链式关系(这个可以看TreeBin的构造方法),如果发现树的数量不满足最低要求,则把树转换为链表结构。
终于把扩容分析完了,过程有点漫长,接下来要容易多了,不过还有一个难点—ConcurrentHashMap 的size 统计。
size 的统计
ConcurrentHashMap 是可以并发执行的,因此其size 只是一个瞬时值,当你拿到该值时,其大小很有可能已经变化很大了,因此其size 值可以参考,但是不要依赖。
传统算法中,用一个变量计算容器大小即可,当有改变时,更新该变量就可以,但是在ConcurrentHashMap 中是并发操作的,如果用一个变量进行统计的话,那么为了保证正确性,需要锁住,或者循环cas,但是如果在竞争比较激烈的时候,在size的设置上将会冲突很大,反而影响了性能,有点不划算,因此ConcurrentHashMap 采用了分而治之设计思想,什么意思呢,这个我们看一下其中的部分定义:
/**
* Table of counter cells. When non-null, size is a power of 2.
*/
/**
* counterCells数组,总数值的分值分别存在每个cell中
*/
private transient volatile CounterCell[] counterCells;
/**
* Spinlock (locked via CAS) used when resizing and/or creating CounterCells.
*/
/**
* 标识当前cell数组是否在初始化或扩容中的CAS标志位
*/
private transient volatile int cellsBusy;
/**
* Base counter value, used mainly when there is no contention,
* but also as a fallback during table initialization
* races. Updated via CAS.
*/
/**
* 总和值的获得方式为 base + 每个cell中分值之和
* 在并发度较低的场景下,所有值都直接累加在base中
*/
private transient volatile long baseCount;
我相信看了上面的定义,应该明白分而治之的思想了吧。
@sun.misc.Contended static final class CounterCell {
volatile long value;
CounterCell(long x) { value = x; }
}
// 计算总和
final long sumCount() {
CounterCell[] as = counterCells; CounterCell a;
long sum = baseCount;
if (as != null) {
// baseCount+每个cell 的值
for (int i = 0; i < as.length; ++i) {
if ((a = as[i]) != null)
sum += a.value;
}
}
return sum;
}
现在我们再来看ConcurrentHashMap 中addCount 中 计算size的方法:
private final void addCount(long x, int check) {
CounterCell[] as; long b, s;
//如果counterCells 为null ,则累计到baseCount 上
if ((as = counterCells) != null ||
!U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
CounterCell a; long v; int m;
// 是否冲突标志,默认未冲突
boolean uncontended = true;
// 获取当前线程的probe值,设置到某个cell 中,如果冲突,设置失败,则执行fullAddCount 方法
if (as == null || (m = as.length - 1) < 0 ||
(a = as[ThreadLocalRandom.getProbe() & m]) == null ||
!(uncontended =
U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
fullAddCount(x, uncontended);
return;
}
if (check <= 1)
return;
//计算总和
s = sumCount();
}
... // 省略后面table 扩容代码
}
private final void fullAddCount(long x, boolean wasUncontended) {
int h;
// 获取当前线程的probe值
// 如果为0,则初始化当前线程probe值
if ((h = ThreadLocalRandom.getProbe()) == 0) {
// 该静态方法会初始化当前线程所持有的随机值
ThreadLocalRandom.localInit(); // force initialization
// 获取生成后的probe值,用于选择cells数组下标元素
h = ThreadLocalRandom.getProbe();
// 由于重新生成了probe,未冲突标志位设置为true
wasUncontended = true;
}
boolean collide = false; // True if last slot nonempty
for (;;) {
CounterCell[] as; CounterCell a; int n; long v;
// cells数组已经被成功初始化
if ((as = counterCells) != null && (n = as.length) > 0) {
// 通过该值与当前线程probe求与,获得cells的下标元素,和hash 表获取索引是一样的
if ((a = as[(n - 1) & h]) == null) {
// cellsBusy 为0表示cells数组不在初始化或者扩容状态下
if (cellsBusy == 0) { // Try to attach new Cell
//创建新cell
CounterCell r = new CounterCell(x); // Optimistic create
// CAS设置cellsBusy,防止其它线程来破坏数据结构
if (cellsBusy == 0 &&
U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
boolean created = false;
try { // Recheck under lock
CounterCell[] rs; int m, j;
if ((rs = counterCells) != null &&
(m = rs.length) > 0 &&
rs[j = (m - 1) & h] == null) {
//将新创建的cell放入对应下标位置
rs[j] = r;
created = true;
}
} finally {
//恢复标识位(未占用)
cellsBusy = 0;
}
//操作成功,则退出死循环
if (created)
break;
continue; // Slot is now non-empty
}
}
collide = false;
}
// 获取了probe对应cells数组中的下标元素,发现不为空
// 并且调用该函数前,调用方CAS操作也已经失败(已经发生竞争)
else if (!wasUncontended) // CAS already known to fail
// 设置未冲突标志位后,重新生成probe,进入死循环
wasUncontended = true; // Continue after rehash
//CAS 执行累加
else if (U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))
// 成功,退出
break;
// 如果数组比较大了,则不需要扩容,继续重试,或者已经扩容了,重试
else if (counterCells != as || n >= NCPU)
collide = false; // At max size or stale
// 过期了,重试
else if (!collide)
collide = true;
// 对Cell数组进行扩容,CAS设置cellsBusy值
else if (cellsBusy == 0 &&
U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
try {
if (counterCells == as) {// Expand table unless stale
//容量增加1倍
CounterCell[] rs = new CounterCell[n << 1];
for (int i = 0; i < n; ++i)
rs[i] = as[i];
counterCells = rs;
}
} finally {
// 恢复
cellsBusy = 0;
}
collide = false;
continue; // Retry with expanded table
}
//重新生成一个probe值
h = ThreadLocalRandom.advanceProbe(h);
}
// 初始化Cell 数组
else if (cellsBusy == 0 && counterCells == as &&
U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
boolean init = false;
try { // Initialize table
if (counterCells == as) {
// 默认初始容量为2
CounterCell[] rs = new CounterCell[2];
//将x的值设置到cell 数组中
rs[h & 1] = new CounterCell(x);
counterCells = rs;
// 初始化成功
init = true;
}
} finally {
// 恢复
cellsBusy = 0;
}
// 初始化成功,退出死循环
if (init)
break;
}
//竞争激烈,其它线程占据cell 数组,直接累加在base变量中
else if (U.compareAndSwapLong(this, BASECOUNT, v = baseCount, v + x))
break; // Fall back on using base
}
}
是不是发现这个fullAddCount 方法也不简单,其大致逻辑如下:
1、初始化用于获取counterCells数组的索引值(线程相关)
2、如果存在counterCells数组,如果counterCells 数组中不存在数据,尝试占据counterCells 数组,创建cell,将cell 设置到counterCells 中的某个位置上,退出循环
3、如果counterCells 中该位置上存在数据,如果有冲突,则重新循环,否则尝试累加数据到cell 中,成功则退出循环。
4、如果counterCells 数组扩容,或者不需要扩容,则重新循环
5、对counterCells 进行扩容操作(容量增加1倍),然后重新循环
6、如果不存在counterCells 数组,则尝试进行初始化,并设置数据到某个cell
7、如果尝试初始化失败,则可能其它线程再初始化,那么累计数据到baseCount,成功就退出,否则重新循环。
到这里,应该基本明白是如何更新ConcurrentHashMap的size 大小的吧,接下来的内容就真的很轻松了,同时分析到这里,对ConcurrentHashMap 应该有了大致认识,明白其原理和一些核心思想。
参考:
https://blog.csdn.net/u014634338/article/details/78796357
https://blog.csdn.net/lsgqjh/article/details/54867107
https://yq.aliyun.com/articles/36781/