ConcurrentHashMap
1.ConcurrentHashMap的出现
我们最常用的集合框架一定包括HashMap,但是都知道它不是线程安全的。在并发插入元素的时候,有可能出现带环链表,让下一次读操作出现死循环。
而想要次避免HashMap的线程安全问题有很多办法,比如改用HashTable或者Collections.synchronizedMap。但是,这两者有着共同的问题:性能。无论读操作还是写操作,它们都会给整个集合加锁,导致同一时间的其他操作为之阻塞。
因此,ConcurrentHashMap应运而生。
2.ConcurrentHashMap底层结构
2.1.JDK1.7之前
在了解ConcurrentHashMap之前,首先要了解一个概念Segment。Segment本身就相当于一个HashMap对象。
Segment事实上是一个可重入锁,它继承了Reentrantlock.
同HashMap一样,Segment包含一个HashEntry数组,数组中的每一个HashEntry既是一个键值对,也是一个链表的头结点。
单一的Segment结构如下:
像这样的Segment对象,在ConcurrentHashMap集合中有多少个呢?有2的N次方个,共同保存在一个名为segments的数组当中。
因此整个ConcurrentHashMap的结构如下:
可以说,ConcurrentHashMap是一个二级哈希表。在一个总的哈希表下面,有若干个自哈希表。
使用这样锁分段技术,每一个Segment就好比一个自治区,读写操作高度自治,Segment之间互不影响。
情况1:不同Segment的并发写入
不同Segment的写入是可以并发执行的。
情况2:同一Segment的一写一读
同一Segment的写和读是可以并发执行的。
情况3:同一Segment的并发写入
Segment的写入是需要上锁的,因此对同一Segment的并发写入会被阻塞。
由此可见,ConcurrentHashMap当中每个Segment各自持有一把锁。在保证线程安全的同时降低了锁的粒度,让并发操作效率更高。
我们可以看一下ConcurrentHashMap的读写过程:
Get方法
- 为输入的Key做hash运算,得到hash值。
- 通过hash值,定位到对应的segment对象。
- 再次通过hash值,定位到segment当中的数组的具体位置。
Put方法
- 为输入的key做hash运算,得到hash值。
- 通过hash值,定位到对应的segment对象。
- 获取可重入锁。
- 再次通过hash值,定位到segment当中数组的具体位置。
- 插入或覆盖hashEntry对象。
- 释放锁。
可以看出ConcurrentHashMap在读写时都需要二次定位。首先定位到segment,之后定位到segment内的具体数组下标。
在调用size方法的时候,如何解决一致性的问题?
size方法的目的是统计ConcurrentHashMap的总元素数量,自然需要把各个segment内部的元素数量汇总起来。
但是,如果在统计segment元素数量的过程中,已统计过的segment瞬间插入新的元素,这时候该怎么办呢?
ConcurrentHashMap的size方法是一个嵌套循环,大体逻辑如下:
- 遍历所有的segment。
- 把segment的元素数量累加起来。
- 把segment的修改次数累加起来。
- 判断所有segment的总修改次数是否大于上一次的总修改次数。如果大于,说明统计过程中有修改,重新统计,尝试次数+1;如果不是。说明没有修改,统计结束。
- 如果尝试次数超过阈值,则对每一个segment加锁,再重新统计。
- 再次判断所有segment的总修改次数是否大于上一次的总修改次数。由于已经加锁,次数一定和上次相等。
- 释放锁,统计结束。
可以看看源码:
public int size() {
// Try a few times to get accurate count. On failure due to
// continuous async changes in table, resort to locking.
final Segment<K,V>[] segments = this.segments;
int size;
boolean overflow; // true if size overflows 32 bits
long sum; // sum of modCounts
long last = 0L; // previous sum
int retries = -1; // first iteration isn't retry
try {
for (;;) {
if (retries++ == RETRIES_BEFORE_LOCK) {
// 如果超过阈值,则进行加锁计算
for (int j = 0; j < segments.length; ++j)
ensureSegment(j).lock(); // force creation
}
sum