Bloddy 2020-02-28
并发编程中使用HashMap可能导致程序死循环。因为多线程会put方法添加键值对时将导致HashMap的Entry链表形成环形数据结构,一旦形成环形数据结构,Entry的next节点永远不为空,就会产生死循环获取Entry。
另外Hashtable只是简单地使用阻塞式锁(synchronized关键字)来保证线程安全,在并发编程中使用HashTable效率较低。
基于此,ConcurrentHashMap解决了上面的两方面的不足,它能够高效安全地对键值对进行读写操作。
ConcurrentHashMap的锁分段技术可有效提升并发访问率。Hashtable效率低的原因是多个线程竞争同一把锁。如果将容器中的数据划分为不同的部分或段,并为这些不同的段分别分配一把锁,当将线程要访问某数据,只需要竞争此数据所属分段的锁,而其他段的数据还是能被其他线程访问。这样就将锁的粒度细化了,实现了更高效的并发处理。
ConcurrentHashMap在JDK1.7和JDK1.8中的内部实现有较大的差异。JDK1.7中,ConcurrentHashMap直接使用Segment类型的数组segments作为分段锁数组(segments是成员变量),同时又是每个分段数据的的容器,每个segments又包含一个Entry数组。可将segments的每个元素看作一个HashMap对象,基于此可进一步想象,ConcurrentHashMap包含多个HashMap,每个HashMap是一个分段数据集,每个HashMap上有一把锁。
而在JDK1.8中,ConcurrentHashMap还是像HashMap一样,使用Node类型数组table作用为哈希表(table是其成员变量),将Node对象作为储存包含键值对节点的容器,不像JDK1.7中可以直接看到RentantLock锁。虽然也定义了Segment静态内部类,但JDK1.8中只有在(反)序列化方法中使用了这个类,只是为了(反)序列化时与JDK1.7相兼容而已。与JDK1.7的版本相比,这里Segment的重要性已经降低太多了,它与ConcurrentHashMap之间的类关系只是依赖而已。
| JDK1.7 | |
|---|---|
| JDK1.8 | ![]() |
成员变量
transient volatile Node<K,V>[] table;
private transient volatile Node<K,V>[] nextTable;
private transient volatile long baseCount;
private transient volatile int sizeCtl;
private transient volatile int transferIndex;
private transient volatile int cellsBusy;
private transient volatile CounterCell[] counterCells;
// views
private transient KeySetView<K,V> keySet;
private transient ValuesView<K,V> values;
private transient EntrySetView<K,V> entrySet;table :哈希表,其长度始终是2的幂次方。
nextTable: 下个要用到的哈希表,在重新调用哈希表table的容量是会用到且只有此时不为null。这主要在保证resize时并发访问,ConcurrentHashMap还是可用的。
baseCount: 键值对个数的计数器,主要在没有线程竞争的时候使用。
sizeCtl: table初始化和大小调整控制的依据。 如果为负,则table将被初始化或resize:-1用于初始化,若为其它负数时,|sizeCtl|=(1 +正在resize的线程数)。若sizeCtl是非负数且table为null时,它表示数组table初始化时的长度。初始化之后,表示table 调整大小时下个元素计数值。
transferIndex: resize时要拆分的nextTable表索引。
cellsBusy: 基于CAS的自旋锁,它会在resize和创建CounterCells时被使用。
counterCells:一个分布式计数的帮助类
而keySet 、values、entrySet就是各种视图。
ConcurrentHashMap有多个构造方法,我们直接研究其参数最多的构造方法。
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;
}参数concurrencyLevel代表预估的并发线程数,它会影响table的长度。构造方法首先通过“if (initialCapacity < concurrencyLevel)”确定用户设定的初始容量和并发级别是否匹配。如果inititalCapacity小于concurrencyLevel,就让inititalCapacity重设为concurrencyLevel。如果环境中真的有concurrencyLevel个线程,那么就应该有concurrencyLevel个锁,因为每个锁管理一段数据,那么就至少要有concurrencyLevel个段数据,每段数据至少包含一个哈希桶,也就是说至少table的长度至少要是concurrencyLevel。
根据“thread=loadFactor*capacity”公式可以看出,“(1.0 + (long)initialCapacity / loadFactor)”算出理论上table的最小长度size,这里加"+1"的原因是考虑不能整除时,要保留小数位的值,只能在整数位进一。而这个长度可size可能并是我们真正会使用的table的长度,因为我们要使用按位与的哈希算法来确定table的下标,就必须使table的长度为2的幂次方,而此时的size可能并不是2的幂次方,我们需要进一步处理。tableSizeFor()方法很巧妙使用了位运算,此方法能求出一个大于等于size的最小且是2的幂次方的值cap。如initialCapacity为14,loadFactor为0.75,concurrencyLevel为4,那么size=20,cap=32.所以最终初始化table时,其长度是32.
private static final int tableSizeFor(int c) {
int n = c - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}put(K,V)和putIfAbsent(K,V)都是直接调用putVal(K,V,boolean)方法。putVal方法包含了添加键值对的流程框架。
public V put(K key, V value) {
return putVal(key, value, false);
}
public V putIfAbsent(K key, V value) {
return putVal(key, value, true);
}putVal()方法的主要逻辑:①先确认table是空,若为空则将其初始化。②再根据位运算“(n-1)& hash”取余求出table的索引i,并进一步获取table下标为i的元素f,此f代表一个哈希桶,它一个单向链表或红黑树。③然后再确认f是否为空,若f为空,就使用CAS更新将f初始化。若CAS更新成功,则退出死循环,否则将再次进入“for (Node<K,V>[] tab = table;??”循环重试(自旋)。此时并没有使用阻塞锁,而是使用基于CAS的自旋锁。④若f的hash是-1,则表明table正在resize,要调用helpTransfer加速扩容。helpTransfer方法结束后,自旋重试添加键值对 ⑤若f不为空且其hash也不是-1时,就使用阻塞锁(synchronized关键字)将对象f锁住,再遍历链表f,查找此链表上是否存在Key对应的节点e。⑥若链表f上存在这样的节点e,就将e的val属性更新为当前需要添加键值对的value 。⑦若链表上不存在这样的节点e,就新构建一个节点,并将这个节点添加链表的尾部⑧若f是红黑树结构,就使用其特有的putTreeVal()方法进行键值对的添加。⑧若f的数据结构是ReservationNode等其他类型节点就同样需要自旋重试,等待状态正常时再来添加键值。⑨若链表长度超过了树形化的阀值,将链表转化为红黑树。链表上若存在这样的e节点,将e节点的原value直接返回;若不存在就调用addCount()来更新元素个数,再返回null 。
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode()); //再散列,求key对应的hash
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(); // table是空就初始化table
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
//键值对要添加到的哈希桶为空时,只要CAS成功初始化此哈希桶即可退出循环。此时不需要加锁
//这个添加键值对的node是这个哈希桶的头节点
break; // no lock when adding to empty bin
}
else if ((fh = f.hash) == MOVED)//MOVED是常量-1
//哈希桶不为空且hash为-1,表明table正在resize,使用helpTransfer加速扩容
tab = helpTransfer(tab, f);
else {
//要添加键值对的Key可能在链表或红黑树上f了,需要遍历它
V oldVal = null;
synchronized (f) { //锁定此链表或红黑树f
//"if (tabAt(tab, i) == f)"主要是保证f是我们要遍历的目标哈希桶。
// 在执行“((fh = f.hash) == MOVED)”时,可能会有其它线程修改了table
if (tabAt(tab, i) == f) {
if (fh >= 0) { //f是正常的哈希节点
binCount = 1;//链表长度
for (Node<K,V> e = f;; ++binCount) {//开始遍历链表(红黑树)f
K ek;
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {//哈希桶f上已存在这个Key对应的节点
oldVal = e.val;//保存原Value,最后需要返回它
if (!onlyIfAbsent)
e.val = value; //将原节点的val属性更新成新添加的value
break; //退出遍历链表的循环
}
Node<K,V> pred = e;
if ((e = e.next) == null) {
//遍历完链表发现,哈希桶f上不存在这个Key对应的节点。
//需要构建一个Node节点,将此节点添加在链表的尾部
pred.next = new Node<K,V>(hash, key,
value, null);
break;//退出遍历链表的循环
}
}
}
else if (f instanceof TreeBin) {
// 哈希桶f是红黑树型结构,使用其特有的putTreeVal方法添加键值对。
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) {
// 只要保证fh>0或f是红黑树,bineCount的最小值就是1。
// 那么也就一定会进入当前if分支
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i); //链表长度超过了树形化的阀值,将链表转化为红黑树
if (oldVal != null)
//原value不为空,直接返回这个值。
// 这时没有添加新的节点,不需要调用addCount方法去更新元素个数
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}(1)putVal()对键值都做了非空判断,这与HashMap有所不同,ConcurrentHashMap的Key和Value都不能为null。
(2)计算hash
而计算hash值的方法spread(int)的算法也与HashMap不同。"(h ^ (h >>> 16))"是HashMap计算hash的方式。此处在此基础之上,将“(h ^ (h >>> 16))”的结果和0x7fffffff进行按位与运算 (HASH_BITS是常量0x7fffffff),0x7fffffff的进二进制形式是“01111111_11111111_11111111_11111111”。根据位运算特点可知,经y=x&0x7fffffff计算后,y与x的四字节二进制形式可能只有最高位不同(y与x可能是相等的),y的最高位一定是0。这里和HASH_BITS按位与运算的目的是将其最高位强制设为0。因为计算机中数字的最高位是符号位,所以spread()方法返回值始终是非负数。
HASH_BITS的注释是“ usable bits of normal node hash”,那么所有正常节点的hash属性都非负数,因为才有putVal()方法体中的“if (fh >= 0) ”,只有哈希桶的头节点是正常节点才会遍历所哈希桶的所有节点。
static final int spread(int h) {
return (h ^ (h >>> 16)) & HASH_BITS;
}(3)初始化table
其主要逻辑是:先根据构造方法中计算出的sizeCtl来决定数组table长度,当状态合适时就将table初始化,再将sizeCtl设为原值的四分之三。
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
while ((tab = table) == null || tab.length == 0) {
if ((sc = sizeCtl) < 0)
//只有sizeCtl是非负数时,才表示table需要初始化。
// sizeCtl是负数表明其他线程可能正在初始化,当前线程放弃对table初始化的竞争,进入自旋状态。
Thread.yield(); // lost initialization race; just spin
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
// CAS尝试将sizeCtl为-1,主要目的让其他线程得到通知。
//其他线程检测有sizeCtl是-1,这些线程就能知道某线程正在初始化table,这些线程就会放弃初始化table
try {
if ((tab = table) == null || tab.length == 0) {
//这里sc不可能小于0,只可能等于0,若sc=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); //sc=0.75*n
}
} finally {
sizeCtl = sc; //无条件将sizeCtl设为原值的3/4
}
break; //CAS更新成功后,一定会将table初始化,可以退出自旋了。
}
}
return tab;
}(4) 确定哈希桶
确定哈希桶的主要是找到哈希桶在哈希表table中的索引。
“tabAt(tab, i = (n - 1) & hash))”可看出也是像HashMap利用位运算“(n - 1) & hash”取余来确定索引。这种利用位运算取余算法的基本前提是n必须是2的幂次方,所以table的长度也一直是2的幂次方。
而tabAt(Node[],int)方法也是简单地调用Unsafe类的getObjectVolatile方法。
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)哈希桶为空,利用CAS初始化哈希桶(的头节点)
casTabAt(Node[],int)简单调用Unsafe类的CAS方法更新table中i索引的元素。
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);
}(6)哈希桶的头节点的hash是-1,表明这个是forward节点(正常节点的hash是非负数),table正在resize,使用helpTransfer加速扩容。
ForwardingNode是Node的子类,其构造方法中调用了父类的构造方法Node(int, K, V , Node<K,V> )。可以看出ForawrdingNode所有实例的hash都是常量-1(MOVED是常量-1),其next属性为null,表明其没有后继节点。ForwardingNode类的设计目的本就是在转移扩容期间,将一个实例添加在哈希桶的头部,且其无后继节点,它是检测是否在转移扩容期间的flag标记。
final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {
Node<K,V>[] nextTab; int sc;
//f是转移节点,且其节点的下个table不为空。
if (tab != null && (f instanceof ForwardingNode) &&
(nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) {
//扩容时的标记位
int rs = resizeStamp(tab.length);
while (nextTab == nextTable && table == tab &&
//局部变量与成员变量相等,即nextTablet和table没有被其它线程修改
(sc = sizeCtl) < 0) {//sizeCtl小于0,表明正在扩容(-1时正在初始化table,显然此时不可能初始化table)
/**
* 若sc右移16位(取其高16位)与rs不等(表明标记位rs变化了)
* 或cs等于rs+1(扩容可以结束了,没有线程在扩容了。默认第一个线程设置sc=rs<<16+2,第一个线程结束扩容sizeCtl自减1,此时sc和rs就相差1)
* 或sc = rs + MAX_RESIZERS(已经达到了最大扩容线程数)
* 或transferIndex<=0(下标正在调整,扩容结束)
* 时,退出循环
*/
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 || //RESIZE_STAMP_SHIFT是常量16
sc == rs + MAX_RESIZERS || transferIndex <= 0) //MAX_RESIZERS是常量65535,表示最大扩容线程数
break;
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) { //CAS将sizeCtl加1
transfer(tab, nextTab);//CAS更新成功,table进行转移扩容(将tab的元素转移到nextTab)
break;
}
}
return nextTab;
}
return table;
}(7)若哈希桶上没有key对应的节点,在添加节点后还需要调用addCount重新计数元素个数
根据键获取值的方法主要有get(Object)、getOrDefault(Object,V)这两个,而getOrDefault方法又是委托get(Object)实现的,因此我们重点关注get(Object)方法。
public V getOrDefault(Object key, V defaultValue) {
V v;
return (v = get(key)) == null ? defaultValue : v;
}这里的get(Obejct)方法和HashMap的get方法类似。先确定Key对应的节点可能存在的某个桶上,然后再遍历这个桶上的所有节点,如果遍历过程中找到这样的节点,就返回节点的val属性,若遍历完后没找到这样的节点,就返回null 。值得注意的是当哈希桶的头节点是非正常节点时,不能直接按照常规的方式获取节点的val属性,此时需要调用其特定类类型的find()方法。
public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
int h = spread(key.hashCode()); //算出hash
//key对应的节点应可能在数组table的"(n - 1) & h)"索引元素上时。
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) {
if ((eh = e.hash) == h) {
//因为h>=0,所以eh>=0,此时e是正常节点,且是e是链表的头节点(红黑树TreeBin的hash常量-2)
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val; //在哈希桶的头节点上时,可直接返回此节点的val.
}
else if (eh < 0)//查找的节点不是哈希桶的头节点,且头节点是非正常节点,使用其特定的find(int,Object)查找
//正常节点的hash是非负数,可能是MOVED(转移)节点、TREEBIN(红黑树)根节点或RESERVED(暂存)节点。
//只有以上三种节点是负数。
return (p = e.find(h, key)) != null ? p.val : null;
while ((e = e.next) != null) {//不在链表的头节点上且头节点是正常节点,需要遍历链表。
if (e.hash == h &&
((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;//在链表上找到此节点,返回此节点的val
}
}
return null;//没找到返回null
}remove(Object,Object)和remove(Object,Object)方法均调用replaceNode(Object,V,Object)来实现自己的功能。
public boolean remove(Object key, Object value) {
if (key == null)
throw new NullPointerException();
return value != null && replaceNode(key, null, value) != null;
}
public V remove(Object key) {
return replaceNode(key, null, null);
}replaceNode()方法比较长,但主要逻辑还是很清楚的。
此方法是四个remove/replace方法的核心实现,也就是说另外的replace(K,V,V) 、replace(K,V)两个API也是基于replaceNode方法实现。
根据传入replaceNode方法的参数value是否为空,来确定是移除键值对还是替换某个键的值。当参数value为空,表明将移除键值对,而参数value非空则表明要替换某个键的值。
replaceNode的核心逻辑:先根据Key的hash确定节点可能在哪个哈希桶上,若计算出的哈希桶f为空,哈希桶还未被初始化,表明哈希表上不可能存在Key对应的节点,退出死循环,返回空。此哈希桶f的头节点是转移节点,表明table正有扩容,调用helpTransfer方法多线程加速扩容。在helpTransfer方法结束后,自旋重试再来移除键值对或替换某个键的值。若哈希桶的头节点是非转移节点,就使用阻塞锁(synchronized关键字)将此哈希桶f锁住,准备遍历这个哈希桶中的所有节点。若哈希桶是链表结构,就遍历链表查找Key对应的节点e。若遍历过程中找到这样的节点e,且条件允许(即预期被替换值(移除值)与实际被替换值(移除值)相等),就更新节点e的val属性或将e的前后节点直接链接起来以移除节点e .若哈希桶是红黑树,也要遍历红黑树查找节点e,其处理流程与链表相似。若哈希桶的数据结构是ReservationNode等其他类型节点就同样需要自旋重试,等待状态正常时再来移除键值对或替换某个键的值。
final V replaceNode(Object key, V value, Object cv) {
int hash = spread(key.hashCode());
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0 ||
(f = tabAt(tab, i = (n - 1) & hash)) == null)
break; //Key对应的哈希桶为nul,它不可能在哈希表table上,退出死循环
else if ((fh = f.hash) == MOVED)
//和putVal类似,检测有table正有扩容,多线程加速扩容
tab = helpTransfer(tab, f);
else {
V oldVal = null;
boolean validated = false;
synchronized (f) { //锁住这个哈希桶
if (tabAt(tab, i) == f) { //保证table没有被其他线程修改
//fh>0表明f是正常节点,且是链表的头节点(红黑色根节点treeBin的hash是常量-2)
if (fh >= 0) {
validated = true; //f是链表,验证通过
for (Node<K,V> e = f, pred = null;;) {//开始遍历链表
K ek;
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {//找到了Key对应的节点
V ev = e.val;
//cv预期被替换的原value, ev是实际会被替换的原value
//若预期被替换值为空或预期被替换值与实际被替换值相等,就执行value替换或节点移除操作
if (cv == null || cv == ev ||
(ev != null && cv.equals(ev))) {
oldVal = ev;//保存节点的原val,方法最后需要返回此值
//传入当前方法replaceNode的参数value非空,表明调用者想修改节点的val属性
//参数value为空,表明调用者想移除节点
if (value != null)
//"replace(K,V)"和"replace(K,V,V)"传入当前replaceNode方法的参数value均非空。
//预期替换后的新value不为空,就更新此节点的val属性,
//将其设为预期替换后的value.
e.val = value;
else if (pred != null)
// "remove(Object,Ojbect)"和"remove(Object)"传入当前replaceNode方法的参数value都是null
//预期替换后的新value为空,表明这是个移除节点的操作,而前驱节点pred非空,表明e不是链表的头节点。
//将前当前节点e的前驱节点和后继节点直接利用next属性越过自身e将两者链接起来。当前节点e也就被移除了。
pred.next = e.next;
else
//value为空,pred为空,表明这是个移除节点的操作且e是链表的头节点
//将e的后继节点作为链表新的头节点,这个原头节点e也就被移除了
setTabAt(tab, i, e.next);
}
break;//找到节点,退出遍历
}
pred = e;
if ((e = e.next) == null)//遍历完链表也需退出遍历过程
break;
}
}
else if (f instanceof TreeBin) {
validated = true;//f是红黑树,验证通过
TreeBin<K,V> t = (TreeBin<K,V>)f;
TreeNode<K,V> r, p;
if ((r = t.root) != null &&
(p = r.findTreeNode(hash, key, null)) != null) { //找到了Key对应节点
V pv = p.val;
// 若预期被替换值为空或预期被替换值与实际被替换值相等,就执行value替换或节点移除操作
if (cv == null || cv == pv ||
(pv != null && cv.equals(pv))) {
oldVal = pv;//保存节点的原val,方法最后需要返回此值
if (value != null)
//value非空,进行节点value替换(val属性更新)
p.val = value;
else if (t.removeTreeNode(p)) //value为空,进行节点移除
//若移除这个节点p后,红黑树的节点个数太少,则需要将红黑树转为链表,
// 并将这个链表放在table上。
setTabAt(tab, i, untreeify(t.first));
}
}
}
}
}
//只要f是链表或红黑树,validated就会被设为true. 这种情况下可能进行了节点的移除或节点的val属性更新
//而f若是Forwarding这类标识型节点,根本就不可能会执行节点的移除和节点的val属性更新,
// 因为这类节点不存放具体有意义的键值对。
if (validated) {
//节点的原value不为空,表明确实执行节点的移除或节点的val属性更新操作,返回原value
if (oldVal != null) {
if (value == null)
//value为空,执行了移除节点操作,需要更新元素个数。
addCount(-1L, -1);
return oldVal;
}
//没找到Key对应节点或找到节点但条件不满足(这里的条件不满足是指预期被替换值与实际被替换值不等),
// 将返回null
break;
}
}
}
return null;
}1)与JDK1.7相比,在JDK1.8 中ConcurrentHashMap使用了更细粒度的锁,JDK1.7中多个哈希桶对应一把锁,而JDK1.8中一个哈希桶对应一把锁,且不一定会用到这个锁(如在添加键值对时恰好是为哈希桶添加头节点,此时使用CAS自旋)。这能够显著提升并发的效率。
2)与JDK1.7相比,在JDK1.8 ConcurrentHashMap代码逻辑更清晰易懂。在JDK1.7中构造方法需要对segments数组进行初始化,里面包含不少的位运算,需要计算段偏移量、段掩码等。而JDK1.8 中构造方法主要是计算初始化容量,没有太多位运算。
3)与JDK1.7相比,在JDK1.8 ConcurrentHashMap对哈希桶的定位更快. 因为在JDK1.7中需要两次散列计算,先要根据通过segmentFor(int)定位到Segment,再继续在segment中找到哈希桶HashEntry的下标。而JDK1.8中只需要一次散列计算后,即可定位到节点所在的哈希桶。