concurrentHashMap考点分析
concurrentHashMap在JDK 1.7怎么解决线程安全
// 分段数组
final Segment<K,V>[] segments;
static final class Segment<K,V> extends ReentrantLock implements Serializable {
transient volatile HashEntry<K,V>[] table;
transient int count;
transient int modCount;
transient int threshold;
final float loadFactor;
// hash节点
static final class HashEntry<K,V> {
final int hash;
final K key;
volatile V value;
volatile HashEntry<K,V> next;
在JDK 1.7版本, 底层将数组分为多段进行存储/查找.
/**
* The default initial capacity for this table,
* used when not otherwise specified in a constructor.
* 默认容量大小
*/
static final int DEFAULT_INITIAL_CAPACITY = 16;
/**
* The default load factor for this table, used when not
* otherwise specified in a constructor.
* 默认负载因子
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
* The default concurrency level for this table, used when not
* otherwise specified in a constructor.
* 默认的并发级别(这里是segment的默认容量大小)
*/
static final int DEFAULT_CONCURRENCY_LEVEL = 16;
public ConcurrentHashMap(int initialCapacity,
float loadFactor, int concurrencyLevel) {
if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
throw new IllegalArgumentException();
// max_segments 是2^16, 意思是segment数组长度至少为2.
if (concurrencyLevel > MAX_SEGMENTS)
concurrencyLevel = MAX_SEGMENTS;
// Find power-of-two sizes best matching arguments
int sshift = 0;
int ssize = 1;
// 默认传入的concurrencyLevel为16
// 如果传入的concurrencyLevel不为2的幂次方, ssize很巧妙地获取了2的幂次方值.
while (ssize < concurrencyLevel) {
++sshift;
ssize <<= 1;
}
this.segmentShift = 32 - sshift;
this.segmentMask = ssize - 1;
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
int c = initialCapacity / ssize;
if (c * ssize < initialCapacity)
++c;
int cap = MIN_SEGMENT_TABLE_CAPACITY;
while (cap < c)
cap <<= 1;
// create segments and segments[0]
Segment<K,V> s0 =
new Segment<K,V>(loadFactor, (int)(cap * loadFactor),
(HashEntry<K,V>[])new HashEntry[cap]);
// 这里可以了解到segment数组默认的容量大小为16
Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize];
UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0]
this.segments = ss;
}
put API 详解
public V put(K key, V value) {
Segment<K,V> s;
if (value == null)
throw new NullPointerException();
int hash = hash(key);
int j = (hash >>> segmentShift) & segmentMask;
// 通过内存偏移量来获取Segment数组
if ((s = (Segment<K,V>)UNSAFE.getObject // nonvolatile; recheck
(segments, (j << SSHIFT) + SBASE)) == null) // in ensureSegment and
// 如果对应index的segment为空, 需要创建并且通过cas赋值到table.
s = ensureSegment(j);
return s.put(key, hash, value, false);
}
// 确保segment初始化
private Segment<K,V> ensureSegment(int k) {
final Segment<K,V>[] ss = this.segments;
long u = (k << SSHIFT) + SBASE; // raw offset
Segment<K,V> seg;
// segment如果没有初始化
if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) {
Segment<K,V> proto = ss[0]; // use segment 0 as prototype
int cap = proto.table.length;
float lf = proto.loadFactor;
int threshold = (int)(cap * lf);
// 创建一个tab, 通过cas赋值到对应的seg
HashEntry<K,V>[] tab = (HashEntry<K,V>[])new HashEntry[cap];
if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
== null) { // recheck
// 创建seg
Segment<K,V> s = new Segment<K,V>(lf, threshold, tab);
// 自旋去赋值seg
while ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
== null) {
if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s))
break;
}
}
}
return seg;
}
// segment#put API
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
// 第一次尝试获取lock
// 如果为true, node为null
// 如果为false, scanAndLockForPut 尝试自旋去获取锁, 如果在超出自旋次数, 就挂起当前线程.
HashEntry<K,V> node = tryLock() ? null :
scanAndLockForPut(key, hash, value);
V oldValue;
try {
HashEntry<K,V>[] tab = table;
int index = (tab.length - 1) & hash;
// 获取对应下标的HashEntry
HashEntry<K,V> first = entryAt(tab, index);
for (HashEntry<K,V> e = first;;) {
// 如果e不为空
if (e != null) {
K k;
// 判断hash和equals
if ((k = e.key) == key ||
(e.hash == hash && key.equals(k))) {
oldValue = e.value;
if (!onlyIfAbsent) {
e.value = value;
++modCount;
}
break;
}
// 继续下个节点
e = e.next;
}
else {
// 如果第一次tryLock没获取到锁, 但是通过自旋获取到node之后, 判断node是否为null
if (node != null)
node.setNext(first);
else
node = new HashEntry<K,V>(hash, key, value, first);
int c = count + 1;
if (c > threshold && tab.length < MAXIMUM_CAPACITY)
rehash(node);
else
setEntryAt(tab, index, node);
++modCount;
count = c;
oldValue = null;
break;
}
}
} finally {
unlock();
}
return oldValue;
}
简单概括一下put的流程
- 第一次进行
hash运算
, 通过hash
与segment
偏移量进行右移运算并与segment
的mask
值进行位与运算.- 通过其结果
内存偏移量
来获取Segment数组
, 如果segment数组为空,创建tab
并通过cas自旋
的方式赋值给seg
- 尝试
tryLock
获取锁, 如果获取成功,取模获取下标
值, 获取对应下标的HashEntry
- 进行循环判断当前
HashEntry
是否存在, 如果存在就判断其节点的hash
和equals
是否一致, 如果不存在就新建一个HashEntry
, 并判断是否需要扩容
, 如果没有超出阈值
, 就直接赋值到对应的下标.- 如果
tryLock
获取锁失败,scanAndLockForPut
内部尝试不断自旋获取锁, 如果超出自旋
次数就挂起, 等待唤醒
获取锁. 直到获取到锁, 并查到对应的HashEntry
节点.- 跟上述的循环判断差不多一致, 只不过
node
不为null, 可以直接赋值, 不需要通过new来创建新的HashEntry
, 这个步骤在scanAndLockForPut
中已经执行过了.
segment分段锁
, 有效地减小了并发冲突的概率, 但是相应的get/put
性能不高.
concurrentHashMap在JDK 1.8怎么解决线程安全
在JDK 1.8版本中, 底层不再使用
segment
分段锁, 但是这个类并没有在JDK1.8中移除, 官方考虑到兼容性, 而保留了部分属性和API.
/**
* 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.
* 这个是非常重要的一个成员变量.
* 作为数组的控制符, 控制数组的初始化和扩容操作.
* 比如该变量为负值时, -1代表正在进行初始化操作, -N代表有N-1个线程正在进行扩容.
*
* 是的..你没听错, 是多个线程共同参与扩容操作, 而不是单个线程扩容, 其他线程等待其扩容完毕.
* 有点像是forkjoin的工作窃取事项, 保证了多核运算的并行效率最大化.
*
* 如果该变量为0时, 代表数组还未进行初始化.
* 如果该变量为正数时, 代表数组下一次要进行扩容的阈值.
*/
private transient volatile int sizeCtl;
还是老规矩, 先看构造函数
, 再看put
方法
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) ?
// tableSizeFor跟HashMap一致
MAXIMUM_CAPACITY : tableSizeFor((int)size);
this.sizeCtl = cap;
}
跟JDK 1.7相比, 1.8版本通俗易懂了更多.
/** Implementation for put and putIfAbsent */
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
// 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)
// 初始化table
tab = initTable();
// 如果对应的tab下标为空, 通过cas的方式赋值, 成功就退出循环, 否则自旋重试, 当然第二次估计已经不为空了.
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;
// 这里考虑到竞争比较强烈, 不考虑cas自旋, 会占用太多cpu的资源.
synchronized (f) {
// 有点像double-check?
if (tabAt(tab, i) == f) {
if (fh >= 0) {
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
K ek;
// hash和equals判断
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;
// 链表next节点为null, 直接挂上.
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;
}
}
}
// size + 1, 并且判断是否需要扩容(#resizeStamp, #transfer)
addCount(1L, binCount);
return null;
}
参与扩容的过长, 主要是通过
ForwardingNode
这个对象来判断当前数组对应下标的节点是否已经处理过或者为空, 如果节点都已经处理完, 则会通过cas方式更新下一次需要扩容的阈值, 并将当前的sizeCtl
赋值为下一次需要扩容的阈值, 这里扩容的阈值是当前tab数组长度的1.5倍
.
源码中是这样的sizeCtl = (n << 1) - (n >>> 1);
在最后提一下, 为什么JDK 1.8版本之后不再使用
segment
分段锁吧.
笔者的个人见解是这样的.
分段锁的确提升了并行效率, 但是由于需要获取两次下标(segment[]
和HashEntry[]
), 在性能上并不高效. 而且在JDK 1.8版本中, 对于链表和红黑树的遍历插入操作, 采取的是synchronized
加锁, 考虑到读写场景的竞争,cas
采取的策略是减小用户态和内核态之间切换
的开销, 代表是占用当前的cpu资源不释放, 当然超出自旋次数会挂起.synchronized
作为jvm锁
, 在jdk版本的不断迭代过长中, jvm锁的性能也逐渐跟了上来, 对应的也有锁升级过长, 从无锁
到偏向锁
到自旋锁
到重量级锁
, 性能和Lock锁并没有差距太大.更何况在写场景竞争大的场景, cas的表现并没有synchronized
表现的更加优秀.
还有一点, lock锁最大的优点就是在粒度比较大的场景,condition
非常灵活, 但是在这种粒度比较小的场景, 拿单个condition
和单个monitor
来体现的, 并没有表现出特别大的优势.
以上, 是笔者的见解.