ConcurrentHashMap源码解析
ConcurrentHashMap是java.util.concurrent包的重要成员, 本文结合JAVA内存模型, 分析ConcurrentHashMap的源码解析.
1. ConcurrentHashMap
在多线程环境下, 一般都是不能直接使用HashMap的, 因为他不是线程安全的. 通常会使用HashTable替代HashMap, 该类基本上所有的方法都采用synchronized进行线程安全的控制, 在高并发的情况下, 每次只有一个线程能够获取对象监视器锁, 这样的并发性能的确不令人满意。另外一种方式通过Collections的Map<K,V> synchronizedMap(Map<K,V> m)将HashMap包装成一个线程安全的Map: SynchronzedMap. 实际上, SynchronzedMap实现依然是采用synchronized独占式锁进行线程安全的并发控制的, 这种方案的性能也是令人不太满意的.
ConcurrentHashMap在JDK 1.5时加入, 底层与HashMap类似, 都采用了数组和链表的结构, 不同的是该类是一个线程安全的Map, 利用了锁分段的思想提高了并发性. 在JDK 1.7中, ConcurrentHashMap为了实现并行访问, 引入了Segment结构, 其最外层不再是一个大的数组, 而是一个Segment数组。每个Segment中包含一个与HashMap数据结构差不多的链表数组, 理论上最大并发度与Segment个数相等。在JDK 1.8中, 为了进一步提高并发性, 摒弃了分段锁方案, 大量使用synchronized以及CAS无锁操作以保证ConcurrentHashMap的线程安全性, 同时为了提高哈希碰撞下的寻址性能, 在链表长度超过一定阈值(8)时将链表(寻址时间复杂度为O(N))转换为红黑树(寻址时间复杂度为O(log(N))).
2. 源码分析
2.1 属性
public class ConcurrentHashMap<K,V> extends AbstractMap<K,V> implements ConcurrentMap<K,V>, Serializable {
// 存储所有元素, 采用懒加载方式, 知道第一次插入数据才进行初始化. 长度总是为2的幂次方
transient volatile Node<K,V>[] table;
// 扩容时使用, 平时都是null, 只有在扩容时才为非null
private transient volatile Node<K,V>[] nextTable;
// 该属性用于控制table数组的大小, 根据是否初始化和是否正在扩容有几种情况
// 负数: -1表示正在初始化, -N表示当前正有N-1个线程进行扩容操作.
// 正数: 如果当前数组为null, 表示正在初始化, 该变量表示新建数组的长度
// 如果已经初始化, 表示当前table数组可用容量, 也可以理解为临界值(插入节点数超过该临界值就需要扩容)
// 具体值为数组的长度 * 负载因子(loadFactor)
// 0: 即数组长度为默认初始值
private transient volatile int sizeCtl;
// CAS相关操作需要使用, 具体可以了解CAS相关内容. 本文不再复述.
private static final sun.misc.Unsafe U;
}
2.2 内部类
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, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.val = val;
this.next = next;
}
}
ConcurrentHashMap的链表结构, 实现了Map.Entry接口, 存放key-value.
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;
}
}
ConcurrentHashMap的红黑树结构, 继承自Node.
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
}
该类并不会负责包装key-value信息, 而是包装TreeNode节点. 实际上ConcurrentHashMap数组中存放的并不是TreeNode对象, 而是TreeBin对象.
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);
this.nextTable = tab;
}
}
该类是在扩容时才会出现的特殊节点, 其key, value, hash全部为null, 并拥有nextTable指针引用的新table数组
2.3 CAS相关操作
public class ConcurrentHashMap<K,V> extends AbstractMap<K,V> implements ConcurrentMap<K,V>, Serializable {
// 该方法用于获取table数组索引为i的元素
static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
}
// 该方法利用cas操作设置数组中索引为i的元素
static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i,
Node<K,V> c, Node<K,V> v) {
return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
}
// 该方法用于设置table数组中索引为i的元素
static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) {
U.putObjectVolatile(tab, ((long)i << ASHIFT) + ABASE, v);
}
}
2.4 构造函数
public class ConcurrentHashMap<K,V> extends AbstractMap<K,V> implements ConcurrentMap<K,V>, Serializable {
public ConcurrentHashMap(int initialCapacity) {
if (initialCapacity < 0)
throw new IllegalArgumentException();
// 判断是否超过了允许的最大值,超过了话则取最大值,否则再对该值进一步处理
int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY : tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
this.sizeCtl = cap;
}
}
ConcurrentHashMap的构造函数并不是很复杂, 其中需要注意的是tableSizeFor方法, 该方法与HashMap中的实现是一致的, 此处不再复述. 另外需要注意的是,调用构造器方法的时候并未构造出table数组(可以理解为ConcurrentHashMap的数据容器),只是算出table数组的长度,当第一次向ConcurrentHashMap插入数据的时候才真正的完成初始化创建table数组的工作。
2.5 初始化数组
public class ConcurrentHashMap<K,V> extends AbstractMap<K,V> implements ConcurrentMap<K,V>, Serializable {
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
while ((tab = table) == null || tab.length == 0) { // 还没有进行初始化
if ((sc = sizeCtl) < 0) { // 1. 保证只有一个线程正在进行初始化操作
Thread.yield(); // lost initialization race; just spin
} else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
if ((tab = table) == null || tab.length == 0) {
// 2. 计算数组需要的大小
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
// 3. 此处才开始初始化数组
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
table = tab = nt;
// 4. 计算数组的可用大小: 实际大小 * 负载因子(0.75)
sc = n - (n >>> 2);
}
} finally {
sizeCtl = sc;
}
break;
}
}
return tab;
}
}
该方法在调用ConcurrentHashMap的put方法时被调用, 用于初始化table数组. 该方法可能存在被多个线程同时调用, 为了保证能够正确初始化,在第1步中会先通过if进行判断,若当前已经有一个线程正在初始化即sizeCtl值变为-1,这个时候其他线程调用Thread.yield()让出CPU时间片。正在进行初始化的线程会调用UnSafe.compareAndSwapInt方法将sizeCtl修改为-1: 即正在初始化的状态。另外还需要注意的是,在第四步中会进一步计算数组中可用的大小(数组实际大小 * 负载因子). 可以看看此处的负载因子是如何计算的, n - (n >>> 2)刚好是n - (1/4)n = (3/4)n.
2.6 扩容
public class ConcurrentHashMap<K,V> extends AbstractMap<K,V> implements ConcurrentMap<K,V>, Serializable {
// 该方法在putVal方法的最后, addCount方法内进行调用
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
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 = n;
}
int nextn = nextTab.length;
// 新建forwardingNode引用,在之后会用到
ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
boolean advance = true;
boolean finishing = false; // to ensure sweep before committing nextTab
for (int i = 0, bound = 0;;) {
Node<K,V> f; int fh;
// 确定循环中的索引i
while (advance) {
int nextIndex, nextBound;
if (--i >= bound || finishing) {
advance = false;
} else if ((nextIndex = transferIndex) <= 0) {
i = -1;
advance = false;
} else if (U.compareAndSwapInt(this, TRANSFERINDEX, nextIndex, nextBound = (nextIndex > stride ? nextIndex - stride : 0))) {
bound = nextBound;
i = nextIndex - 1;
advance = false;
}
}
// 将原数组中的元素复制到新数组中去
if (i < 0 || i >= n || i + n >= nextn) {
int sc;
if (finishing) {
// 处理完成, 重置nextTable, 修改sizeCtl
nextTable = null;
table = nextTab;
sizeCtl = (n << 1) - (n >>> 1);
return;
}
if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
return;
finishing = advance = true;
i = n; // recheck before commit
}
} else if ((f = tabAt(tab, i)) == null) {
// 当前数组中第i个元素为null,用CAS设置成特殊节点forwardingNode(可以理解成占位符)
advance = casTabAt(tab, i, null, fwd);
} else if ((fh = f.hash) == MOVED) {
// 如果遍历到ForwardingNode节点, 说明这个点已经被处理过了 直接跳过 这里是控制并发扩容的核心
advance = true; // already processed
} else {
synchronized (f) {
if (tabAt(tab, i) == f) {
Node<K,V> ln, hn;
if (fh >= 0) {
// 处理当前节点为链表的头结点的情况,构造两个链表,一个是原链表, 另一个是原链表的反序排列
int runBit = fh & n;
Node<K,V> lastRun = f;
for (Node<K,V> p = f.next; p != null; p = p.next) {
int b = p.hash & n;
if (b != runBit) {
runBit = b;
lastRun = p;
}
}
if (runBit == 0) {
ln = lastRun;
hn = null;
} else {
hn = lastRun;
ln = null;
}
for (Node<K,V> p = f; p != lastRun; p = p.next) {
int ph = p.hash; K pk = p.key; V pv = p.val;
if ((ph & n) == 0) {
ln = new Node<K,V>(ph, pk, pv, ln);
} else {
hn = new Node<K,V>(ph, pk, pv, hn);
}
}
// 在nextTable的i位置上插入一个链表
setTabAt(nextTab, i, ln);
// 在nextTable的i + n的位置上插入另一个链表
setTabAt(nextTab, i + n, hn);
// 在table的i位置上插入forwardNode节点, 表示已经处理过该节点
setTabAt(tab, i, fwd);
// 设置advance为true, 返回到上面的while循环中, 就可以执行i--操作
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) {
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;
}
}
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;
setTabAt(nextTab, i, ln);
setTabAt(nextTab, i + n, hn);
setTabAt(tab, i, fwd);
advance = true;
}
}
}
}
}
}
}
整个扩容操作可以分为两个部分:
第一部分就是构建一个nextTable, 它的容量是原来的两倍.
第二部分就是将原来table中的元素复制到nextTable中. 根据运算得到当前遍历的数组的位置i,然后利用tabAt方法获得i位置的元素再进行判断:
- 如果这个位置为空,就在原table中的i位置放入forwardNode节点,这个也是触发并发扩容的关键点
- 如果这个位置是Node节点(fh >= 0),如果它是一个链表的头节点,就构造一个反序链表,把他们分别放在nextTable的i和i+n的位置上
- 如果这个位置是TreeBin节点(fh = -2),也做一个反序处理,并且判断是否需要将红黑树转换为链表, 把处理的结果分别放在nextTable的i和i+n的位置上
- 遍历过所有的节点以后就完成了复制工作,这时让nextTable作为新的table,并且更新sizeCtl为新容量的 0.75 倍,完成扩容。
![ConcurrentHashMap]()
2.7 添加
public class ConcurrentHashMap<K,V> extends AbstractMap<K,V> implements ConcurrentMap<K,V>, Serializable {
public V put(K key, V value) {
return putVal(key, value, false);
}
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null)
throw new NullPointerException();
// 1. 计算hash, 此处可以查看HashMap的实现方式
int hash = spread(key.hashCode());
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0) {
// 2. 如果当前table未初始化, 进行初始化工作
tab = initTable();
} else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
// 3. table中索引为i的元素为null, 直接添加即可
if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
} else if ((fh = f.hash) == MOVED) {
// 4. 正在扩容
tab = helpTransfer(tab, f);
} else {
// hash碰撞的情况下, 插入到链表或者红黑树中
V oldVal = null;
synchronized (f) {
if (tabAt(tab, i) == f) {
// 此处需要注意的是, 如果该节点是红黑树, 它的hash值为-2
if (fh >= 0) {
// 5. 当前为链表, 在链表中插入新的键值对
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
// 如果在链表中找到相同的key, 直接进行覆盖
K ek;
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;
if ((e = e.next) == null) {
// 如果直到链表的末尾也没有找到相同的key, 追加到链表的末尾
pred.next = new Node<K,V>(hash, key, value, null);
break;
}
}
} else if (f instanceof TreeBin) {
// 6. 当前为红黑树, 插入新的键值对
Node<K,V> p;
binCount = 2;
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key, value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
// 7. 插入完成后, 根据实际大小判断是否需要转换为红黑树
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
// 8.对当前容量大小进行检查,如果超过了临界值(实际大小 * 负载因子)就进行扩容
addCount(1L, binCount);
return null;
}
}
从整体而言,为了解决线程安全的问题,ConcurrentHashMap使用了synchronzied和CAS进行加锁. 如果ConcurrentHashMap没有出现哈希冲突的情况,每个元素将均匀的分布在哈希桶数组中。当出现哈希冲突的时,将hash值相同的节点构成链表,称为“拉链法”. 另外,在1.8版本中为了防止拉链过长,当链表的长度大于8的时候会将链表转换成红黑树。table数组中的每个元素实际上是单链表的头结点或者红黑树的根节点。当需要插入时, 首先应该定位到要插入的桶,即插入table数组的索引处, 此处使用(n - 1) & hash进行计算即可得到索引.
如果当前节点不为null, 并且为特殊节点(ForwardingNode, 该节点的hash值为MOVED; -1)的话,就说明当前concurrentHashMap正在进行扩容.
接下来, 当前节点不为null也不为特殊节点, 并且hash值大于0的情况下, 说明当前节点为所有hash一致的元素组成的链表的头节点. 通过synchronized的方式进行加锁以实现线程安全性, 将键值对插入到链表中. 之后就是对红黑树的添加, 可以看到if判断中的条件是f instanceof TreeBin, 说明TreeBin是对TreeNode的进一步封装, 对红黑树进行操作的时候针对的是TreeBin而不是TreeNode. 与添加链表的逻辑相同, 如果key在红黑树中已经存在就覆盖旧值,否则就向红黑树追加新节点。
在插入方法的最后, 会对当前链表大小进行调整. 当前链表节点个数大于等于8(TREEIFY_THRESHOLD)的时候,就会调用treeifyBin方法将链表转换成红黑树。
2.8 删除
public class ConcurrentHashMap<K,V> extends AbstractMap<K,V> implements ConcurrentMap<K,V>, Serializable {
public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
// 计算hash
int h = spread(key.hashCode());
// 判断数组是否已经初始化, 并且判断取出的值不为空
if ((tab = table) != null && (n = tab.length) > 0 && (e = tabAt(tab, (n - 1) & h)) != null) {
if ((eh = e.hash) == h) {
// 存储的hash值与传入key的hash相同.
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
// key完全相同, 直接返回即可
return e.val;
} else if (eh < 0) {
// 此处处理的是红黑树
return (p = e.find(h, key)) != null ? p.val : null;
}
while ((e = e.next) != null) {
// 从链表中查找
if (e.hash == h && ((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
return null;
}
}
整个获取的方法比较简单, 直接看注释即可.
3. 总结
相对于JDK 1.7, JDK 1.8中的ConcurrentHashMap修改的比较多, 比如放弃Segment采用锁node方式减少锁的粒度等等. 关于更多1.7版本与1.8版本中ConcurrentHashMap的实现对比, 可以参考这里.
本文分析的代码只是ConcurrentHashMap的一部分, 更多的源码分析请参考其他文章进行阅读.

浙公网安备 33010602011771号