ConcurrentHashMap(JDK1.8)实现线程安全的思想也已经完全变了,它摒弃了Segment(锁段)的概念,而是启用了一种全新的方式实现,利用CAS算法。它沿用了与它同时期的HashMap版本的思想,底层依然由“数组”+链表+红黑树的方式思想。
前言
在学习 ConcurrentHashMap 源码之前你需要知道以下知识:
- HashMap 源码分析(JDK1.8)
(由于 ConcurrentHashMap 与 HashMap 基本算法一致,建议先学习 HashMap) - ConcurrentHashMap 中运用到了很多实现线程安全类的技巧:线程封闭、可见性、不变性等,可以参考:Java并发编程实战(基础篇)
1.重要属性
这些属性中需要对sizeCtl
认真理解,在后面的操作中sizeCtl
起到了非常重要的作用。
/**
* 在 ConcurrentHashMap 中将负载因子改为了常量 0.75
* 在 ConcurrentHashMap 取消了 HashMap 中 loadFactor 属性
*/
private static final float LOAD_FACTOR = 0.75f;
/**
* 单个线程在扩容时处理 table 桶的最小数量(一个线程最少复制 table 中16个桶的元素)
*/
private static final int MIN_TRANSFER_STRIDE = 16;
// 下面两个属性都是用于扩容时,对容量或者sizeCtl进行位运算移位处理的数据(只要记住为 16 即可)
/**
* The number of bits used for generation stamp in sizeCtl.
* Must be at least 6 for 32bit arrays.
*/
private static int RESIZE_STAMP_BITS = 16;
/**
* The bit shift for recording size stamp in sizeCtl.
*/
private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;
/**
* 标识了 Node hash值对应的状态
*/
static final int MOVED = -1; // hash for forwarding nodes
static final int TREEBIN = -2; // hash for roots of trees
/**
* 存储数据的数组
*/
transient volatile Node<K,V>[] table;
/**
* 扩容时使用的辅助数组(只能在扩容时使用)
*/
private transient volatile Node<K,V>[] nextTable;
/**
* 控制标识符 - 在不同的时期拥有不同的作用,取值不同也代表不同的含义
* -1: 正在初始化
* <-1: sizeCtl 低16位存储着 (扩容线程数+1)
* 0: table 还未初始化
* > 0: 初始化容量或下一次扩容的阈值 (如果为正数,始终为容量的 0.75,相当于 HashMap 的 threshold)
*/
private transient volatile int sizeCtl;
/**
* 扩容时,下一个线程复制 table 数据的开始索引
*/
private transient volatile int transferIndex;
2.重要内部类
2.1 Node
Node 是最核心的内部类,它包装了 key-value 键值对,所有插入 ConcurrentHashMap 的数据都包装在这里面。它与 HashMap 中的定义很相似,但是但是有一些差别它对 value 和 next 属性设置了 volatile 同步锁,它不允许调用 setValue 方法直接改变 Node 的 value 域,它增加了 find 方法辅助 map.get() 方法(简化了 get 方法代码)。
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
volatile V val;
volatile Node<K,V> next;
// 其他代码省略
public final V setValue(V value) {
throw new UnsupportedOperationException();
}
/**
* Virtualized support for map.get(); overridden in subclasses.
*/
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;
}
}
2.2 TreeNode
和 HashMap 一样,当链表长度过长的时候,Node 会转为 TreeNode。但是与HashMap 不相同的是,它并不是直接转换为红黑树,而是把这些结点包装成TreeNode 放在 TreeBin 对象中,由 TreeBin 完成对红黑树的包装。而且TreeNode 在 ConcurrentHashMap 集成自 Node 类,而并非 HashMap 中的集成自 LinkedHashMap.Entry<K,V> 类。
2.3 TreeBin
这个类并不负责包装用户的key、value信息,而是包装的很多TreeNode节点。它代替了TreeNode的根节点,也就是说在实际的ConcurrentHashMap“数组”中,存放的是TreeBin对象,而不是TreeNode对象,这是与HashMap的区别。另外这个类还带有了读写锁。
可以看到在构造TreeBin节点时,仅仅指定了它的hash值为 TREEBIN(-2) 常量,这也就是个标识为。同时也看到我们熟悉的红黑树构造方法
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
/**
* Creates bin with initial set of nodes headed by b.
*/
TreeBin(TreeNode<K,V> b) {
super(TREEBIN, null, null, null);
// TreeNode 根结点
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);
}
}
2.4 ForwardingNode
一个用于连接 table 和 nextTable 的节点类,只有在扩容时才会出现。它包含一个 nextTable 指针,用于指向下一张扩容之后的表。而且这个节点的 key、value 、next 指针全部为null,它的hash值为-1(在扩容时,表示当前桶中元素已经复制完成)。find的方法是从 nextTable 里进行查询节点。
/**
* 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);
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;
}
else
return e.find(h, k);
}
if ((e = e.next) == null)
return null;
}
}
}
}
3.Unsafe 与 CAS
在ConcurrentHashMap中,随处可以看到U, 大量使用了U.compareAndSwapXXX的方法,这个方法是利用一个CAS算法实现无锁化的修改值的操作,他可以大大降低锁代理的性能消耗。这个算法的基本思想就是不断地去比较当前内存中的变量值与你指定的一个变量值是否相等,如果相等,则接受你指定的修改的值,否则拒绝你的操作。因为当前线程中的值已经不是最新的值,你的修改很可能会覆盖掉其他线程修改的结果。是一种乐观锁的思想。
3.1 Unsafe 静态代码块
Unsafe.objectFieldOffset(Field var1)
方法:获取了 ConcurrentHashMap 中一些重要属性(字段)相对Java对象的“起始地址”的偏移量,这样就可以调用 CAS 方法来使用前面的偏移量访问、修改某个属性(字段),这样实现了偏移量和属性的双向绑定。
// 这段代码就是通过 sizeCtl 的偏移量 SIZECTL,来调用 compareAndSwapInt 方法
U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)
在后面的源码中,这种代码非常多,作用就是 CAS 来为某个变量赋值。
// Unsafe mechanics
// 定义了属性、变量的偏移量
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);
}
}
3.2 三个核心方法
ConcurrentHashMap 定义了三个原子操作,用于对指定位置的节点进行操作。正是这些原子操作保证了 ConcurrentHashMap 的线程安全。
@SuppressWarnings("unchecked")
/**
* 获取数组 tab,桶i 的结点 Node
*/
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 交换数组 tab,桶i 的 Node节点
*/
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);
}
/**
* 设置数组 tab,桶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);
}
4.方法分析
分析常用方法以及核心方法
4.1 构造方法
ConcurrentHashMap 构造方法与 HashMap 基本相似,这里主要讲一个在构造方法ConcurrentHashMap(Map<? extends K, ? extends V> m)
中 putAll(Map<? extends K, ? extends V> m)
方法中的 tryPresize(int size)
方法。
tryPresize(int size)
:尝试预先调整表的大小以容纳给定数量的元素
/**
* Tries to presize table to accommodate the given number of elements.
* 在向 table 加入元素之前,先检查 table 是否初始化或扩容
* @param size number of elements (doesn't need to be perfectly accurate)
*/
private final void tryPresize(int size) {
/**
* 1.size >= 最大容量的一半:那容量就为最大容量
* 2.size < 最大容量的一半:将 size 作为阈值计算出所需容量
* size + (size >>> 1) + 1 这种方式和 HashMap 的方法类似,详细
* 查看 HashMap 源码分析
*/
int c = (size >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY :
tableSizeFor(size + (size >>> 1) + 1);
int sc;
// sizeCtl >= 0:还未初始化或扩容容量或下一次扩容阈值
while ((sc = sizeCtl) >= 0) {
Node<K,V>[] tab = table; int n;
// table 未初始化
if (tab == null || (n = tab.length) == 0) {
/**
* sc表示构造方法传入容量或默认容量
* c 表示size所需容量
* 两者较大值作为 table 的容量大小(要容纳 m 的元素)
*/
n = (sc > c) ? sc : c;
// CAS 设置 sizeCtl=-1 表示当前正在初始化状态
if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
// 防止其他线程提前初始化了 table
if (table == tab) {
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
table = nt;
// 计算阈值下面计算相当于 n*0.75
sc = n - (n >>> 2);
}
} finally {
sizeCtl = sc;
}
}
}
/**
* 如果size所需容量小于起始容量或 table 已经初始化
* table 不需要初始化或扩容,退出方法
*/
else if (c <= sc || n >= MAXIMUM_CAPACITY)
break;
// table 不为 null,table 处于需要扩容或正在初始化或正在扩容
else if (tab == table) {
// 获取当前 table 容量的戳(后面会讲解该方法)
int rs = resizeStamp(n);
// sc<0:table 正在扩容或者正在初始化
if (sc < 0) {
Node<K,V>[] nt;
/**
这几个判断都是判断当前线程是否需要去协助扩容
* 1.(sc >>> RESIZE_STAMP_SHIFT) != rs
* 比较 sc 高16位的戳与上面获取的戳是否相同
* 相同:table 容量还是n,扩容未完成
* 不同:table 扩容或初始化完成
* 2.sc == rs + 1 和 sc == rs + MAX_RESIZERS
* 这两个语句不太懂?有人会的评论留言
* 3.(nt = nextTable) == null
* 扩容期间的辅助 table 为空,扩容结束
* 4.transferIndex <= 0
* 下一个线程赋值 nextTable 开始的索引小于等于0
* 表示扩容结束
*/
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
transferIndex <= 0)
break;
// 协助扩容, sizeCtl+1
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
// 当前线程调用扩容方法
transfer(tab, nt);
}
// 当前线程开始扩容 sizeCtl = (rs << RESIZE_STAMP_SHIFT) + 2
else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
transfer(tab, null);
}
}
}
这里需要注意:当前线程主动扩容时,为什么执行sizeCtl = (rs << RESIZE_STAMP_SHIFT) + 2
语句?
语句执行的操作:将rs(获取容量的戳)向左移位16位(即高16位存储容量戳),再加2(低位16位存储 扩容线程数-1)。
为什么第一次主动扩容的线程要 +2?在后面的 transfer方法会讲到。
4.2 resizeStamp(int n)
resizeStamp(int n)
:获取容量n的戳。
在上面的方法中已经讲到了这个方法的用处:与 sizeCtl 高16位进行比较,来判断当前容量的戳是否一致。
static final int resizeStamp(int n) {
return Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1));
}
Integer.numberOfLeadingZeros(n)
:获取最高位1之前0的个数
因为容量 n 是2的幂次方,所以每个容量最高位1之前0的个数都不同
1 << (RESIZE_STAMP_BITS - 1)
:将1左移15位
将上面两个结果求或:**得到一个容量 n 特有的16位二进制戳。
4.3 putVal(K key, V value, boolean onlyIfAbsent)
基本思路与 HashMap 相似。
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
// 与HashMap的hash方法基本相同
int hash = spread(key.hashCode());
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
// table 未初始化
if (tab == null || (n = tab.length) == 0)
// 调用初始化方法,下面分析
tab = initTable();
// 获取 (n - 1) & hash 桶的结点,如果为空
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
// CAS将新的结点设置在table 的i桶
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
/**
* MOVED=-1
* 表示当前结点为fwd(table正在扩容)
* 当前线程停止添加,先去协助扩容
*/
else if ((fh = f.hash) == MOVED)
// 协助扩容,下面分析
tab = helpTransfer(tab, f);
// 当前桶i中存在结点
else {
V oldVal = null;
// 使用桶i的第一个结点,为桶i独占加锁(访问桶i的线程只能等待)
synchronized (f) {
// 再次判断桶i第一个结点是否为之前获取的f结点
if (tabAt(tab, i) == f) {
// hash>=0:链表
if (fh >= 0) {
// 记录链表中元素个数,用于判断是否扩展为红黑树
binCount = 1;
// 下面步骤与HashMap相同不在赘述
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;
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) {
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那么就将链表扩展为红黑树
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
// table 中元素个数 +1(可能会加在baseCount或者CounterCell[]中)
addCount(1L, binCount); // 下面分析
return null;
}
看了 putVal 方法源码基本思路与 HashMap 相似,那么接下来就方法中新出现的方法进行分析。
4.4 initTable()
代码基本和前面tryPresize(int size)
方法相似。
/**
* 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) {
// sc<0:正在初始化或扩容,当前线程让出CPU,重新自旋
if ((sc = sizeCtl) < 0)
Thread.yield(); // lost initialization race; just spin
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);
}
} finally {
sizeCtl = sc;
}
break;
}
}
return tab;
}
4.5 helpTransfer(Node<K,V>[] tab, Node<K,V> f)
helpTransfer()
:在put方法中,如果 table 正在扩容,那么当前线程协助扩容。
线程去协助扩容主要工作就是将原table的数据复制到nextTable中。
/**
* 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;
// table 不为空,f结点为转发结点,nextTable已经创建完成
if (tab != null && (f instanceof ForwardingNode) &&
(nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) {
// 记录原 table 容量的戳
int rs = resizeStamp(tab.length);
// 正在扩容状态
while (nextTab == nextTable && table == tab &&
(sc = sizeCtl) < 0) {
// 和 tryPresize 方法中的判断语句作用一样
// 判断是否扩容完成
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || transferIndex <= 0)
break;
// 扩容没有完成,当前线程去协助扩容
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
transfer(tab, nextTab);
break;
}
}
return nextTab;
}
return table;
}
4.6 addCount(long x, int check)
addCount(long x, int check)
:向 table 元素进行计数,x 是需要添加的数量,check 大小标志着是否需要扩容检查(基本都是需要检查的)。
/**
* Adds to count, and if table is too small and not already
* resizing, initiates transfer. If already resizing, helps
* perform transfer if work is available. Rechecks occupancy
* after a transfer to see if another resize is already needed
* because resizings are lagging additions.
*
* @param x the count to add
* @param check if <0, don't check resize, if <= 1 only check if uncontended
*/
private final void addCount(long x, int check) {
CounterCell[] as; long b, s;
/**
* 1.(as = counterCells) != null
* counterCells[]:大小与CUP数量相关,存储着table的元素个数
* 通过 sumCount 方法(计算table中元素个数)可知,元素个数
* 由 baseCount 和 countCells[] 中所有个数总和组成,设计师
* 创建 countCells[] 可能是为了缓解只有一个baseCount来记录
* 总的元素个数带来的并发压力吧
* countCells不为null,直接进入if将x加到其中一个索引下的值
* 2.CAS 为baseCount+x,如果操作成功,那么就不需要再向countCells中添加
*/
if ((as = counterCells) != null ||
!U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
CounterCell a; long v; int m;
boolean uncontended = true;
/**
* 1.as == null || (m = as.length - 1) < 0
* 判断as是否为空,为空进入代码块
* 2.(a = as[ThreadLocalRandom.getProbe() & m]) == null
* 以当前线程获取随机数与m(as容量为2的幂次方)获得数组索引
* 为空进入代码块
* 3.走到这步说明 a 不为空,CAS向a+x,如果失败进入下面代码块
*/
if (as == null || (m = as.length - 1) < 0 ||
(a = as[ThreadLocalRandom.getProbe() & m]) == null ||
!(uncontended =
U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
// 向countCells中+x,下面分析
fullAddCount(x, uncontended);
return;
}
// 如果 check<=1 不需要判断扩容
if (check <= 1)
return;
// 重新统计table元素个数
s = sumCount();
}
// 扩容检查
if (check >= 0) {
Node<K,V>[] tab, nt; int n, sc;
// table元素个数大于阈值 且 table不为空 且 table容量小于最大容量
while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
(n = tab.length) < MAXIMUM_CAPACITY) {
// 进行扩容,记录当前容量 n 的戳
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;
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
transfer(tab, nt);
}
else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
transfer(tab, null);
// 再次统计数组元素,循环继续判断是否需要扩容
s = sumCount();
}
}
}
4.7 fullAddCount(long x, boolean wasUncontended)
上面的 addCount 方法可知:countCells未初始化、countCells对应的桶未初始化、CAS baseCount失败
;出现这三种情况就会调用fullAddCount
方法,来向计数缓存数组countCells
中增加计数 x;
wasUncontended
:countCells的某个桶是否出现线程竞争,如果出现竞争,那么就需要重新获取线程随机值来访问countCells别的桶(false 表示存在竞争)。
下面是 ConcurrentHashMap 元素统计的大致流程图:
这只是一个大致的示意图,具体情况见代码分析。
// See LongAdder version for explanation
private final void fullAddCount(long x, boolean wasUncontended) {
int h;
/**
* 判断当前线程的随机值是否初始化
* ThreadLocalRandom类保证了每个线程都有自己的随机种子
* probe 记录调用线程的threadLocalRandomSeed的偏移量,每个线程都不同
* 可以认为每个线程的probe就是它在CounterCell数组中的hash值
*/
if ((h = ThreadLocalRandom.getProbe()) == 0) {
// 该静态方法会初始化当前线程所持有的随机值
ThreadLocalRandom.localInit(); // force initialization
// 获取生成的probe值,用作选择countCells数组下标元素
h = ThreadLocalRandom.getProbe();
// 并未存在桶竞争
wasUncontended = true;
}
/**
* 最后一个桶不为空就设为true(我是这样理解这个变量的作用,如有问题评论指正)
* 这是是否扩容的标志,在countCells存在的情况下,当数组所有的桶都已经初始化
* 并向countCells中的桶进行CAS计数尝试失败,就会将collide设为true
* 接下来就会对countCells进行扩容操作
*/
boolean collide = false; // True if last slot nonempty
for (;;) {
CounterCell[] as; CounterCell a; int n; long v;
// 一、如果countCells不为null
if ((as = counterCells) != null && (n = as.length) > 0) {
// 1.通过随机值获取的桶为空,尝试对桶初始化
if ((a = as[(n - 1) & h]) == null) {
if (cellsBusy == 0) { // Try to attach new Cell
// 创建value为x的CounterCell
CounterCell r = new CounterCell(x); // Optimistic create
// CAS cellsBusy 为1,当前线程独占,别的线程无法进行扩容
// 或桶的初始化操作
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) {
// 将CounterCell 插入桶中
rs[j] = r;
created = true;
}
} finally {
// 恢复标识位
cellsBusy = 0;
}
if (created)
break;
continue; // Slot is now non-empty
}
}
// 未成功
collide = false;
}
/**
* 2.在addCount方法中最后一个判断尝试对不为空的桶CAS添加计数失败,
* 就将未冲突设为true,重新获取随机值(伪随机值)
*/
else if (!wasUncontended) // CAS already known to fail
wasUncontended = true; // Continue after rehash
// 3.向当前桶CAS添加计数,成功就退出循环
else if (U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))
break;
// 4.countCells 正在扩容 或 超过最大容量
else if (counterCells != as || n >= NCPU)
collide = false; // At max size or stale
// 5.countCells遍历到最后一个桶都未添加计数成功,那么下一次循环可能会进行扩容操作
else if (!collide)
collide = true;
// 6.扩容countCells数组
else if (cellsBusy == 0 &&
U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
try {
if (counterCells == as) {// Expand table unless stale
// 大小变为2倍
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);
}
// countCells 为空,进行初始化操作,CAS cellsBusy 为1
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];
// 与1,数组下标要么是0、要么是1
rs[h & 1] = new CounterCell(x);
counterCells = rs;
init = true;
}
} finally {
cellsBusy = 0;
}
if (init)
break;
}
// 竞争激烈,其它线程占据countCells数组,尝试CAS baseCount来计数
else if (U.compareAndSwapLong(this, BASECOUNT, v = baseCount, v + x))
break; // Fall back on using base
}
}
看了分析fullAddCount
还是较为复杂的,那么接下来总结一下大致流程:
- 判断countCells是否为空(如果不为空);
1.判断当前线程通过probe寻址的桶是否为空(如果为空,初始化桶并插入元素)
2.CAS向桶中添加计数(成功则退出)
3.重新计算probe再次寻址,重复上面步骤,当数组的桶都被初始化还是没有添加计数成功
4.扩容数组,再次CAS - countCells 为空,那么就初始化数组;
- 线程竞争,无法得到操作权(cellsBusy),那么就尝试CAS baseCount。
4.8 transfer(Node<K,V>[] tab, Node<K,V>[] nextTab)
经过上面的分析知道了 ConcurrentHashMap 如何线程安全的记录数组元素个数,那么接下来分析 ConcurrentHashMap 是如何安全地进行多线程扩容的。
先讲下ConcurrentHashMap扩容的大致思路:
/**
* Moves and/or copies the nodes in each bin to new table. See
* above for explanation.
*/
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
int n = tab.length, stride;
// 这个判断会根据CUP的个数来分配每个线程需要复制的数组桶的个数,默认最少16个
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
stride = MIN_TRANSFER_STRIDE; // subdivide range
// 第一次调用扩容方法,辅助数组nextTable为空,先初始化nextTable
if (nextTab == null) { // initiating
try {
@SuppressWarnings("unchecked")
// 新的数组是原数组的2倍
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;
// 注意:将原数组的大小 n 赋给了 transferIndex
// 赋值为n,表示第一个线程从(n-1)索引开始复制
transferIndex = n;
}
int nextn = nextTab.length;
// 创建转发结点,并将nextTable存储到该结点内
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;
while (advance) {
int nextIndex, nextBound;
// 1.将i向后移一位
if (--i >= bound || finishing)
advance = false;
// 2.判断线程是否完成数组的所有复制任务
else if ((nextIndex = transferIndex) <= 0) {
// 如果完成就会将i置为-1
i = -1;
advance = false;
}
/**
* 3.计算下一个线程复制的开始索引、当前线程的复制范围(开始索引和结束索引)
* 每个线程第一次循环都会执行这个判断
* 假设n=transferIndex=32、stride=16,那么nextIndex=32
* nextBount通过计算为16,所以下一个线程的transferIndex=16.
* 而当前线程的结束索引bound=nextBound=16,
* 开始索引i=nextIndex-1=31
*/
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 = null;
table = nextTab;
sizeCtl = (n << 1) - (n >>> 1);
return;
}
// 完成数组复制之后,线程退出该方法,sizeCtl低16位存储的线程数-1
if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
/**
* 这个判断控制只有最后一个线程可以进入上面最终的赋值操作
* 下面会图解分析这个判断是如何控制的
*/
if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
return;
// 最后一个线程会重新将开始索引变为n,重新遍历一遍
// 检查是够全部复制完成,之后会调用上面方法退出transfer()
finishing = advance = true;
i = n; // recheck before commit
}
}
// 如果当前桶结点为空,那么不用复制直接置为fwd
else if ((f = tabAt(tab, i)) == null)
advance = casTabAt(tab, i, null, fwd);
// 表示当前桶结点为fwd,已经复制过,继续向前走
else if ((fh = f.hash) == MOVED)
advance = true; // already processed
// 链表或者红黑树
else {
// 将桶的第一个结点加锁,独占该桶
synchronized (f) {
if (tabAt(tab, i) == f) {
Node<K,V> ln, hn;
// 为链表,和 HashMap的扩容相同,将结点复制到i、i+n的位置
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);
}
// CAS设置新table对应位置的链表
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;
}
}
}
}
}
}
上面代码中遗留了一个问题:
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
}
这个判断为什么可以控制最后一个线程留下来执行最终的赋值操作?
我们分两种情况来分析:单线程扩容和多线程扩容
假设当前容量为n,戳为 0001000(表示16位二进制),而下面的单数(0、1、2。。)也表示16位二进制
1.单线程扩容
线程1:sizeCtl = (rs << RESIZE_STAMP_SHIFT) + 2 sizeCtl = 0001000 2
执行:U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)
sc = 0001000 2 sizeCtl = 0001000 1
判断:(sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT
resizeStamp(n) << RESIZE_STAMP_SHIFT = 0001000 0(将n的戳左移16位,那么低16位就为0)
sc - 2 = 0001000 0
所以该方法返回 false,最后一个线程1进入下面代码
2.多线程扩容
线程1:sizeCtl = (rs << RESIZE_STAMP_SHIFT) + 2 sizeCtl = 0001000 2
线程2:U.compareAndSwapInt(this, SIZECTL, sc, sc + 1) sizeCtl = 0001000 3
线程3:U.compareAndSwapInt(this, SIZECTL, sc, sc + 1) sizeCtl = 0001000 4
resizeStamp(n) << RESIZE_STAMP_SHIFT = 0001000 0 为定值
当扩容完成执行:U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)
线程1:sc = 0001000 4 sizeCtl = 0001000 3
判断:(sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT
sc - 2 = 0001000 2
返回true 线程1 return
U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)
线程2:sc = 0001000 3 sizeCtl = 0001000 2
判断:(sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT
sc - 2 = 0001000 1
返回true 线程2 return
U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)
线程3:sc = 0001000 2 sizeCtl = 0001000 1
判断:(sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT
sc - 2 = 0001000 0
返回 false 最后一个线程3进入下面代码
从上面的执行流程可以看出只有最后一个进入该判断的线程才会留下来执行最后的重复检查和扩容赋值操作,这也是为什么第一次开启扩容的线程需要(rs << RESIZE_STAMP_SHIFT) + 2
的原因。
4.9 ConcurrentHashMap 遍历
另写了一篇博客来分析ConcurrentHashMap的遍历,具体参考:
ConcurrentHashMap遍历 — 弱一致性的迭代器(Iterator)实现原理
那么别的方法较为简单这个就不在过多分析。
5.JDK1.7与JDK1.8的比较
其实可以看出 JDK1.8 版本的 ConcurrentHashMap 的数据结构已经接近HashMap,相对而言 ConcurrentHashMap 只是增加了同步的操作来控制并发,从 JDK1.7 版本的 ReentrantLock+Segment+HashEntry,到 JDK1.8 版本中synchronized+CAS+HashEntry+红黑树,相对而言,总结如下:
- JDK1.8的实现降低锁的粒度,JDK1.7版本锁的粒度是基于Segment的,包含多个HashEntry,而JDK1.8锁的粒度就是HashEntry(首节点);
- JDK1.8版本的数据结构变得更加简单,使得操作也更加清晰流畅,因为已经使用 synchronized 来进行同步,对桶的加锁算是更细粒度的分段锁,也就不需要Segment 这种数据结构了,由于粒度的降低,实现的复杂度也增加了;
- JDK1.8使用红黑树来优化链表(当链表的长度超过8将链表转化为红黑树),基于长度很长的链表的遍历是一个很漫长的过程,而红黑树的遍历效率是很快的,代替一定阈值的链表,这样形成一个最佳拍档;
- JDK1.8 使用内置锁 synchronized 来代替重入锁 ReentrantLock (JDK1.8 的 sync 效率已经不必 ReentrantLock差,而且基于JVM的synchronized优化空间更大,使用内嵌的关键字比使用API更加自然)
JDK1.7
JDK1.8