您的位置:首页 > 编程语言 > Java开发

更好地理解jdk1.8中ConcurrentHashMap实现机制

2017-08-03 02:00 344 查看

前言

Java JDK升级到1.8后有些集合类的实现有了变化,其中ConcurrentHashMap就有进行结构上的大调整。jdk1.6、1.7实现的共同点主要是通过采用分段锁Segment减少热点域来提高并发效率,1.8版本的实现有哪些变化呢?

重要概念

在正式研究前,我们需要先知道几个重要参数,提前说明其值所代表的意义以便更好的讲解源码实现。

table

所有数据都存在table中,table的容量会根据实际情况进行扩容,table[i]存放的数据类型有以下3种:

- TreeBin 用于包装红黑树结构的结点类型

- ForwardingNode 扩容时存放的结点类型,并发扩容的实现关键之一

- Node 普通结点类型,表示链表头结点

nextTable

扩容时用于存放数据的变量,扩容完成后会置为null。

sizeCtl

首先看下源码中对sizeCtl的描述:

/**
* Table initialization and resizing control.  When negative, the
* table is being initialized or resized: -1 for initialization,
* else -(1 + the number of active resizing threads).  Otherwise,
* when table is null, holds the initial table size to use upon
* creation, or 0 for default. After initialization, holds the
* next element count value upon which to resize the table.
*/
private transient volatile int sizeCtl;


注释已经说的很清楚了,以volatile修饰的sizeCtl用于数组初始化与扩容控制,它有以下几个值:

if table未初始化:
=0  //未指定初始容量时的默认值
>0  //指定初始容量(非传入值,是2的幂次修正值)大小的两倍
=-1 //表明table正在初始化
else if nextTable为空:
if 扩容时发生错误(如内存不足、table.length * 2 > Integer.MAX_VALUE等):
=Integer.MAX_VALUE    //不必再扩容了!
else:
=table.length * 0.75  //扩容阈值调为table容量大小的0.75倍
else:
=-(1+N)  //N的低RESIZE_STAMP_SHIFT位表示参与扩容线程数,后面详细介绍


其它的分析源码时再细说。

put()

public V put(K key, V value) {
return putVal(key, value, false);
}

final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode());   //@1
int binCount = 0;  //i处结点标志,0: 未加入新结点, 2: TreeBin或链表结点数, 其它:链表结点数。主要用于每次加入结点后查看是否要由链表转为红黑树
for (Node<K,V>[] tab = table;;) {   //CAS经典写法,不成功无限重试
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0)
//除非构造时指定初始化集合,否则默认构造不初始化table,真正添加时元素检查是否需要初始化。
tab = initTable();  //@2
//CAS操作得到对应table中元素
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { //3
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break;                   //null创建Node对象做为链表首结点
}
else if ((fh = f.hash) == MOVED)  //当前结点正在扩容
tab = helpTransfer(tab, f); //扩容完毕再在新table中放入键值对,扩容节细讲
else {
V oldVal = null;
synchronized (f) {
if (tabAt(tab, i) == f) {  //双重检查i处结点未变化
if (fh >= 0) {  //表明是链表结点类型,hash值是大于0的,即spread()方法计算而来
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) {
//jdk1.8版本是把新结点加入链表尾部,next由volatile修饰
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) {   //@4
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)  //默认桶中结点数超过8个数据结构会转为红黑树
treeifyBin(tab, i);   //@5
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);  //更新size,检测扩容
return null;
}


注释已说的比较明白,上面的代码中的数字注释再单独细说下:

spread()

jdk1.8的hash策略,与以往版本一样都是为了减少hash冲突:

static final int HASH_BITS = 0x7fffffff; // usable bits of normal node hash   //01111111_11111111_11111111_11111111

static final int spread(int h) {
//无符号右移加入高位影响,与HASH_BITS做与操作保留对hash有用的比特位,有让hash>0的意思
return (h ^ (h >>> 16)) & HASH_BITS;
}


initTable()

initTable()值得一提的是调用时是没有加锁的,那么如何处理并发呢?

由下面代码可以看到,当要初始化时会通过CAS操作将sizeCtl置为-1,而sizeCtl由volatile修饰,保证修改对后面线程可见。

这之后如果再有线程执行到此方法时检测到sizeCtl为负数,这个线程就会告诉调度程序,我可以放弃对CPU的使用权让其它兄弟做做事啦!至于会不会理会就是另外一回事了~

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();
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
if ((tab = table) == null || tab.length == 0) {
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;  //DEFAULT_CAPACITY为16
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>
;
table = tab = nt;
sc = n - (n >>> 2);   //新容量的0.75倍
}
} finally {
sizeCtl = sc;   //扩容保护
}
break;
}
}
return tab;
}


