总体结构(Node数组+链表+红黑树)
构造方法
- 初始化容量不能小于并发等级
- 1.7中的Segment为创建即加载,并初始化0下标的HashEntry,在1.8中改进为懒加载,初始化时仅进行Node数组长度的计算
- 长度的计算方式是 (1.0 + (long)initialCapacity / loadFactor) 大于等于该数的2次方数
public ConcurrentHashMap(int initialCapacity,
float loadFactor, int concurrencyLevel) {
if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0)
throw new IllegalArgumentException();
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);
this.sizeCtl = cap;
}
Get方法
- 在1.8的构造中采用Node代替Segment,在获取数据的时候不需要通过 UNSAFE.getObjectVolatile保证可见性
- 正常的所有的hash值都会被设定为正数
- 对于正在扩容的节点会赋值为-1,树节点会为-2,因此在判断出该节点hash值为负数时,则说明不是正常的链表结构,需要使用find进行查询
- 如果是链表结构则直接进行遍历获取
public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
// spread 方法能确保返回结果是正数
int h = spread(key.hashCode());
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) {
// 如果头结点已经是要查找的 key
if ((eh = e.hash) == h) {
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;
}
// hash 为负数表示该 bin 在扩容中或是 treebin, 这时调用 find 方法来查找
else if (eh < 0)
return (p = e.find(h, key)) != null ? p.val : null;
// 正常遍历链表, 用 equals 比较
while ((e = e.next) != null) {
if (e.hash == h &&
((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
return null;
}
Put
- 在元素没有真正put成功前,会一直死循环,首先会计算出key对应的hash下标
- 首先会通过自旋+CAS的方式创建数组桶,只有第一个通过CAS操作将sizectl改为-1的才说明获取到锁,-1也就代表正在进行初始化或扩容操作,-N则说明有N个线程正在进行操作,其他没有获取到锁的线程则会调用yield()方法让出时间片,让其他线程能够以更高的效率进行操作,在桶创建成功后再通过CAS将元素加入到桶中
- 如果桶中的元素为空,则直接CAS放入即可,不需要进行加锁操作,退出put方法
- 如果此时判断出节点的Hash值为-1,说明此时旧数组桶正在进行扩容,并且旧数组桶的该下标已经扩容完毕,此时该线程会帮忙进行扩容流程
- 当数组桶创建好,并且没有在扩容状态中,并且此时出现Hash冲突,此时会进行Synchronized加锁操作,此时只会对该Node节点进行加锁操作
- 如果该桶对应的是链表,则直接遍历,如果有相同的节点则进行覆盖操作,没有则会使用尾插法插入到该链表的末尾
- 否则说明是红黑树,则会执行对应的红黑树添加节点流程
- 添加节点完毕后,如果是链表节点会记录出链表的长度,如果链表长度达到8,则会将链表转为红黑树,红黑树的转换操作会进行Synchronized加锁操作,锁住头节点
- 当所有流程结束后,会对map中记录了所有节点的值进行添加操作,类似于原子类
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) {
// key 和 value 不能为空
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode());
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
// f = 目标位置元素
Node<K,V> f; int n, i, fh;// fh 后面存放目标位置的元素 hash 值
if (tab == null || (n = tab.length) == 0)
// 数组桶为空,初始化数组桶(自旋+CAS)
tab = initTable();
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
// 桶内为空,CAS 放入,不加锁,成功了就直接 break 跳出
if (casTabAt(tab, i, null,new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
V oldVal = null;
// 使用 synchronized 加锁加入节点
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;
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) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
while ((tab = table) == null || tab.length == 0) {
if ((sc = sizeCtl) < 0)
Thread.yield();
// 尝试将 sizeCtl 设置为 -1(表示初始化 table)
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
// 获得锁, 创建 table, 这时其它线程会在 while() 循环中 yield 直至 table 创建
try {
if ((tab = table) == null || tab.length == 0) {
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
table = tab = nt;
sc = n - (n >>> 2);
}
} finally {
sizeCtl = sc;
}
break;
}
}
return tab;
}
size计数
- size的计算会在put和remove的过程中进行统计
- 如果没有竞争,则会向basecount进行计数
- 如果有竞争发生,则会记录在countersCells
扩容过程
- 通过CPU核数的计算,计算出每个线程要迁移多少个桶,每个线程要迁移的桶是平均的
- 计算出扩容后的数组大小,即原数组的两倍
- 每个线程最少处理16个桶的扩容,通过CAS操作为每个节点分配任务,即初始值和长度
- 处理过的节点会将节点标识设置为forward即标志位-1
private final void addCount(long x, int check) {
CounterCell[] cs; long b, s;
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;
}
if (check <= 1)
return;
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) << RESIZE_STAMP_SHIFT;
if (sc < 0) {
if (sc == rs + MAX_RESIZERS || sc == rs + 1 ||
(nt = nextTable) == null || transferIndex <= 0)
break;
if (U.compareAndSetInt(this, SIZECTL, sc, sc + 1))
transfer(tab, nt);
}
else if (U.compareAndSetInt(this, SIZECTL, sc, rs + 2))
transfer(tab, null);
s = sumCount();
}
}
}
/**
* 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;
if (tab != null && (f instanceof ForwardingNode) &&
(nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) {
int rs = resizeStamp(tab.length) << RESIZE_STAMP_SHIFT;
while (nextTab == nextTable && table == tab &&
(sc = sizeCtl) < 0) {
if (sc == rs + MAX_RESIZERS || sc == rs + 1 ||
transferIndex <= 0)
break;
if (U.compareAndSetInt(this, SIZECTL, sc, sc + 1)) {
transfer(tab, nextTab);
break;
}
}
return nextTab;
}
return table;
}
为什么使用Synchronized代替ReentrantLock
锁的粒度
- 在JDK7中使用的是
**Segment+Entry**
的组合,Segment是继承自**ReentrantLock**
的,在进行Put操作时,需要先**获取到Segment锁**
才能进行操作,即锁住整个Segment - 并且在JDK7中的Segment在
**初始化后就固定**
了,在数据量大的情况下,Segment内会包含许多的Entry节点,在这些Entry节点需要进行put操作时,都需要获取Segment锁,此时需要频繁的等待锁 - 在JDK8中优化为Node,在进行Put操作时需要获取到
**链表头节点Node**
的Synchronized锁,即**锁住Node节点所在的链表**
- JDK8中
**缩小了锁的粒度**
,也就减少了锁竞争的情况,因此JDK8的并发效率会强于JDK7
锁的效率
- 因为JDK8中的锁粒度在于链表头结点,要远小于JDK7中的Segment,因此其实在JDK8中出现锁竞争的情况已经较少。
- Synchronized经过有
**偏向锁、自旋锁等优化**
后,性能已经比以前好很多,此时JDK8中的情况属于锁竞争较小的情况,有比较大的可能可以通过自旋的方式,**在重试数十次后**
获取到锁,而不需要升级为重量级锁。 - ReentrantLock在第一次没有获取到锁后,在新增节点后只会再尝试获取一次锁,如果
**两次获取锁失败就会被 LockSupport.park()挂起**
,挂起操作与Synchronized获取重量级锁一样,需要切换到内核态,是十分耗费时间的。 - 因此基于JDK8中基于Node的情况并发量小的情况,采用Synchronized更合适
为什么ConcurrentHashMap不允许有null
- 在
**ConcurrentHashMap**
的put方法中,会判断key和value是否为null,如果为**null会抛出空指针异常**
final V putVal(K key, V value, boolean onlyIfAbsent) {
// key 和 value 不能为空
if (key == null || value == null) throw new NullPointerException();
- 在HashMap中是
**允许存储**
有null值的,允许有**一个key**
为null,允许有**多个value**
为null - ConcurrentHashMap不允许存储null值的原因是,
**不允许存在二义性**
,在get方法返回null后,无法判断是该value为null,还是该key不存在 - HashMap是线程不安全的,默认在
**单线程环境**
下执行,HashMap在get方法返回null后,可以通过**containsKey**
的返回值判断该null值是存在还是不存在。HashMap默认key为null的hash值为0 - 但是ConcurrentHashMap是线程安全的集合,在get方法返回null后,再通过containsKey判断是否存在时,可能在中间时间会有其他线程对该key进行修改,返回的结果不一定是当时真实的结果,因此会产生二义性。
- 为了避免这种二义性存在,因此ConcurrentHashMap不允许存储null值