jdk1.8中ConcurrentHashMap的源码解读
ConcurrentHashMap是Java中一个非常重要的并发容器,它提供了一个高效的线程安全的哈希表,支持多个线程同时进行读写操作,而不需要加锁。在jdk1.8中,ConcurrentHashMap的实现发生了很大的变化,本文将从以下几个方面来分析其源码:
- 基本结构和属性
- put方法
- get方法
- resize方法
基本结构和属性
ConcurrentHashMap的基本结构如下:
java
public class ConcurrentHashMap<K,V> extends AbstractMap<K,V>
implements ConcurrentMap<K,V>, Serializable {
private static final long serialVersionUID = 7249069246763182397L;
// 最大容量,2^30
private static final int MAXIMUM_CAPACITY = 1 << 30;
// 默认初始容量,16
private static final int DEFAULT_CAPACITY = 16;
// 默认负载因子,0.75
private static final float LOAD_FACTOR = 0.75f;
// 并发级别阈值,当并发线程数超过这个值时,扩容时会使用多线程
private static final int CONCURRENCY_LEVEL = 16;
// 数组大小掩码,用于计算数组索引
private static final int HASH_BITS = 0x7fffffff;
// 转移节点的hash值,表示当前节点是一个转移节点
static final int MOVED = -1;
// 树形节点的hash值,表示当前节点是一个树形节点
static final int TREEBIN = -2;
// 红黑树节点的hash值,表示当前节点是一个红黑树节点
static final int RESERVED = -3;
// 最小树形化阈值,当链表长度超过这个值时,会转换为红黑树
static final int MIN_TREEIFY_CAPACITY = 64;
// Node类,表示一个普通的链表节点
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;
}
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 key + "=" + val; }
public final V setValue(V value) {
throw new UnsupportedOperationException();
}
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)));
}
// 返回当前节点的后继节点,如果是转移节点,则帮助转移后返回后继节点
Node<K,V> find(int h, Object k) {
Node<K,V> e = this;
if (k != null) {
do {
int eh; K ek;
if ((eh = e.hash) == h &&
((ek = e.key) == k || (ek != null && k.equals(ek))))
return e;
if (eh < 0)
return e.find(h, k);
} while ((e = e.next) != null);
}
return null;
}
}
// ForwardingNode类,表示一个转移节点,用于扩容时的数据迁移
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;
}
Node<K,V> find(int h, Object k) {
// loop to avoid arbitrarily deep recursion on forwarding nodes
outer: for (Node<K,V>[] tab = nextTable;;) {
Node<K,V> e; int n;
if (k == null || tab == null || (n = tab.length) == 0 ||
(e = tabAt(tab, (n - 1) & h)) == null)
return null;
for (;;) {
int eh; K ek;
if ((eh = e.hash) == h &&
((ek = e.key) == k || (ek != null && k.equals(ek))))
return e;
if (eh < 0) {
if (e instanceof ForwardingNode) {
tab = ((ForwardingNode<K,V>)e).nextTable;
continue outer;
}
return e.find(h, k);
}
if ((e = e.next) == null)
return null;
}
}
}
}
// TreeBin类,表示一个树形节点,用于存储红黑树的根节点和锁对象
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
// 构造方法,将链表转换为红黑树
TreeBin(TreeNode<K,V> b) {
super(TREEBIN, null, null, null);
this.first = b;
TreeNode<K,V> r = null;
for (TreeNode<K,V> x = b, next; x != null; x = next) {
next = (TreeNode<K,V>)x.next;
x.left = x.right = null;
if (r == null) {
x.parent = null;
x.red = false;
r = x;
}
else {
K k = x.key;
int h = x.hash;
Class<?> kc = null;
for (TreeNode<K,V> p = r;;) {
int dir, ph;
K pk = p.key;
if ((ph = p.hash) > h)
dir = -1;
else if (ph < h)
dir = 1;
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0)
dir = tieBreakOrder(k, pk);
TreeNode<K,V> xp = p;
if ((p = (dir <= 0) ? p.left : p.right) == null) {
x.parent = xp;
if (dir <= 0)
xp.left = x;
else
xp.right = x;
r = balanceInsertion(r, x);
break;
}
}
}
}
this.root = r;
assert checkInvariants(root);
}
// 省略其他方法...
}
// TreeNode类,表示一个红黑树节点,继承自Node类
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);
}
// 省略其他方法...
}
// transient关键字表示该属性不会被序列化
transient volatile Node<K,V>[] table;
// 下一个扩容的阈值,等于容量乘以负载因子
private transient volatile int sizeCtl;
// 记录当前正在进行扩容的线程数
private transient volatile int transferIndex;
// 记录当前正在进行扩容的线程的栈
private transient volatile TransferStack<K,V> transferStack;
// 记录当前map中元素的个数,使用LongAdder类来保证原子性和高效性
private transient volatile long baseCount;
// 记录当前map
put方法
put方法是ConcurrentHashMap的核心方法之一,它用于向map中插入或更新一个键值对。put方法的源码如下:
java
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();
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)
tab = initTable(); // 初始化table
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // CAS成功,直接插入
}
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f); // 扩容中,帮助转移
else {
V oldVal = null;
synchronized (f) { // 同步锁定头节点
if (tabAt(tab, i) == f) {
if (fh >= 0) { // 链表节点
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
K ek;
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val; // 找到相同的key,更新value
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) { // 树形节点
Node<K,V> p;
binCount = 2;
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val; // 找到相同的key,更新value
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
if (binCount != 0) { // 检查是否需要树形化或扩容
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount); // 增加元素个数
return null;
}
从上面的代码可以看出,put方法的大致流程如下:
- 首先,判断key和value是否为空,如果为空则抛出空指针异常。
- 然后,计算key的hash值,并根据hash值找到对应的数组索引i。
- 接着,判断数组索引i处是否有节点存在,如果没有,则使用CAS操作尝试插入一个新的节点。
- 如果有节点存在,则判断该节点的hash值是否为MOVED,如果是,则说明当前正在进行扩容,那么就调用helpTransfer方法来帮助转移数据,并重试。
- 如果该节点的hash值不为MOVED,则判断该节点是链表节点还是树形节点。如果是链表节点,则遍历链表,查找是否有相同的key存在,如果有,则更新value,如果没有,则插入新的节点。如果是树形节点,则调用putTreeVal方法,按照红黑树的规则,查找或插入新的节点。
- 在遍历或插入的过程中,需要同步锁定头节点,以防止其他线程的干扰。同时,需要记录链表或树形的长度,以便后续判断是否需要树形化或扩容。
- 最后,如果插入了新的节点,则调用addCount方法来增加元素个数,并检查是否需要扩容。如果更新了旧的节点,则返回旧的value。
从这个流程可以看出,put方法尽量减少了锁的使用,只有在链表或树形节点的情况下才会锁定头节点,而且只锁定一个节点,不影响其他索引处的操作。同时,put方法也利用了CAS操作来实现无锁插入和扩容,提高了并发效率。另外,put方法还支持链表转换为红黑树的功能,以提高查询性能。
get方法
get方法是ConcurrentHashMap的另一个核心方法,它用于从map中获取一个键对应的值。get方法的源码如下:
java
public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
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) {
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
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;
}
从上面的代码可以看出,get方法的大致流程如下:
- 首先,判断key是否为空,如果为空则抛出空指针异常。
- 然后,计算key的hash值,并根据hash值找到对应的数组索引i。
- 接着,判断数组索引i处是否有节点存在,如果没有,则返回null。
- 如果有节点存在,则判断该节点的hash值是否等于key的hash值,如果是,则判断该节点的key是否等于key,如果是,则直接返回该节点的value。
- 如果该节点的hash值不等于key的hash值,则判断该节点的hash值是否小于0,如果是,则说明该节点是转移节点或树形节点。那么就调用find方法来查找对应的value。
- 如果该节点的hash值大于0,则说明该节点是链表节点。那么就遍历链表,查找是否有相同的key存在,如果有,则返回对应的value。
从这个流程可以看出,get方法是一个完全无锁的操作,它不需要同步任何数据结构,也不需要CAS操作。它只是简单地根据hash值和key来查找对应的value。这样可以保证get方法的高效性和一致性。
resize方法
用于在元素个数超过阈值时,将原来的数组扩大为原来的两倍,并将原来数组中的数据迁移到新数组中。resize方法的源码如下:
java
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;
}
transferStack = new TransferStack<K,V>();
transferIndex = n;
advanceCount = new LongAdder();
}
int nextn = nextTab.length;
TransferStack<K,V> fs = transferStack;
TransferStack.Node snode;
int sc; // signal
while (true) {
if ((snode = fs.pop()) == null) { // pop transfer task
if ((sc = sizeCtl) < 0)
break; // end of resize
if (sc > 0 && fs.push(new TransferStack.Node(sc))) {
sizeCtl = sc - 1; // claim new task
continue;
}
}
else if (snode instanceof TransferStack.ForwardingNode) {
continue; // already processed
}
else {
int fn = snode.fence, hn = snode.hash, origin, index;
if ((index = hn + stride) >= n)
index -= n;
if ((origin = hn) < index || index < 0)
index += n;
boolean empty = true;
for (int i = origin;;) { // traverse and process
Node<K,V> f, e; int h; K k; V v;
if ((f = tabAt(tab, i)) == null)
advanceCount.increment();
else if ((h = f.hash) < 0) {
if (f instanceof ForwardingNode) {
if (((ForwardingNode<K,V>)f).nextTable == nextTab)
advanceCount.increment();
else
setTabAt(tab, i, new ForwardingNode<K,V>(nextTab));
}
else if (f instanceof TreeBin)
((TreeBin<K,V>)f).split(this, tab, i, n);
else { // list split
Node loHead = null, loTail = null;
Node hiHead = null, hiTail = null;
Node next;
do {
next = f.next;
if ((f.hash & n) == 0) {
if ((loTail == null))
loHead = f;
else
loTail.next = f;
loTail = f;
}
else {
if ((hiTail == null))
hiHead = f;
else
hiTail.next = f;
hiTail = f;
}
} while ((f = next) != null);
setTabAt(nextTab, i, loHead);
setTabAt(nextTab, i + n, hiHead);
setTabAt(tab, i, new ForwardingNode<K,V>(nextTab));
advanceCount.increment();
}
}
else {
boolean added = false;
for (Node<K,V> e1;;) {
K ek; V ev; int eh; V enew; Node<K,V> pred, p, q;
if ((e1 = e.next) == null ||
(ek = e1.key) == null ||
!(ek instanceof Comparable)) {
break; // give up on list
}
if ((eh = spread(ek.hashCode())) <= h)
break; // end of run
pred = e; e1.hash |= MOVED; e1.key = null; e1.val = null;
for (p = e;;) {
if ((q = p.next) == null ||
(k = q.key) == null ||
!(k instanceof Comparable)) {
p.next = e1; e1.next = q;
added = true;
break;
}
if ((ev = q.val) == null) {
if (q instanceof TreeBin)
break; // leave to tree split
if (q.casVal(null, enew = f.apply(ek, ev))) {
added = true;
break;
}
}
else if ((q.hash & MOVED) != 0 ||
!ek.equals(k))
break; // end of list
else if (q.casVal(ev, enew = f.apply(ek, ev))) {
added = true;
break;
}
p = q;
}
if (added)
break;
}
if (!added) { // try to append
Node<K,V> r, p;
if ((r = new ReservationNode<K,V>()) != null) {
synchronized (r) {
if ((p = tabAt(tab, i)) == f) {
int rs = resizeStamp(n);
Node<K,V> g = new ReservationNode<K,V>();
g.next = r; r.next = f;
setTabAt(tab, i, g);
for (int j = 0; j < n; ++j) {
while ((p = tabAt(tab, j)) != null &&
p.hash < 0) {
Thread.yield(); // wait for resize
}
}
transfer(tab, nextTab);
return nextTab;
}
}
}
}
}
if (!empty)
advanceCount.increment();
if (i == index || !empty)
break;
if ((i += stride) >= n)
i -= n;
}
fs.addCount(1L, -1);
}
}
}
从上面的代码可以看出,resize方法的大致流程如下:
- 首先,判断是否需要初始化新的数组,如果是,则创建一个长度为原来两倍的数组,并初始化一些辅助变量,如transferStack、transferIndex和advanceCount。
- 然后,从transferStack中弹出一个转移任务,如果没有,则尝试从sizeCtl中获取一个新的任务,并将其压入transferStack中。
- 接着,根据转移任务的范围,遍历原数组中的节点,并将其迁移到新数组中。迁移的过程中,需要判断节点的类型,如果是普通节点,则按照hash值的最高位来分配到新数组的两个位置。如果是转移节点,则直接跳过。如果是树形节点,则调用split方法来分割红黑树。如果是链表节点,则按照链表的顺序来分割链表。
- 在迁移的过程中,需要将原数组中的节点替换为一个ForwardingNode,表示该位置已经被转移。同时,需要使用CAS操作来保证原子性和一致性。另外,还需要使用advanceCount来记录转移的进度和状态。
- 最后,如果所有的转移任务都完成了,则返回新的数组,并更新相关的属性。
从这个流程可以看出,resize方法是一个非常复杂和精妙的方法,它利用了多线程、CAS操作、无锁编程、红黑树等多种技术,实现了高效和安全的扩容功能。它不仅保证了扩容过程中不影响其他线程的读写操作,而且还能平衡扩容的负载和速度。它是ConcurrentHashMap性能优异的重要保证。