tabAt()/casTabAt()/setTabAt()

ABASE表示table中首个元素的内存偏移地址,所以
(long)i << ASHIFT) + ABASE
得到table[i]的内存偏移地址:

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);
}

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);
}

static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) {
U.putObjectVolatile(tab, ((long)i << ASHIFT) + ABASE, v);
}


这里我们可以看到对i位置结点的写操作有两个方法,为什么要定义两个方法,casTabAt()与setTabAt()到底有何区别?

源码中有这样一段注释:

* Note that calls to setTabAt always occur within locked regions,
* and so in principle require only release ordering, not
* full volatile semantics, but are currently coded as volatile
* writes to be conservative.


所以真正要进行有原子语义的写操作需要使用casTabAt()方法,setTabAt()是在锁定桶的状态下需要使用的方法,如此方法实现只是带保守性的一种写法而已。

放松一下继续~



TreeBin

注释4、5都是有关TreeBin的操作,为进一步提升性能,ConcurrentHashMap引入了红黑树,红黑树是一种自平衡二叉查找树,有如下性质:

每个节点要么是红色,要么是黑色。

根节点永远是黑色的。

所有的叶节点都是空节点(即 null),并且是黑色的。

每个红色节点的两个子节点都是黑色。(从每个叶子到根的路径上不会有两个连续的红色节点)

从任一节点到其每个叶子的所有简单路径都包含相同数目的黑色节点。

图例:



treeifyBin()

桶内元素超时8个时会调用到此方法。

private final void treeifyBin(Node<K,V>[] tab, int index) {
Node<K,V> b; int n, sc;
if (tab != null) {
if ((n = tab.length) < MIN_TREEIFY_CAPACITY)
tryPresize(n << 1);  //如果数组整体容量太小则去扩容,放弃转红黑树结构
else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {
synchronized (b) {
if (tabAt(tab, index) == b) {
TreeNode<K,V> hd = null, tl = null;
for (Node<K,V> e = b; e != null; e = e.next) {
TreeNode<K,V> p =
new TreeNode<K,V>(e.hash, e.key, e.val,
null, null);
if ((p.prev = tl) == null)
hd = p;
else
tl.next = p;
tl = p;
}
setTabAt(tab, index, new TreeBin<K,V>(hd));


可以看出按原Node链表顺序转为了TreeNode链表,每个TreeNode的prev、next已完备,传入头结点hd构造红黑树。

TreeBin构造函数

TreeBin(TreeNode<K,V> b) {
//Node(int hash, K key, V val, Node<K,V> next)
super(TREEBIN, null, null, null);
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;
//key有实现Comparable接口则使用compareTo()进行比较,否则采用tieBreakOrder中默认的比较方式,即比较hashCode。
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) {  //左子节点或右子节点为空则在p下添加新结点,否则p的值更新为子节点继续查找。红黑树中结点p.left <= p <= p.right
x.parent = xp;  //保存新结点的父结点
if (dir <= 0)
xp.left = x; //排序小的放左边
else
xp.right = x;  //排序大的放右边
r = balanceInsertion(r, x);  //平衡红黑树
break;
...
this.root = r;
...
}


putTreeVal()与此方法遍历方式类似不再介绍。

扩容实现

写这篇文章主要就是想讲讲扩容,Let’s go!

什么时候会扩容?

使用put()添加元素时会调用addCount(),内部检查sizeCtl看是否需要扩容。

tryPresize()被调用,此方法被调用有两个调用点:

链表转红黑树(put()时检查)时如果table容量小于64,则会触发扩容。

调用putAll()之类一次性加入大量元素,会触发扩容。

addCount()

addCount()与tryPresize()实现很相似,我们先以addCount()分析下扩容逻辑:

private final void addCount(long x, int check) {
...
//check就是binCount,有新元素加入成功才检查是否要扩容。
if (check >= 0) {
Node<K,V>[] tab, nt; int n, sc;
//大于当前扩容阈值并且小于最大扩容值才扩容,如果table还未初始化则等待初始化完成。
while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
(n = tab.length) < MAXIMUM_CAPACITY) {
int rs = resizeStamp(n);  //@1
if (sc < 0) {  //已经有线程在进行扩容工作
//检查是原容量为n的情况下进行扩容,保证sizeCtl与n是一块修改好的,条件2与条件3在当前RESIZE_STAMP_BITS情况下应该不会成功,欢迎指正。条件4与条件5确保tranfer()中的nextTable相关初始化逻辑已走完。
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))  //有新线程参与扩容则sizeCtl统计加1
transfer(tab, nt);
}
else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))  //有线程检测到需要扩容时走这里,初始值为(rs << RESIZE_STAMP_SHIFT) + 2)),+2没什么特别,只是为符合-(1+扩容线程数)的定义。
transfer(tab, null);
s = sumCount();
}
}
}


