目录
介绍
相对线程安全:相对线程安全就是我们通常意义上所讲的线程安全,它需要保证对这个对象单次的操作是线程安全的,我们在调用的时候不需要进行额外的保障措施,但是对于一些特定顺序的连续调用,就可能需要在调用端使用额外的同步手段来保证调用的正确性。
数据结构:数组+链表+(红黑树+双向链表).与HashMap不同的是key和value不能为null. 红黑树的头节点是TreeBin,它的hash是TREEBIN = -2.
使用CAS和synchronized(链化的头节点)来保证put和扩容的线程安全。
在进行计算map中存在的映射数量的时候,使用到了类似于LongAdder的方法(int cellsBusy,CounterCell[] counterCells,long baseCount)。
在进行扩容的时候支持多个线程同时进行扩容的协助(大体思想可以联想到 redis的Hash类型进行扩容)。
源码分析
字段的含义
sizeCtl
-
sizeCtl>0
-
table==null 未进行初始化前:保存使用赋值“初始容量”的构造方法计算得到的初始容量。
|–构造方法 2.
-
已经初始化:表示扩容阈值
|–initTable方法的 2.3-2.4
-
-
sizeCtl=0
:使用默认的初始容量16。 |–initTable方法的 2.2
-
sizeCtl<0
:-
sizeCtl=-1
:在initTable方法中获得sizeCtl同步标志的锁,可能进行初始化操作。 |–initTable方法的 2
-
sizeCtl!=-1
:高16位:扩容时代表容量的标识戳使用resizeStamp方法进行计算得到。低16位代表为改容量正在扩容的线程数加1。如果为1,表示已经完成扩容;如果为0,表示还未进行扩容。
-
nextTable
扩容时创建的新的table, 大小为oldTable的二倍. 用于进行数据迁移. 当数据完成迁移之后, 会将table=nextTable, nextTable=null. 也就是说nextTable为null, 表示没有进行扩容.
ForwardingNode
在进行扩容的时候,每个线程都会创建一个ForwardingNode. 并将完成扩容的slot指向该ForwardingNode,表示数据在进行扩容的nextTable.
注意: nextTable指向扩容的新容器, hash为 MOVED = -1.
static final class ForwardingNode<K,V> extends Node<K,V> {
final Node<K,V>[] nextTable;
ForwardingNode(Node<K,V>[] tab) {
super(MOVED, null, null);
this.nextTable = tab;
}
}
//=================================================
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) {
this.hash = hash;
this.key = key;
this.val = val;
}
Node(int hash, K key, V val, Node<K,V> next) {
this(hash, key, val);
this.next = next;
}
}
构造方法
//loadFactor和concurrencyLevel在此处只是计算初始容量的大小
//concurrentHashMap的负载因子为:private static final float LOAD_FACTOR = 0.75f;
public ConcurrentHashMap(int initialCapacity,
float loadFactor, int concurrencyLevel) {
if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0)
throw new IllegalArgumentException();
//1. max(initialCapacity,concurrencyLevel)/loadFactor + 1 向下取整
if (initialCapacity < concurrencyLevel) // Use at least as many bins
initialCapacity = concurrencyLevel; // as estimated threads
long size = (long)(1.0 + (long)initialCapacity / loadFactor);
int cap = (size >= (long)MAXIMUM_CAPACITY) ?
MAXIMUM_CAPACITY : tableSizeFor((int)size);
//2. 将计算得到的初始容量赋值给sizeCtl
this.sizeCtl = cap;
}
tabkeSizeFor方法
//得到>=c的2的次幂整数
private static final int tableSizeFor(int c) {
//此处的运算和HashMap相同,Integer.numberOfLeadingZeros(c - 1)得到c-1的“最高阶”之前的位数
int n = -1 >>> Integer.numberOfLeadingZeros(c - 1);
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
tabAt方法
//|--寻找table中i下标的节点
static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
return (Node<K,V>)U.getObjectAcquire(tab,
//通过运算得到i下标所在内存中的偏移地址
((long)i << ASHIFT) + ABASE);
}
// |-- ABASE:table数组在内存中的起始地址(对象的实例数据部分)
// scale:Node数组中,一个元素所占的大小(2的次幂)
// ASHIFT:如果scale=2^n,那么ASHIFT=n
ABASE = U.arrayBaseOffset(Node[].class);
int scale = U.arrayIndexScale(Node[].class);
if ((scale & (scale - 1)) != 0)
throw new ExceptionInInitializerError("array index scale not a power of two");
ASHIFT = 31 - Integer.numberOfLeadingZeros(scale);
为什么使用计算slot的 tabAt(Node<K,V>[] tab, int i)
方法,而不是直接使用tab[i]
?
initTable方法
//采用自旋和CAS的方式来完成初始化,有点类似于单例模式的双重检查实现
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
while ((tab = table) == null || tab.length == 0) {
//1. 如果sizeCtl<0(sizeCtl==-1),说明有其他线程正在进行初始化工作,Thread.yield()让出cpu使用权限。
if ((sc = sizeCtl) < 0)
Thread.yield(); // lost initialization race; just spin
//2. 这里sizeCtl作为同步标志,使用CAS将sizeCtl设置为-1表示获取到锁。
else if (U.compareAndSetInt(this, SIZECTL, sc, -1)) {
try {
//2.1. 判断是否在获取到锁之前,其他线程已经完成初始化。双重检查。
if ((tab = table) == null || tab.length == 0) {
//2.2. sc(=sizeCtl) 等于0:使用默认的初始容量16。
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
table = tab = nt;
//2.3. 类似 n*0.75 求出扩容阈值
sc = n - (n >>> 2);
}
} finally {
//2.4. 两种情况
// CASE1 在 2.1 之前其他线程已经完成初始化,此时的sizeCtl的值为扩容阈值。
// 在获取锁的时候已经将其改为-1,需要还原,同时也有释放锁。
// CASE2 进行初始化,将扩容阈值赋值给sizeCtl,同时释放锁
sizeCtl = sc;
}
break;
}
}
return tab;
}
spread方法
在使用spread(int h)
计算hash的 时候,为什么要添加&HASH_BITS的操作,而HashMap中却没有使用?
//ConcurrentHashMap
static final int spread(int h) {
return (h ^ (h >>> 16)) & HASH_BITS;
}
static final int HASH_BITS = 0x7fffffff;//0111 1111 1111 1111 1111 1111 1111 1111
//HashMap
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
原因:其因为有些为负数的hash值是由特殊含义的,比如fwd节点和treebin节点。这个操作是为了让正常node节点的hash值为正数。
resizeStamp方法
//n:old-length,每一个大小的map进行扩容时有唯一的一个标识戳
static final int resizeStamp(int n) {
return Integer.numberOfLeadingZeros(n) |
//为了将其变成负数
(1 << (RESIZE_STAMP_BITS - 1));
}
putVal方法
//添加k-v,如果发生替换返回oldVal,否则返回null
final V putVal(K key, V value, boolean onlyIfAbsent) {
//不能存储key或value为null的映射
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode());
int binCount = 0;
//<1> 添加k-v
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh; K fk; V fv;
//<1.1> 调用初始化方法initTable
if (tab == null || (n = tab.length) == 0)
tab = initTable();
// <1.2> 该slot==null,尝试进行CAS设置(没有发生阻塞).如果成功不再自旋,失败走其他逻辑.
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value)))
break; // no lock when adding to empty bin
}
// <1.3> 前提不为null:如果当前位置被扩容成功,会将该位置设置为ForwardingNode. 说明map正在被扩容调用helpTransfer方法去帮助扩容. 然后再添加元素.
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
//如果不允许修改,和头节点进行对比.如果相等直接将val返回
else if (onlyIfAbsent // check first node without acquiring lock
&& fh == hash
&& ((fk = f.key) == key || (fk != null && key.equals(fk)))
&& (fv = f.val) != null)
return fv;
//<1.4> 对链表头节点不为null且允许替换val,和红黑树的情况进行处理
else {
V oldVal = null;
//<1.4.1> 对头节点加锁
synchronized (f) {
//检查f是否是头节点. f在 <1.2>的时候被赋值为当时的头节点,可能在运行到此时其他线程已经完成扩容(头插法)等操作,导致f不再是头节点. 如果错误则进行自旋.
if (tabAt(tab, i) == f) {
// CASE1 链表结构
if (fh >= 0) {
binCount = 1;//代表链表第几个节点,从1开始
//进行自旋使用尾插法插入到链尾.pred: e的前一个节点 e:当前节点(从头节点算起)
//逻辑:pred=e e=e.next
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);
break;
}
}
}
// CASE2 红黑树结构
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;
}
}
else if (f instanceof ReservationNode)
throw new IllegalStateException("Recursive update");
}
}
//<1.5> 判断是否需要树化:如果插入或替换操作之前链表的长度>8,并且table.length>=64,则进行树化
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
//<2> 计算是否需要扩容
addCount(1L, binCount);
return null;
}
addCount方法
//计算map中的k-v映射数量,类似于LongAdder的计算方法.并决定是否进行扩容.
private final void addCount(long x, int check) {
CounterCell[] cs; long b, s;
//类似于LongAdder的add方法
if ((cs = counterCells) != null ||
!U.compareAndSetLong(this, BASECOUNT, b = baseCount, s = b + x)) {
CounterCell c; long v; int m;
boolean uncontended = true;
if (cs == null || (m = cs.length - 1) < 0 ||
(c = cs[ThreadLocalRandom.getProbe() & m]) == null ||
!(uncontended =
U.compareAndSetLong(c, CELLVALUE, v = c.value, v + x))) {
fullAddCount(x, uncontended);
return;
}
//前提counterCells!= null或对baseCount赋值的时候发生竞争
// CASE1 check=0,slot为null并且使用CAS设置成功. 即未进行阻塞
// CASE2 check=1,头节点.next=null并添加,或对头节点进行替换.
if (check <= 1)
return;
s = sumCount();
}
if (check >= 0) {
Node<K,V>[] tab, nt; int n, sc;
//<1> 扩容的逻辑s>=扩容阈值
while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
(n = tab.length) < MAXIMUM_CAPACITY) {
int rs = resizeStamp(n);
//<1.1> 正在进行扩容,因为已经进行过初始化的判断
if (sc < 0) {
// 1)对比标记戳sc的高16位是否相同(当前超过扩容阈值的容器是否是同一个容器)
// 2)sc的低16位是线程数+1,因此当sc == rs<<16 + 1 是已经完成扩容
// 3)进行扩容的线程达到最大值
// 4)扩容已经结束 nextTable=null
// 5)扩容的任务已经被分配完成(table.length=16例:有效范围1<=transferIndex<=16)
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
transferIndex <= 0)
break;
//前提条件: 1)-5)不成立,sc+1: 代表扩容的线程+1.
if (U.compareAndSetInt(this, SIZECTL, sc, sc + 1))
transfer(tab, nt);
}
//<1.2> 这是进行扩容的第一个线程,注意 rs<<16+2
else if (U.compareAndSetInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
transfer(tab, null);
//<1.3> 进行计算,直到不<=扩容阈值退出while循环
s = sumCount();
}
}
}
transfer方法
//从后向前进行扩容 15-0
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
int n = tab.length, stride;
//<1> 根据cpu计算每个线程能过扩容的slot个数
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
stride = MIN_TRANSFER_STRIDE; // subdivide range
//<2> 第一个进行扩容的线程,扩容后的大小为原来的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;
}
//<2.1> 初始化nextTable和transferIndex
nextTable = nextTab;
transferIndex = n;
}
int nextn = nextTab.length;
//<3> 创建fwd节点,用于在完成slot的移动后,将oldTable中的slot设置为指向该fwd的节点.告知其已经扩容完成,到nextTable中去进行get和put操作
ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
boolean advance = true;//true: 允许进行扩容任务分配 false: 不允许进行扩容分配
boolean finishing = false; // 是否已经完成扩容
//i: 正在进行元素转移的slot,bound和nextIndex: 分配的转移元素的slot范围[indexIndex-1,bound].
//i=-1: 已经没有任务分配 或 做最后的任务的线程已经完成元素转移
for (int i = 0, bound = 0;;) {
Node<K,V> f; int fh;
//<4> 如果advance为true, 继续执行未完成的任务或分配新的任务
while (advance) {
int nextIndex, nextBound;
// 1) 不能进行分配的条件
// 条件1: 还未完成已经分配的任务 条件2: 已经完成扩容
if (--i >= bound || finishing)
advance = false;
// 条件2: 已经将所有的slot分配完成无需再分配,将 i赋值-1
else if ((nextIndex = transferIndex) <= 0) {
i = -1;
advance = false;
}
// 2)进行分配
else if (U.compareAndSetInt
(this, TRANSFERINDEX, nextIndex,
nextBound = (nextIndex > stride ?
nextIndex - stride : 0))) {
bound = nextBound;
i = nextIndex - 1;
advance = false;
}
}
//<5> i=-1: 已经没有任务分配 或 做最后的任务的线程已经完成元素转移
if (i < 0 || i >= n || i + n >= nextn) {
int sc;
//<5.1> 已经完成扩容 在<5.2>的时候进行判断是否是最后一个完成元素转移的线程(sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT.
//与开始的时候呼应
if (finishing) {
nextTable = null;
table = nextTab;
//设置新的扩容阈值0.75*nextTable.length
sizeCtl = (n << 1) - (n >>> 1);
return;
}
//<5.2> 将线程数-1,addCount方法中<1.1> 的 2)会使用到. 如果已经完成扩容finishing=true,并且i=n进行检查
if (U.compareAndSetInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
return;
finishing = advance = true;
//与<7> 中的检查有关
i = n; // recheck before commit
}
}
//<6> 前置条件: 没有执行完任务. 使用CAS转移null节点,只需指向fwd即可.
// true: 继续转移下一个slot false: 当前slot有数据了(因为在扩容时,允许向null的slot中写入数据 putVal方法的<1.1>),会自旋到<8> 转移数据.
else if ((f = tabAt(tab, i)) == null)
advance = casTabAt(tab, i, null, fwd);
//<7> 前置条件: 没有执行完任务. (fh = f.hash) == MOVED: 最后一个线程会对所有的slot进行检查,看是否有遗漏的slot,并将其完成转移.
else if ((fh = f.hash) == MOVED)
advance = true; // already processed
else {
//<8> 锁住头节点,对非null slot进行元素转移.
synchronized (f) {
if (tabAt(tab, i) == f) {
Node<K,V> ln, hn;
// 1) 链表 使用头插法
// runBit: 对扩容后的table 1:高端 0: 低端
// lastRun: 最后的runBit相同的链首,因为new Node,所以节省了一定的空间
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;
}
// 2) 树化 使用双向链表 尾插法
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;
}
}
}
}
}
}
为什么在进行转移链化的元素的时候需要 new Node(). 而在转移树化的元素的时候没有new TreeNode(),采用了和HashMap类似的方法?
我的理解: 避免在转移的过成中(因为在转移成功后才设置头节点为fwd节点, 所以在转移的过程中get还需使用oldTable中的数据而不是fwd节点指向的nextTable),get操作因为next 引用的改变导致查找丢失. 而 tree,利用的双向链表采用与HashMap一样的方法 是因为在查询的时候用的是treenode的左右节点 而没有使用prev和next, 所以对get操作不会影响.
所以HashMap如果一个线程在扩容的时候, 另一个线程进行get操作也是不安全的.