力求简单而明确地直指 JDK1.7 ConcurrentHashMap 的设计要点。
线程安全之 ConcurrentHashMap
在 JDK 中,有 java.util.concurrent
包,里面存有这一些线程安全的集合类。其中应用最多的就是 ConcurrentHashMap
,这是一个线程安全的 HashMap
,不同的 JDK 版本可能使用了不同的技巧来保证这个线程安全的特性。这里我讨论的是 JDK1.7 版本的线程安全设计。
JDK7 采用了分段锁的方式来保证 ConcurrentHashMap
的线程安全,分段锁的工作原理是:
在 HashMap
下需要完成线程安全可以使用 Collections.synchronizedMap
构造一个线程安全的 HashMap,他的原理就是利用 synchronized
关键字对基本上所有的方法进行上锁,是一种非常粗暴的解决方案。可想而知的是低性能换来的线程安全。
而 ConcurrentHashMap
下有多个 Segment
(默认 16 个), Segment
下是一个 HashTable
用来存 key-value。 Segment
是继承 ReentrantLock
,可以直接享有相关的锁方法。一个数据存入 ConcurrentHashMap
需要先进行一次 hash 来确定他是在哪个 Segment
下,再对该 Segment
上锁写数据。可以看到这一点已经比前者的实现高明了。
ConcurrentHashMap 的性能优化
上一节中提到了 put
操作会对 Segment
上锁,事实上这里 JDK7 还有所优化,调用 put
方法会先尝试获取锁,tryLock()
这个是 ReentrantLock
的方法,获取不到会继续去尝试 scanAndLockForPut()
,这个方法里会不停的 tryLock(),这里会涉及到 MAX_SCAN_RETRIES
次尝试 tryLock()
,超过次数了会直接 lock()
获取锁。在操作最后会在 try...finally
里 unlock()
释放锁。
static final int MAX_SCAN_RETRIES =
Runtime.getRuntime().availableProcessors() > 1 ? 64 : 1;
还有 size()
方法获取 ConcurrentHashMap
的大小时,会有一系列的操作来保证得到准确的 size。
获取 size 大小是一个无限循环,每次循环会先自增一次 retries
,只有 retries == RETRIES_BEFORE_LOCK(=2)
时才会将所有的 Segment
都上锁再计算。这里有个 trick,会记录上一次的 Segment
的 modCount 总值,在下一次计算时会比较,如果不相等,那就是再来一次了。这里利用了 map 中 modCount 机制。
for (;;) {
// 先尝试直接获取 size 大小,这里有计算一个 modCount的数值,会和上次(last)作比较,如果一样的话,说明map没有做增删操作啥的,就是正确的了
// 如果尝试的次数超过了 RETRIES_BEFORE_LOCK, 就直接去锁 segment 再计数
if (retries++ == RETRIES_BEFORE_LOCK) {
for (int j = 0; j < segments.length; ++j)
ensureSegment(j).lock(); // force creation
}
sum = 0L;
size = 0;
overflow = false;
for (int j = 0; j < segments.length; ++j) {
Segment<K,V> seg = segmentAt(segments, j);
if (seg != null) {
sum += seg.modCount;
int c = seg.count;
if (c < 0 || (size += c) < 0)
overflow = true;
}
}
if (sum == last)
break;
last = sum;
}
至于其他的方法,涉及到的锁步骤无非也就上面的重复了,就不多谈了。在此留笔,方便日后再记。
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 [email protected]