高频面试题
- ConcurrentHashMap的实现原理是怎样的?是如何保证线程安全的?
- ConcurrentHashMap在哪些地方做了并发控制?
- 为什么ConcurrentHashMap不允许null值?
- ConcurrentHashMap是如何保证fail-safe的?
- ConcurrentHashMap为什么在JDK1.8废弃分段锁?
- ConcurrentHashMap为什么在JDK1.8使用synchronized而不是ReentrantLock?
今天我们带着上述面试高频被问到的问题,结合源码来一探究竟吧
put方法
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();
// 对输入的hashcode进行扩散,将输入的hashcode高16位和低16位进行异或操作,将高位的影响扩散到低位(通常我们只关心hashcode的低位),其结果与HASH_BITS进行与操作,确保最高位为0,这是因为在ConcurrentHashMap中,最高位被用作特殊标记
// 目的是使得散列更加均匀
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)
// 初始化hash表
tab = initTable();
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { // hash桶tab[i]为空
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null))) // 通过cas将新的节点添加到桶中,如果添加成功则跳出循环
break; // no lock when adding to empty bin
}
else if ((fh = f.hash) == MOVED) // 一个bin(桶)中的节点数超过一定阈值时,会触发扩容操作,扩容时原有的节点会被分散到新的hash表中,为了标记这个过程,原有的接点会被替换为一个特殊的节点,其hash值为MOVED
// 当前节点的hash值 == MOVED 表明hash正在进行扩容
tab = helpTransfer(tab, f); // 将当前节点f中的键值对移动到新的hash表中
else { // 当前桶中有元素
V oldVal = null;
synchronized (f) { // 对当前节点f节点加锁
if (tabAt(tab, i) == f) { // 如果f在桶tab[i]时
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相同时新值覆盖旧值
if (!onlyIfAbsent)
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) { // f为树节点
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;
}
}
}
}
if (binCount != 0) {
// 树化的条件:桶中节点个数达到8时 && ConcurrentHashMap中的节点数量达到64
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
// 更新ConcurrentHashMap中元素的数量
addCount(1L, binCount);
return null;
}
initTable方法
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
while ((tab = table) == null || tab.length == 0) {
// sizeCtl即阈值用于表的初始化和扩容
// -1 正在初始化
// < -1 多个线程正在进行调整大小操作,其绝对值为正在调整大小的线程数
// sizeCtl被声明为volatile,因为在多线程环境下,可能有多个线程同时对sizeCtl进行读写操作,为了保证所有线程都能看到最新的sizeCtl值
if ((sc = sizeCtl) < 0)
// 当前线程在尝试初始化table时,发现已经有其他线程进行初始化,所以让出CPU资源
Thread.yield(); // lost initialization race; just spin
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
// compareAndSwapInt是Unsafe类的方法,是一个原子操作
// SIZECTL是当前值,sc是期望值,-1是新值
// 如果SIZECTL == sc 返回true 将这个变量的值设置为新值-1 否则不做任何操作 返回false
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;
// 阈值等于table长度的3/4倍
sc = n - (n >>> 2);
}
} finally {
sizeCtl = sc;
}
break;
}
}
return tab;
}
helpTransfer方法
// 入参:当前的hash表tab和节点f(ForwardingNode节点)
// ForwardingNode是一个特殊的节点,不存储任何键值对,而是存储了一个指向新的hash表的引用。当其他线程在进行查询操作时,如果遇到ForwardingNode,则表面ConcurrentHashMap正在进行扩容,通过ForwardingNode中存储的引用找到新的hash表
final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {
Node<K,V>[] nextTab; int sc;
// tab不为空 && f是ForwardingNode类型 获取到ForwardingNode中存储的新hash表newTab
if (tab != null && (f instanceof ForwardingNode) &&
(nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) {
int rs = resizeStamp(tab.length) << RESIZE_STAMP_SHIFT;
// nextTab == nextTable && table == tab 表明当前hash表没有变化
// sizeCtl < 0 表明正在进行扩容操作
while (nextTab == nextTable && table == tab &&
(sc = sizeCtl) < 0) {
// 确定是否还需要继续扩容操作
if (sc == rs + MAX_RESIZERS || sc == rs + 1 ||
transferIndex <= 0)
break;
// CAS将sc + 1,如果成功则transfer扩容并跳出循环
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
transfer(tab, nextTab);
break;
}
}
return nextTab;
}
return table;
}
transfer方法
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
int n = tab.length, stride; // stride 步长
// NCPU是CPU核心数,也就是我们常说的机器配置4核16G中4
// 如果NCPU>1 则步长 = n / 8 / NCPU 保证多处理器环境下将hash表的部分区域分配给不同的处理器进行处理,以实现并发处理
// 如果NCPU<=1 则步长 = n
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
stride = MIN_TRANSFER_STRIDE; // subdivide range
// 说明是一个新的扩容操作 创建一个新的hash表,长度是旧hash表的2倍
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<K,V> fwd = new ForwardingNode<K,V>(nextTab);
// 控制循环进程 为true时,表示还要继续寻找下一个需要处理的元素的索引
boolean advance = true;
// 控制是否完成扩容
boolean finishing = false; // to ensure sweep before committing nextTab
// i表示当前正在处理的元素的索引,每次循环开始时,i都会自减1
// bound是当前处理的元素的边界,只有当i>=bound或者完成扩容时,才会停止寻找下一个元素
// nextIndex是下一个需要处理的元素索引(等于transferIndex),nextIndex<=0则i=-1并停止寻找下一个元素
for (int i = 0, bound = 0;;) {
Node<K,V> f; int fh;
// 这个while循环主要是为了尽可能利用多核CPU的并行处理能力,从而提升扩容效率
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))) { // nextIndex>0 尝试使用CAS操作更新transferIndex,并将i和bound设置为新的值。
bound = nextBound;
i = nextIndex - 1;
advance = false;
}
}
if (i < 0 || i >= n || i + n >= nextn) { // 当前索引是否超出边界 或者是否已经完成了扩容
int sc;
if (finishing) { // 如果已经完成了扩容 更新table 和 sizeCtl
nextTable = null;
table = nextTab;
sizeCtl = (n << 1) - (n >>> 1); // 扩容后的阈值 2*n*3/4
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指向的元素为空
advance = casTabAt(tab, i, null, fwd); // CAS将i指向的元素设置为fwd,并将advance设置为true
else if ((fh = f.hash) == MOVED) // 当前节点已经被处理过(转移到新hash表)
advance = true; // already processed
else {
synchronized (f) { // 锁住当前节点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;
}
}
// 如果runBit==0 则节点在新的hash表中的位置是i 否则为i + n
// 和HashMap扩容不同的是,扩容后元素在新hash表的位置,是通过hashcode和新数组长度与运算的
// 但是在ConcurrentHashMap,为了支持并发扩容,在扩容的过程中,扩容后元素在新hash表的位置,是通过节点的hash值和旧数组长度与运算的。如果结果为0,节点在新hash表的位置就是i,否则就是i + n,扩容过程可以分两步进行,先将原来的节点复制到新位置i,然后再讲节点复制到为止i + n,这两步操作可以由不同的线程并发执行
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)
// 低位 扩容到新表中的i
ln = new Node<K,V>(ph, pk, pv, ln);
else
// 高位 扩容到新表中的i + n
hn = new Node<K,V>(ph, pk, pv, hn);
}
// 将节点ln插入到nextTab的i位置
setTabAt(nextTab, i, ln);
// 将节点的hn插入到nextTab的i + n位置
setTabAt(nextTab, i + n, hn);
// 在旧hash表tab的索引i设置为fwd,fwd是一个ForwardingNode,是一个特殊节点,用于扩容操作中表示当前的节点已经被移动
// 扩容过程中,当一个桶的节点被移动到新的hash表后,会在旧hash表对应的位置插入一个ForwardingNode,表示这个位置的节点已经被转移到新的hash表,当其他线程访问旧的hash表时,如果看到一个ForwardingNode,就会去新的hash表查找
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) {
int h = e.hash;
TreeNode<K,V> p = new TreeNode<K,V>
(h, e.key, e.val, null, null);
if ((h & n) == 0) { // 同理 树节点转移到新hash表的i
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;
}
}
}
}
}
}
get方法
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) { // 找到对应hash桶
if ((eh = e.hash) == h) { // 如果是桶中第一个节点
if ((ek = e.key) == key || (ek != null && key.equals(ek))) // key相等的话,返回val
return e.val;
}
else if (eh < 0)
// eh < 0表示当前节点是一个特殊节点 在ConcurrentHashMap中有四种特殊的节点
// 1. MOVED:表示当前节点是一个ForwardingNode,表示当前节点已经被移动到新hash表中
// 2. TREEBIN:表示当前节点是一个TreeBin,是红黑树的根节点
// 3. TreeNode: 表示当前节点是一个红黑树节点
// 4. RESERVED:表示当前节点已经被其他线程预定,其他线程不能进行操作
// find() 方法有具体不同的实现类来实现
return (p = e.find(h, key)) != null ? p.val : null;
while ((e = e.next) != null) { // 其他情况下当做链表处理,遍历链表,找到对应的key,则返回对应的val
if (e.hash == h &&
((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
return null;
}
find方法
Node<K,V> find(int h, Object k) { // 这里是find的默认实现方法,遍历链表,找到对应的key,则返回对应的val
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;
}
未完待续...