resizeStamp()

在上面的代码中首先有调用到这样的一个方法,它代表什么意思呢?

/**
* 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;

static final int resizeStamp(int n) {
return Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1));
}


可以看到这个方法的返回一个与table容量n大小有关的扩容标记,而n是2的幂次,可得知返回值rs对于不同容量大小的table值必然不相同,经过
rs << RESIZE_STAMP_SHIFT
变为负数后再赋值给sizeCtl,那么在扩容时sizeCtl值的意义便如下图所示:

高RESIZE_STAMP_BITS位低RESIZE_STAMP_SHIFT位
扩容标记并行扩容线程数
sizeCtl值的意义明白后其它部分看注释即可。

tryPresize()

private final void tryPresize(int size) {
int c = (size >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY :
tableSizeFor(size + (size >>> 1) + 1);  //检查是否直接设置为最大容量
int sc;
while ((sc = sizeCtl) >= 0) {
Node<K,V>[] tab = table; int n;
if (tab == null || (n = tab.length) == 0) {
n = (sc > c) ? sc : c;   //table未初始化则给一个初始容量,这边sc可能为Integer.MAX_VALUE
//后面相似代码不再讲解
if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
if (table == tab) {
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>
;
table = nt;
sc = n - (n >>> 2);
}
} finally {
sizeCtl = sc;
}
}
}
else if (c <= sc || n >= MAXIMUM_CAPACITY)
break;
else if (tab == table) {
int rs = resizeStamp(n);
if (sc < 0) {
Node<K,V>[] nt;
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);
}
}
}


addCount()与tryPresize()处扩容有何区别?

从上面的分析中我们可以看,addCount()是扩容是老老实实按容量x2来扩容的,而tryPresize()会传入一个size参数,可能一次性扩容很多倍。后面采用一样的方式调用transfer()来进行真正的扩容处理。

接下来继续分析transfer()方法。

transfer()

jdk1.8版本的ConcurrentHashMap支持无锁并行扩容,上面已经分析了一小部分,下面这个方法是真正进行并行扩容的地方。

private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
int n = tab.length, stride;
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)  //每个线程处理桶的最小数目,可以看出核数越高步长越小,最小16个。
stride = MIN_TRANSFER_STRIDE; // subdivide range
if (nextTab == null) {    //不会重复初始化,addCount()处已有分析。
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;
}
nextTable = nextTab;
transferIndex = n;  //扩容总进度,transferIndex之后的桶都已分配出去。
}
int nextn = nextTab.length;
ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);  //准备扩容时的特殊节点
boolean advance = true;  //在table[i]的桶是否已完成迁移,这里的初始值不用理。
boolean finishing = false; //table内所有桶都已迁移到nextTable标志位。
for (int i = 0, bound = 0;;) {
Node<K,V> f; int fh;
while (advance) {
int nextIndex, nextBound;
if (--i >= bound || finishing)  //倒序处理职责内节点
advance = false;
//还未进行桶分配或一次迁移结束才会进行这个判断,为true表明要迁移的桶都已分配完毕,线程你就退出干活吧!
else if ((nextIndex = transferIndex) <= 0) {
i = -1;
advance = false;
}
else if (U.compareAndSwapInt
(this, TRANSFERINDEX, nextIndex,
nextBound = (nextIndex > stride ?
nextIndex - stride : 0))) {  //transferIndex减少已分配出去的桶。
//确定当前线程每次分配的待迁移桶的范围[bound, nextIndex)
bound = nextBound;
i = nextIndex - 1;
advance = false;
}
}
//当前线程自己的活已经做完或所有线程的活都已做完。
if (i < 0 || i >= n || i + n >= nextn) {
int sc;
if (finishing) {  //所有线程已干完活,最后才走这里。
nextTable = null;
table = nextTab;  //替换新table
sizeCtl = (n << 1) - (n >>> 1); //调sizeCtl为新容量0.75倍。
return;
}
////还未进行桶分配或一次迁移结束才会进行这个判断,为true表明要迁移的桶都已分配完毕,线程你就退出干活吧!
if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
////还记得addCount()处给sizeCtl赋的初值吗?这里检查是不是所有线程都干完活了,是的话置finishing=advance=true,为保险再检查一次。
if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
return;
finishing = advance = true;
i = n; // recheck before commit
}
}

//下面是正常的扩容逻辑,i处没节点放入ForwardingNode,方便其它线程检测。
//ForwardingNode的作用主要有两个:1. 标明此节点已完成迁移,2. 为方便扩容期间的元素查找需求,里面有find()方法是从nextTable查找元素。
else if ((f = tabAt(tab, i)) == null)
advance = casTabAt(tab, i, null, fwd);
else if ((fh = f.hash) == MOVED)  //已迁移过,跳过。
advance = true; // already processed
else {
synchronized (f) {  //对桶内元素迁移加锁。
if (tabAt(tab, i) == f) {
Node<K,V> ln, hn;
if (fh >= 0) {  //链表
int runBit = fh & n;
Node<K,V> lastRun = f;
//找出最后一段完整的fh&n不变的链表
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);
}
//1.6版本扩容套路,假设最后一段完整的fh&n不变的链表的runbit都是0,则nextTab[i]内元素ln前逆序,ln及其之后顺序,nextTab[i+n]内元素全部相对原table逆序。
setTabAt(nextTab, i, ln);
setTabAt(nextTab, i + n, hn);
setTabAt(tab, i, fwd);  //在原table中设置ForwardingNode节点以提示该桶扩容完成。
advance = true;
}
else if (f instanceof TreeBin) { //红黑树处理,相似。
...


helpTransfer()

添加、删除节点之处都会检测到table的第i个桶是ForwardingNode的话会调用helpTransfer()方法。

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);
while (nextTab == nextTable && table == tab &&
(sc = sizeCtl) < 0) {
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;
}


代码很熟悉吧?前面已经介绍,可以看出此方法的作用帮助新线程加入到扩容的队伍中。

并发扩容总结

单线程新建nextTable,扩容为原table容量的两倍。

每个线程想增/删元素时,如果访问的桶是ForwardingNode节点,则表明当前正处于扩容状态,协助一起扩容完成后再完成相应的数据更改操作。

扩容时将原table的所有桶倒序分配,每个线程每次最小分16个桶进行处理,防止资源竞争导致的效率下降, 每个桶的迁移是单线程的,但桶范围处理分配可以多线程,在没有迁移完成所有桶之前每个线程需要重复获取迁移桶范围,直至所有桶迁移完成。

一个旧桶内的数据迁移完成但迁移工作没有全部完成时,查询数据委托给ForwardingNode结点查询nextTable完成(这个后面看find()分析)。

迁移过程中sizeCtl用于记录参与扩容线程的数量,全部迁移完成后sizeCtl更新为新table的扩容阈值。

扩容节结束!其它常用操作再说下。

get()

public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
int h = spread(key.hashCode());
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) {
if ((eh = e.hash) == h) {  //总是检查头结点
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;
}
else if (eh < 0)  //在迁移或都是TreeBin
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;
}
}
return null;
}


可以看到get()操作如果查询链表不用加锁,如果有红黑树结构的话e.find()方法内部实现需要获取锁。

remove()

public V remove(Object key) {
return replaceNode(key, null, null);
}

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;
else if ((fh = f.hash) == MOVED)  //删除时也需要确实扩容完成后才可以操作。
tab = helpTransfer(tab, f);
else {
V oldVal = null;
boolean validated = false;
synchronized (f) {
if (tabAt(tab, i) == f) {
if (fh >= 0) {
validated = true;
for (Node<K,V> e = f, pred = null;;) {
K ek;
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
V ev = e.val;
if (cv == null || cv == ev ||
(ev != null && cv.equals(ev))) {  //cv不为null则替换,否则是删除。
oldVal = ev;
if (value != null)
e.val = value;
else if (pred != null)
pred.next = e.next;
else
//没前置节点就是头节点
setTabAt(tab, i, e.next);
}
break;
}
pred = e;
if ((e = e.next) == null)
break;
}
}
else if (f instanceof TreeBin) {
validated = true;
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) {
V pv = p.val;
if (cv == null || cv == pv ||
(pv != null && cv.equals(pv))) {
oldVal = pv;
if (value != null)
p.val = value;
else if (t.removeTreeNode(p))
setTabAt(tab, i, untreeify(t.first));
}
}
}
}
}
if (validated) {
if (oldVal != null) {
if (value == null)
addCount(-1L, -1);
return oldVal;
}
break;
}
}
}
return null;
}


主要改进

CAS无锁算法与synchronized保证并发安全,数据结构变更为数组+链表+红黑树,提高性能。

jdk1.8版弃用变量

Segment

只有序列化时会用到。

loadFactor

仅用于构造函数中设定初始容量,已不能影响扩容阈值,jdk1.8版本中阈值计算基本恒定为0.75。

concurrencyLevel

只能影响初始容量,table容量大小与它无关。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: