Java中的concurrenthashmap集合详细剖析
作者:top啦它
concurrenthashmap
有参构造后第一次put时会进行初始化。初始化的容量并不是所传入的数。
源码:
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;
}
由源码可知,会先判断所传入的容量是否>=最大容量的一半,如果满足条件,就会将容量修改为最大值,反之则会将容量改为所传入数*1.5+1。
并且这个tableSizeFor函数会将这个数修改为2**n>initialCapacity + (initialCapacity >>> 1) + 1的最小2**n。
举个例子证明一下:
ConcurrentHashMap<String,String> a = new ConcurrentHashMap<>(32);
Class<? extends ConcurrentHashMap> aClass = a.getClass();
Field sizeCtl = aClass.getDeclaredField("sizeCtl");
sizeCtl.setAccessible(true);
System.out.println(sizeCtl.getName()+":"+sizeCtl.get(a));
输出如下:
sizeCtl:64
这里的sizeCtl有四种情况。
| 类型 | 含义 |
| sizeCtl为0 | 代表此时还未进行初始化操作。 |
| sizeCtl为正数 | 若还未初始化则代表的是初始容量,已经初始化则代表的是扩容阈值 |
| sizeCtl为-1 | 代表正在进行初始化操作 |
| sizeCtl为负数不为-1时 | 代表此时有-1*(1+n)个线程正在进行扩容操作 |
当第一次写入数据时:
源码如下:
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) {//onlyIfAbsent是当存在相同元素时是否覆盖。这里传入了false就是覆盖的意思。
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)//如果table还未进行初始化操作,在第一次写入数据时会进行初始化。
tab = initTable();
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
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 (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;
}
for (Node<K,V>[] tab = table;;) {//这里是一个死循环。这个循环中存在4个判断
分别是:
1、if (tab == null || (n = tab.length) == 0)//代表还未初始化或者当前长度为0。
2、else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {//根据key的hash值来判断将要放入哪个位置,并将f指向这个位置。
如果这个位置目前还不存在数据的话。就会进入
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; 语句。这里存在一个cas自旋。并且会进行双重检测,一旦发现符合条件,就会将要添加的节点放入f指向的位置,并跳出循环,结束。
3、else if ((fh = f.hash) == MOVED)//MOVED为-1,表示当前entry已经迁移,里面的tab = helpTransfer(tab, f);表示协助迁移。迁移完成后或再次循环并进行判断。
4、else中有一把琐synchronized (f) 。在上面的代码中i已经被赋值为(n - 1) & hash)。
这把锁所包括的代码块中
存在双重检测代码if (tabAt(tab, i) == f)。
他的存在就是为了检测曾经获得的桶的头节点的位置是否已经修改。
因为在map中存在树-->链表和链表->树的操作,这回改变桶头节点的位置。
但是这并不能保证安全,
因为在转化后头节点的位置可能不会发生改变,所以还存在if (fh >= 0)这一句代码,
表示f这个节点的hash。
树节点的hash时=是负数。
ok,如果未修改的话他会遍历桶下面的节点,判断是否存在相同的节点。如果存在的话,
就覆盖,不存在就加到链表尾部。
在添加完成后执行:
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
这部分代码表示是否进行树化。如果当前map的长度不小于64,那么当一个桶中的元素大于等于8的时候,会进行
树化。
最后执行addCount(1L, binCount);
这个代码很复杂。可以参考https://www.bilibili.com/video/BV17i4y1x71z?from=search&seid=2906009396999368521&spm_id_from=333.337.0.0
初始化源码:
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(); // lost initialization race; just spin
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
if ((tab = table) == null || tab.length == 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);
}
} finally {
sizeCtl = sc;
}
break;
}
}
return tab;
}
我的理解是在吗map初始化之后,map的长度缩减到0的话,会重新进行一次初始化。
在并发环境下初始化的时候有可能会有多个线程进入此方法。
不过这里存在一个cas自旋锁U.compareAndSwapInt(this, SIZECTL, sc, -1),首先,当未初始化的时候sizeCtl为0,所以会进入else if。可能会有多个线程符合标准进入else if,但是这里存在一个cas自旋锁,当第一个触碰到cas的线程进行操作后,sizeCtl就会被修改为-1,如果修改成功则返回true,当另一个线程进入自旋锁后会发现sizeCtl, sc并不相等了,所以会返回false,离开else if,这时再吃循环到if语句时会发现sizeCtl=-1,然后进入if,执行Thread.yield(),谦让出cpu。
直到初始化完成sizeCtl为扩容阈值为止。此时sizeCtl已经不满足if语句了,所以那些还在谦让的线程就会开始陆陆续续的执行else if语句,发现cas返回true,然后再else if中存在双重检测,判断table是否为null。很显然并不满足。
于是执行finally中的语句,最后跳出循环,返回table。
到此这篇关于Java中的concurrenthashmap集合详细剖析的文章就介绍到这了,更多相关concurrenthashmap集合内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
