1、简介
1.1、继承关系和特点
- ConcurrentHashMap是J.U.C(java.util.concurrent包)的重要成员,它是HashMap的一个线程安全的、支持高效并发的版本。
- 在默认理想状态下,ConcurrentHashMap可以支持16个线程执行并发写操作及任意数量线程的读操作。
1.2、底层结构
1.3、如何保证的线程的安全性
node + CAS + Synchronized 代替Segment
2、jdk7与jdk8的区别
- 取消了 segment 分段设计,直接使用 Node 数组来保存数据,并且采用 Node 数组元素作为锁来实现每一行数据进行加锁来进一步减少并发冲突的概率
- 将原本数组+单向链表的数据结构变更为了数组+单向链表+红黑树的结构。
3、基础属性
3.1、属性含义
//哈希桶——采用懒加载,第一次插入数据初始化,之后都是2的幂次方
transient volatile Node<K,V>[] table;
//扩容时使用,平时为null,只有在扩容的时候才为非null
private transient volatile Node<K,V>[] nextTable;
/**
* 该属性用来控制table数组的大小,根据是否初始化和是否正在扩容有几种情况:
* 当值为负数时:如果为-1表示正在初始化,如果为-N则表示当前正有N-1个线程进行扩容操作;
* 当值为正数时:表明下一次要调整的table的大小,也是负载,sizeCtl=sc=n*0.75=12
*/
private transient volatile int sizeCtl;
//计数器单元格表。当非空时,size 是 2 的幂
private transient volatile CounterCell[] counterCells;
//记录hashmap的元素个数
private transient volatile long baseCount;
//默认的并发度
private static final int DEFAULT_CONCURRENCY_LEVEL = 16;
//最大的容量
private static final int MAXIMUM_CAPACITY = 1 << 30;
//初始化哈希桶桶容量
private static final int DEFAULT_CAPACITY = 16;
//负载因子大小
private static final float LOAD_FACTOR = 0.75f;
并发度,说明什么,就是说Hash的哈希桶最多有多少个,那么可以最多有多少个并发度,因为每一个线程可以锁住每一个桶中的首节点。
3.2、节点Node
关键属性使用volatile修饰
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节点中的val,和next都使用volatile修饰,保证线程通信的数据。
3.3、树节点
static final class TreeNode<K,V> extends Node<K,V> {
TreeNode<K,V> parent; // red-black tree links
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev; // needed to unlink next upon deletion
boolean red;
//......
}
3.4、TreeBin
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//表示正在设置读说
}
用作树的头结点,只存储root和first节点,不存储节点的key、value值。
4、构造方法
4.1、无参构造
public ConcurrentHashMap() {
}
4.2、指定大小的构造方法
//最大的容量
private static final int MAXIMUM_CAPACITY = 1 << 30;
public ConcurrentHashMap(int initialCapacity) {
if (initialCapacity < 0)
throw new IllegalArgumentException();
int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
MAXIMUM_CAPACITY :
tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
this.sizeCtl = cap;
}
动态调整sizeCtl,调用调用tableSizeFor方法
保证sizeCtl必须是2的n次方
4.3、初始化指定map的构造方法
public ConcurrentHashMap(Map<? extends K, ? extends V> m) {
this.sizeCtl = DEFAULT_CAPACITY;
putAll(m);
}
4.4、指定初始化容量和加载因子
public ConcurrentHashMap(int initialCapacity, float loadFactor) {
this(initialCapacity, loadFactor, 1);
}
这个初始化方法默认的并发度为1
4.5、设置初始化容量,负载因子和并发度
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;
}
5、添加操作
5.1、putVal方法
代码
//转成红黑树的临界值
static final int TREEIFY_THRESHOLD = 8;
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPinterException();
//计算hash值
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();
//计算对应的桶数,如果桶中没有节点,f是对应的桶
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;
}
// 该结点的hash值为MOVED
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
V oldVal = null;
//对桶的首节点进行加锁
synchronized (f) {
//双重判定——如果对应桶的内容没有改变,还是之前的f
if (tabAt(tab, i) == f) {
//如果是桶中首节点的hash值>=0
if (fh >= 0) {
//那么binCount=1
binCount = 1;
//循环遍历桶中的节点,表明了遍历的节点个数
for (Node<K,V> e = f;; ++binCount) {
K ek;
//如果找到key相同的节点,将value替换掉,退出
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;
}
}
}
//如果首节点Hash值<0,判断是否为树
else if (f instanceof TreeBin) {
Node<K,V> p;
//设置bincount=2
binCount = 2;
//将节点添加到红黑树中
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
//如果binCount!=0,说明进行了链表或者红黑树操作
if (binCount != 0) {
//如果binCount>=8说明,链中的节点>8了,因为binCount是遍历的次数。
if (binCount >= TREEIFY_THRESHOLD)
//转换为红黑树
treeifyBin(tab, i);
//如果oldVal不等于null,也就是说存在key相同的值,直接返回
if (oldVal != null)
return oldVal;
break;
}
}
}
// 增加binCount的数量
addCount(1L, binCount);
return null;
}
binCount的说明:
- binCount=0,说明桶中的数据为空直接添加成功
- binCount >0,如果是普通节点,也就是遍历的链表节点的个数,也就是没有计算新添加的节点的个数
if (binCount >= TREEIFY_THRESHOLD) treeifyBin(tab, i);
,如果binCount>=8则转换为红黑树,binCount只记录扫描的链表的节点个数,所以说是>8的时候进行转换成红黑树。
- binCount >0,如果是红黑树,恒等于2
具体内容
- 如果对应桶中的数据不为空,会对桶的头节点加锁
- 最后进行计数器+1
5.2、计算哈希值spread()
static final int HASH_BITS = 0x7fffffff;
static final int spread(int h) {
return (h ^ (h >>> 16)) & HASH_BITS;
}
5.3、读取哈希桶数据-tabAt()方法
下面的读可以读取到最新的值
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);
}
5.4、CAS更新哈希桶数据-casTabAt
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);
}
5.5、compareAndSwap…方法参数详解
compareAndSwapLong(Object obj, long offset, long expect, long update)
5.6、初始化哈希桶
//哈希桶的初始化大小
private static final int DEFAULT_CAPACITY = 16;
rivate final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
//只要哈希桶为空,也就是没有初始化
while ((tab = table) == null || tab.length == 0) {
//判断sizeCtl是否<0,说明有人在初始化
if ((sc = sizeCtl) < 0)
//线程礼让
Thread.yield(); // lost initialization race; just spin
//使用CAS尝试将sizeCtl变成-1,也就是表示table哈希桶正在初始化中
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
//如果哈希桶没有初始化
if ((tab = table) == null || tab.length == 0) {
//并且sizeCtl是否大于0,如果不大于设置成初始化大小
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
@SuppressWarnings("unchecked")
//设置一个n个node节点,然后复制给table
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
table = tab = nt;
//将n无符号右移2位,也就是1/4,最后sc会变成n的0.75,也就是=12
sc = n - (n >>> 2);
}
} finally {
//直接将sc赋值过去
sizeCtl = sc;
}
break;
}
}
return tab;
}
- 如果sizeCtl小于0:说明别的数组正在进行初始化,则让出执行权
- 如果sizeCtl大于0的话:表明下一次要调整的table的大小,也是负载,sizeCtl=sc=n*0.75=12
- 否则的话初始化一个默认大小(16)的数组
- 然后设置sizeCtl的值为数组长度的3/4 * 该属性用来控制table数组的大小
5.7、helpTransfer方法
/**
* 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;
//旧数组不为空且nextTable也不空的情况下才能复制
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;
//cas操作保证线程安全
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
transfer(tab, nextTab);//调用扩容方法
break;
}
}
return nextTab;
}
return table;
}
参考博客
https://www.jianshu.com/p/8ca315547c1f
6、计数器+1实现
如果保证size更新的安全性
原子性:cas不可行
atomic:cas
加锁,性能下降
6.1、具体实现
//计数器单元格表。当非空时,size 是 2 的幂
private transient volatile CounterCell[] counterCells;
//记录hashmap的元素个数
private transient volatile long baseCount;
- 在无并发的情况下,使用单一的属性 baseCount 进行累计(CAS),一旦操作不成功,进入并发场景。
- 在并发情况下,使用多个分区,将增加或减少的个数累计到相应的分区,可很大程度避免多线程操作同一对象的并发问题,最后在获取时,将各分区的值进行相加,得到一个弱一致性的数量值。
- CounterCells 是一个数组,线程需要与其 length 进行 & 操作,以 hash 到具体的 CounterCell 中,所以为线程增加了属性 probe,该属性由位操作实现,得到一个随机的值。同时该值可以被更新。
CounterCell counterCell = cs[ThreadLocalRandom.getProbe() & m])
6.2、addCount方法
//计数器单元格表。当非空时,size 是 2 的幂
private transient volatile CounterCell[] counterCells;
//记录hashmap的元素个数
private transient volatile long baseCount;
private final void addCount(long x, int check) {
CounterCell[] as; long b, s;
//==========================================① 第一部分做元素增加尝试==================================
//当计算表格counterCells为空时,先尝试将其baseCount+1,如果成功,就不需要计算器表格来辅助了
if ((as = counterCells) != null ||
!U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
CounterCell a; long v; int m;
// uncontended = true 即表示由无并发进入,否则为 CAS 并发失败进入
boolean uncontended = true;
//如果counterCells不为null,或者counterCells为null但是baseCount的CAS更新操作失败,进行到下面
//如果as为空,
if (as == null || (m = as.length - 1) < 0 ||
//as不为空
//尝试获取一个随机数索引,然后对应到计数器单元格中的位置
(a = as[ThreadLocalRandom.getProbe() & m]) == null ||
//对应的内容做CAS+1操作
!(uncontended =
U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
//如果计算器表格更新没有成功,那么就进行fullAddCount方法
fullAddCount(x, uncontended);
return;
}
if (check <= 1)
return;
//记录节点总和
s = sumCount();
}
//=======================================② 如果计算表格为空,对哈希桶进行扩容===================================
//如果check>=0,然后对哈希桶进行扩容
if (check >= 0) {
Node<K,V>[] tab, nt; int n, sc;
// 当前存储大于 75%,且总大小小于最大容量
while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
(n = tab.length) < MAXIMUM_CAPACITY) {
//计算rs
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
break;
// 此时其他线程如果扩容完毕,修改 sc 的值,继续扩容
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
transfer(tab, nt);
}
// 如果 sc > 0,说明是刚开始,因为 sc < 0 时,表示有多少条线程在进行转移是:sc-1
// 所以这里要 rs + 2
else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
transfer(tab, null);
s = sumCount();
}
}
}
check 是影响扩容检查的主要元素。而前面说过,check 是外部修改影响到的个数 —— 可通过查看方法 putValue 等,check 实际传参为 binCount。
- 对普通节点,即 Node 的链表节点,操作如果需要遍历,则check = 遍历个数(不包含新添加的节点,所以是节点数>8的时候才可能扩容);
- 对树结构,check 总是等于 2。
6.3、fullAddCount方法,进行+1
private final void fullAddCount(long x, boolean wasUncontended) {
int h;
if ((h = ThreadLocalRandom.getProbe()) == 0) {
ThreadLocalRandom.localInit(); // force initialization
h = ThreadLocalRandom.getProbe();
wasUncontended = true;
}
// collide : 碰撞,是否冲突(这属于进入方法 rehash 后的重新一次冲突检测)
boolean collide = false;
for (;;) {
CounterCell[] cs; CounterCell c; int n; long v;
// 非空,且 size 大于 0,通过线程的 hash & size ,定位所在的 CounterCell
if ((cs = counterCells) != null && (n = cs.length) > 0) {
// 第一步:解决问题 c = cs[ThreadLocalRandom.getProbe() & m]) == null,即初始化或扩容产生的新 CounterCell
if ((c = cs[(n - 1) & h]) == null) {
if (cellsBusy == 0) {
// 初始化值,赋值完成后就 break 了
CounterCell r = new CounterCell(x); // Optimistic create
// CAS 替换,相当于获取锁,只有一条线程能够进入
if (cellsBusy == 0 &&
U.compareAndSetInt(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;
}
else if (!wasUncontended)
// 此处只有 CAS 失败的才会进入,将为 CAS 尝试做一次 rehash 操作,然后重新尝试
wasUncontended = true; // Continue after rehash
else if (U.compareAndSetLong(c, CELLVALUE, v = c.value, v + x)) // 如果已存在值,则进行 CAS 替换,不成功继续死循环
break;
else if (counterCells != cs || n >= NCPU)
// 边界判断,如果没有正在扩容,则判断是否超过大小边界,是则进行 rehash,否则扩容
// 正处于无效状态或 CounterCell 大小已超过 CPU 个数,此时对 Probe 进行重置(翻倍)
collide = false; // At max size or stale
else if (!collide)
collide = true;
else if (cellsBusy == 0 &&
U.compareAndSetInt(this, CELLSBUSY, 0, 1)) {
try {
// 翻倍
if (counterCells == cs) // Expand table unless stale
counterCells = Arrays.copyOf(cs, n << 1);
} finally {
cellsBusy = 0;
}
collide = false;
continue; // Retry with expanded table
}
h = ThreadLocalRandom.advanceProbe(h);
}
// cellsBusy 为 0 且 counterCells == cs 相等(cs 在前面被赋值:cs=counterCells),CAS 替换 cellsBusy 成功,进入初始化
else if (cellsBusy == 0 && counterCells == as &&
U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
boolean init = false;
try {
//如果counterCells还等于之前的
if (counterCells == as) {
//把他初始化为2个数组
CounterCell[] rs = new CounterCell[2];
//计算对应的位置的初始化为1
rs[h & 1] = new CounterCell(x);
//设置属性counterCells
counterCells = rs;
//初始化完成
init = true;
}
} finally {
//释放cellsBusy
cellsBusy = 0;
}
if (init)
break;
}
else if (U.compareAndSetLong(this, BASECOUNT, v = baseCount, v + x))
// 如果上述两个步骤无法进入,认为出现了错误,使用 CAS(BaseCount) 作为备用/保障选项
break; // Fall back on using base
}
}
6.4、获取总数操作
public int size() {
//计数方法
long n = sumCount();
return ((n < 0L) ? 0 :
(n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE :
(int)n);
}
private transient volatile CounterCell[] counterCells;
final long sumCount() {
//获取计数数组counterCells
CounterCell[] as = counterCells; CounterCell a;
//获取基数,然后累加获取元素大小
long sum = baseCount;
if (as != null) {
for (int i = 0; i < as.length; ++i) {
if ((a = as[i]) != null)
sum += a.value;
}
}
return sum;
}
参考博客
计数器+1方法:https://segmentfault.com/a/1190000039667702
7、get方法
计算hash值,如果定位到table本身,直接返回;如果不是,根据当前节点类型,分别按照链表和红黑树的方式去查找当前元素所在的位置
public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
// 双重 hash
int h = spread(key.hashCode());
// 如果 table 不为空 并且查找的 key 的节点不为 null
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) {
// 如果节点 e 的hash 是要查找的key的hash
if ((eh = e.hash) == h) {
// 如果节点 e 的 key 与要查找的key相等
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
// 返回 value
return e.val;
}
// 判断是否是红黑树
else if (eh < 0)
// 遍历红黑树查找元素
return (p = e.find(h, key)) != null ? p.val : null;
// 遍历链表查找key值所在的节点e
while ((e = e.next) != null) {
if (e.hash == h &&
((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
return null;
}
8、jdk1.8如何保证的线程安全
8.1、初始化线程安全保障
table使用volatile保证每次获取到的都是最新写入的值
//哈希桶
transient volatile Node<K,V>[] table;
8.2、put方法的线程安全保障
-
volatile变量(sizeCtl):它是一个标记位,用来告诉其他线程这个坑位有没有人在,其线程间的可见性由volatile保证。
-
CAS操作:CAS操作保证了设置sizeCtl标记位的原子性,保证了只有一个线程能设置成功
- 在tabAt(tab, i)方法,其使用Unsafe类volatile的操作volatile式地查看值,保证每次获取到的值都是最新的
- Unsafe类volatile式地拿到最新的Node
-
如果需要添加到桶中已有的节点,对头节点进行加锁
8.3、计数的安全性
- 使用CAS操作
- 使用数组+基础计数来实现计数的高并发性
8.4、扩容的线程安全性
- 减小锁粒度:将Node链表的头节点作为锁,若在默认大小16情况下,将有16把锁,大大减小了锁竞争(上下文切换),就像开头所说,将串行的部分最大化缩小,在理想情况下线程的put操作都为并行操作。同时直接锁住头节点,保证了线程安全
- Unsafe的getObjectVolatile方法:此方法确保获取到的值为最新
- ConcurrentHashMap运用各类CAS操作,将扩容操作的并发性能实现最大化,在扩容过程中,
- 就算有线程调用get查询方法,也可以安全的查询数据,若有线程进行put操作,还会协助扩容,
- 利用sizeCtl标记位和各种volatile变量进行CAS操作达到多线程之间的通信、协助,在迁移过程中只锁一个Node节点,即保证了线程安全,又提高了并发性能。
9、面试题
9.1、ConCurrentHashMap的key和vlaue是否可以为Null
不行 如果key或者value为null会抛出空指针异常
9.2、ConCurrentHashMap每次扩容为原来容量的几倍
2倍 在transfer方法里面会创建一个原数组的俩倍的node数组来存放原数据。
9.3、为什么 ConcurrentHashMap 比 HashTable 效率要高?
HashTable使用一把锁,锁了整个结构,多个线程使用一把锁,会阻塞,影响效率;
而jdk1.7ConcurrentHashMap使用分段锁,锁的粒度降低;
jdk1.8的ConcurrentHashMap使用volatile+CAS+synchronized,将锁的粒度再次降低,提高并发量。
8.4、ConcurrentHashMap 锁机制具体分析(JDK 1.7 VS JDK 1.8)?
1.7使用分段锁的机制,底层使用数组加链表的结构,使用Segment、HashEntry数据结构,Segment继承ReentrantLock可重入锁,使用它来保护HashEntry操作的数据原子性
1.8使用Node、CAS、synchronized关键字来保证并发安全,取消了Segment这一层;同时使用了红黑树机制,红黑树可以和链表相互转化,以提升查询性能;
8.5、ConcurrentHashMap 的并发度是什么?
1.7 默认的并发度为16,可以在构造函数进行设置,但是进行设置时,ConcurrentHashMap会使用一个 >=要改数字的2的最小次方数作为实际并发数,比如设置为17,实际并发度为 32;
1.8 并发度没有实际意义,当我设置初始容量小于并发度时,将容量提升至并发度大小,但是默认还是16。也就是说并发度就是桶的个数,因为1.8锁住的是node。
8.6、ConcurrentHashMap迭代器是强一致性还是弱一致性?HashMap呢?
弱一致性,HashMap强一致性。
ConcurrentHashMap可以支持在迭代过程中,向map添加新元素,而HashMap则抛出了ConcurrentModificationException,因为HashMap包含一个修改计数器,当你调用他的next()方法来获取下一个元素时,迭代器将会用到这个计数器。
- HashMap的forEach方法
public final void forEach(Consumer<? super K> action) {
Node<K,V>[] tab;
if (action == null)
throw new NullPointerException();
if (size > 0 && (tab = table) != null) {
int mc = modCount;
for (int i = 0; i < tab.length; ++i) {
for (Node<K,V> e = tab[i]; e != null; e = e.next)
action.accept(e.key);
}
//当有人修改过不等于之前的修改次数,会抛出异常
if (modCount != mc)
throw new ConcurrentModificationException();
}
}
8.7、ConcurrentHashMap1.7和1.8的区别
jdk1.8的实现降低锁的粒度,jdk1.7锁的粒度是基于Segment的,包含多个HashEntry,而jdk1.8锁的粒度就是Node
数据结构:jdk1.7 Segment+HashEntry;jdk1.8 数组+链表+红黑树+CAS+synchronized