有别于HashMap的线程不安全和HashTable的低效率(稍微看了一下源码,发现使用了大量的synchronized关键字修饰的同步方法),ConcurrentHashMap使用的是cas来保证整个元素插入、删除、扩容时候的同步安全。充分解决了HashMap和HashTable存在的问题。
下面需要对ConcurrentHashMap的源码做一些解读,让读者更好的理解ConcurrentHashMap的底层运行逻辑。
如果我们在new一个ConcurrentHashMap的时候给定参数,那么put之后,该ConcurrentHashMap的初始容量为大于给定参数的2的幂次方,比如
// 给定参数32,那么在put之后chm的初始容量为64(JDK7是还是32)
ConcurrentHashMap chm = new ConcurrentHashMap(32);
原因 是ConcurrentHashMap源码
public ConcurrentHashMap(int initialCapacity) {
if (initialCapacity < 0)
throw new IllegalArgumentException();
//给定参数大于最大容量的1/2容量?若大于,初始容量为最大值2^30
int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
MAXIMUM_CAPACITY :
//参数是32的话,传入tableSizeFor的参数是32+16+1=49
tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
this.sizeCtl = cap;
}
如果并不大于最大容量的1/2,调用以下函数。
这段代码是指,右移多少位,就把最高位右边的第x位设置为1;第一次,就把最右边为1;第二次,就把右边第2位再设置为1;第3次,就把右边第3位再设置为1;这样执行完,原来是110000(48),变成了111111,最后加1,就变成2的整数次方数了(64)。
private static final int tableSizeFor(int c) {
// 传入49
int n = c - 1;
// n=48
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
// 此时n=63,最后经过两次判断后返回值为64
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
sizeCtl = 0
,代表数组未初始化,且数组初始容量为16
sizeCtl > 0
,如果数组未初始化,那么其记录的是数组的初始容量,如果数组已经初始化,那么其记录的是数组的扩容阈值
sizeCtl = -1
,表示数组正在进行初始化
sizeCtl < 0 && sizeCtl != -1
,表示数组正在扩容,-(1+n),表示此时有n个线程正在共同完成数组的扩容操作。
首先我们来看ConcurrentHashMap的添加元素过程的源码
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();
// 基于key计算hash值,并进行一定的扰动(目的是使结果分步平均)
// 这个值一定是一个整数,方便后面添加元素,判断该节点的类型
int hash = spread(key.hashCode());
//记录某个桶上元素的个数,如果超过8个,会转成红黑树
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
//如果数组还未初始化,先对数组进行初始化
if (tab == null || (n = tab.length) == 0)
// 解读源码1,数组初始化
tab = initTable();
// if判断是指,hash函数计算得到的数组下标对应的桶中若为空,就利用cas直接把元素放入数组
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
// 在这里也使用了cas自旋锁操作。因为有可能有两个线程进入当前位置,确保只能有一个线程访问临界资源
if (casTabAt(tab, i, null,new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
// 如果hash计算得到的桶位置元素的hash值为MOVED,证明正在扩容,那么协助扩容
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
V oldVal = null;
// 保证这个位置的桶元素插入时线程安全的,即对桶加锁
// 不影响其他元素的桶位置插入;既保证安全,又不影响效率
// hashtable则是锁了整个数组
synchronized (f) {
// 保证还在该位置,比如变成树或者扩容之后,位置改变了
if (tabAt(tab, i) == f) {
// 判断hash值大于0 ,就表示当前情况下该位置桶还是链式结构
if (fh >= 0) {
binCount = 1;
// 遍历链表
for (Node<K,V> e = f;; ++binCount) {
K ek;
// 如果在链表中找到了put中key值,那么就替换
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
// 完成替换之后就跳出循环
break;
}
// 如果没有找到该值,就在使用尾插法将Entry插入链表的尾部
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;
binCount = 2;
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
// 以上就是ConcurrentHashMap添加元素的安全操作
// 从上面代码可以得到,ConcurrentHashMap是通过对桶加锁而不是对整个数组加锁,对效率有提高
if (binCount != 0) {
// 如果元素个数大于等于8且数组长度大于64,就变成了树
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}
通过源码分析可以得到,ConcurrentHashMap插入元素大体来说和HashMap差不多,不同的是,ConcurrentHashMap添加了不少同步操作,如图红色标记,这样就实现了同步安全。
不同于HashTable的是,ConcurrentHashMap主要采用的是CAS自旋锁,提高了效率。
此外,ConcurrentHashMap锁的对象是数组中的每一个桶而不是整个数组,这就意味着,在多线程操作的时候,同一个数组不同的桶之间操作不影响,也就是说,同一个时间,可以有多个线程对数组有插入元素的操作,提高了效率。
// 数组初始化
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
// table 表示初始数组
// 进行cas+自旋锁,保证线程安全,对数进行初始化
while ((tab = table) == null || tab.length == 0) {
// 如果sizeCtl小于0,说明此时正在初始化,让出cpu
if ((sc = sizeCtl) < 0)
Thread.yield(); // lost initialization race; just spin
// cas修改sizeCtl的值为-1,如果修改成功,进行数组初始化,如果修改失败,继续自选
// 就是sc和SIZECTL对比
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
// 完成yield后,sc不是小于0
if ((tab = table) == null || tab.length == 0) {
// 如果sizeCtl值为0,取默认长度16;否则取sizeCtl中的值
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*n
sc = n - (n >>> 2);
}
} finally {
// 最后将扩容阈值赋值给sizeCtl
sizeCtl = sc;
}
break;
}
}
return tab;
}
通过对initTable源码和putVal源码的阅读比较,发现二者在实现同步的过程中都采用cas自旋锁来实现同步的,极大提高了资源。
在源码中大量使用了 Unsafe.compareAndSwapInt(Object 0, long offset, int expected, int x)
,此方法是Java的native方法,并不由Java语言实现。
方法的作用是,读取传入对象o在内存中偏移量为offset位置的值与期望值expected作比较。相等就把x值赋值给offset位置的值。方法返回true。不相等,就取消赋值,方法返回false。这也是CAS的思想,及比较并交换。用于保证并发时的无锁并发的安全性。
CAS程序流程图
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
int n = tab.length, stride;
// 如果是多cpu,那么每个线程划分任务,最小任务量是16个桶位的迁移
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
stride = MIN_TRANSFER_STRIDE; // MIN_TRANSFER_STRIDE = 16
// 如果是扩容线程,此时新数组为null
// 计算最少任务量
if (nextTab == null) {
// initiating
try {
@SuppressWarnings("unchecked")
// 创建新的数组,数组长度为原来数组的两倍
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1]; //左移0.5倍,右移2倍
nextTab = nt;
} catch (Throwable ex) {
// try to cope with OOME
sizeCtl = Integer.MAX_VALUE;
return;
}
nextTable = nextTab;
// 记录线程开始迁移的桶位,从后往前迁移
transferIndex = n;
}
// 记录新数组的末尾
int nextn = nextTab.length;
// 如果桶位已经被迁移,会用ForwardingNode占位(这个节点的hash值为-1--MOVED)
ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
boolean advance = true;
boolean finishing = false; // to ensure sweep before committing nextTab
// 自旋,i记录当前正在迁移桶位的索引值
for (int i = 0, bound = 0;;) {
Node<K,V> f; int fh;
// 计算每一个线程到底负责多少元素的迁移
while (advance) {
int nextIndex, nextBound;
// bound记录下一次任务迁移的开始桶位
// --i >= bound 成立表示当前线程分配的迁移任务还没有完成
if (--i >= bound || finishing)
advance = false;
// 没有元素需要迁移 -- 后续会去将扩容线程数减1,并判断扩容是否完成
else if ((nextIndex = transferIndex) <= 0) {
i = -1;
advance = false;
}
// 主要计算是在这里,这里的迁移是从后往前迁移
// 计算下一次任务迁移的开始桶位,并将这个值赋值给transferIndex
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;
// 是否所有线程都做完了
// 扩容结束后,保存新数组,并重新计算扩容阈值,赋值给sizeCtl
if (finishing) {
nextTable = null;
table = nextTab;
// 这一行代码是说2 * n - 0.5 * n = 1.5 * n = 0.75 * 2n,位运算效率高
sizeCtl = (n << 1) - (n >>> 1);
return;
}
// 判断扩容操作是否完成
// 扩容任务线程数减1
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
}
}
//当前迁移的桶位没有元素,直接在该位置添加一个fwd节点
else if ((f = tabAt(tab, i)) == null)
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) {
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);
}
setTabAt(nextTab, i, ln);
setTabAt(nextTab, i + n, hn);
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) {
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;
}
}
}
}
}
}
如果是多核CPU的前提下,那么每个线程划分任务,最小任务量是16个桶位的迁移。
在迁移过程中,通过自旋来控制整个过程的持续性,直到所有线程完成扩容任务。
对于桶位来说,如果桶位已经被迁移,会用ForwardingNode占位(这个节点的hash值为-1–MOVED)。使用advance标记线程是否完成扩容。那么,如果说当前迁移的桶位没有元素,那该怎么办呢?在源码中是直接在该位置添加一个fwd节点
在扩容的时候,需要计算下一次任务迁移的开始桶位,并将这个值赋值给transferIndex,这个过程是用cas完成的。
如果当前桶位需要被迁移,就好比在当前桶位插入数据一样,需要使用synchronized关键字来为该桶位加锁,保证多线程安全。
多线程协助扩容的操作会在两个地方被触发:
① 当添加元素时,发现添加的元素对应的桶位为fwd节点,就会先去协助扩容,然后再添加元素
② 当添加完元素后,判断当前元素个数达到了扩容阈值,此时发现sizeCtl的值小于0,并且新数组不为空,这个时候,会去协助扩容
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();
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
//发现此处为fwd节点,协助扩容,扩容结束后,再循环回来添加元素
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
//省略代码
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;
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
//扩容,传递一个不是null的nextTab
transfer(tab, nextTab);
break;
}
}
return nextTab;
}
return table;
}
① CounterCell数组不为空,优先利用数组中的CounterCell记录数量
② 如果数组为空,尝试对baseCount进行累加,失败后,会执行fullAddCount逻辑
③ 如果是添加元素操作,会继续判断是否需要扩容
private final void addCount(long x, int check) {
CounterCell[] as; long b, s;
// 一开始counterCells不为空,所以前面一个判断不成立
if ((as = counterCells) != null ||
// 在单线程的条件下,将s=b+1,加成功之后compareAndSwapLong返回true,取反为false,所以不进入代码,直接加入成功
// 当有两个以上的线程进入这个位置,那么必然有一个线程加成功,其他线程加失败,所以返回false,取反返回true,进入代码块
!U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
CounterCell a; long v; int m;
boolean uncontended = true;
// 数组为空,或数组不存在(长度小于0)
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;
// sumCount是获取当前数组长度
s = sumCount();
}
// 移除或替换元素
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);
// 第一次sc不会小于0
if (sc < 0) {
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
transferIndex <= 0)
break;
// 协助扩容,nt指的是新的数组
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
transfer(tab, nt);
}
// 移完之后,最高位是1,所以变为sc为负数,所以sizeCtl也小于0
else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
// 扩容
transfer(tab, null);
s = sumCount();
}
}
}
① 当CounterCell数组不为空,优先对CounterCell数组中的CounterCell的value累加
② 当CounterCell数组为空,会去创建CounterCell数组,默认长度为2,并对数组中的CounterCell的value累加
③ 当数组为空,并且此时有别的线程正在创建数组,那么尝试对baseCount做累加,成功即返回,否则自旋
// See LongAdder version for explanation
private final void fullAddCount(long x, boolean wasUncontended) {
int h;
if ((h = ThreadLocalRandom.getProbe()) == 0) {
ThreadLocalRandom.localInit(); // force initialization
h = ThreadLocalRandom.getProbe();
wasUncontended = true;
}
boolean collide = false; // True if last slot nonempty
// 整个过程没有加锁动作,只是使用cas+自旋的动作
for (;;) {
CounterCell[] as; CounterCell a; int n; long v;
// 数组不为空
if ((as = counterCells) != null && (n = as.length) > 0) {
// 创建CounterCell对象,并对CounterCell中的value累加值,若成功,则结束循环
if ((a = as[(n - 1) & h]) == null) {
if (cellsBusy == 0) {
// Try to attach new Cell
CounterCell r = new CounterCell(x); // Optimistic create
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) {
rs[j] = r;
created = true;
}
} finally {
cellsBusy = 0;
}
if (created)
break;
continue; // Slot is now non-empty
}
}
collide = false;
}
// 如果wasUncontended==false,那么rehash,然后asUncontended设置为true
else if (!wasUncontended) // CAS already known to fail
wasUncontended = true; // Continue after rehash
// 桶位value累加成功结束循环
else if (U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))
break;
// 有别的线程对数组扩容/数组容量达到最大值就是cpu的核数,并rehash
else if (counterCells != as || n >= NCPU)
collide = false; // At max size or stale
// collide=true,并rehash
else if (!collide)
collide = true;
// 数组进行扩容,成功后继续循环
else if (cellsBusy == 0 &&
U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
try {
if (counterCells == as) {
// Expand table unless stale
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
}
h = ThreadLocalRandom.advanceProbe(h);
}
// 数组为空,先创建数组
else if (cellsBusy == 0 && counterCells == as &&
U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
boolean init = false;
try {
// Initialize table
if (counterCells == as) {
CounterCell[] rs = new CounterCell[2];
rs[h & 1] = new CounterCell(x);
counterCells = rs;
init = true;
}
} finally {
cellsBusy = 0;
}
if (init)
break;
}
// 数组正在被创建,且数组为空,baseCount++
else if (U.compareAndSwapLong(this, BASECOUNT, v = baseCount, v + x))
break; // Fall back on using base
}
}
public int size() {
long n = sumCount();
return ((n < 0L) ? 0 :
(n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE :
(int)n);
}
final long sumCount() {
CounterCell[] as = counterCells; CounterCell a;
//获取baseCount的值
long sum = baseCount;
if (as != null) {
//遍历CounterCell数组,累加每一个CounterCell的value值
for (int i = 0; i < as.length; ++i) {
if ((a = as[i]) != null)
sum += a.value;
}
}
return sum;
}
注意:这个方法并不是线程安全的
ConcurrentHashMap与HashMap思路差不多,但是ConcurrentHashMap支持并发操作。
整个ConcurrentHashMap由一个个Segment组成,其作用就是用于分段锁。
Segment继承自ReentrantLock加锁。
所以每一个加锁锁住的是一个个segment,确保每个segment是安全的,那么全局也是安全的。
与HashMap类似的,Java 7 中的ConcurrentHashMap的底层也是数组+链表。
Java 8 中的ConcurrentHashMap则是数组+链表+红黑树的结构实现
ConcurrentHashMap的ConcurrentLevel(并发级别)默认有16个Segments,理论上最多可以同时支持16个线程并发,只要它们的操作分布在不同的Segment上。
这个值(ConcurrentLevel)最初可以设置为其他值,但一旦初始化后,就不可以再扩容。
细化到Segment内部,其实每一个Segment相当于一个HashMap,不过要保证线程安全,所以要更麻烦些。
【Java自顶向下】ConcurrentHashMap面试题(2021最新版)