如果实在懒得看也没有关系,我们来简单回顾一下HashMap的结构:
简单来说,HashMap是一个Entry对象的数组。数组中的每一个Entry元素,又是一个链表的头节点。
Hashmap不是线程安全的。在高并发环境下做插入操作,有可能出现下面的环形链表:
Segment
在高并发的情况下,一般会使用currentHashMap,ConcurrentHashMap对比HashMap主要是增加了Segment的概念。
Segment是什么呢?Segment本身就相当于一个HashMap对象。
同HashMap一样,Segment包含一个HashEntry数组,数组中的每一个HashEntry既是一个键值对,也是一个链表的头节点。
单一的Segment结构如下:
像这样的Segment对象,在ConcurrentHashMap集合中有多少个呢?有2的N次方个,共同保存在一个名为segments的数组当中。
因此整个ConcurrentHashMap的结构如下:
可以说,ConcurrentHashMap是一个二级哈希表。在一个总的哈希表下面,有若干个子哈希表。
这样的二级结构,和数据库的水平拆分有些相似。
Case1:不同Segment的并发写入:
不同Segment的写入是可以并发执行的。
Case2:同一Segment的一写一读:
Segment的写入是需要上锁的,因此对同一Segment的并发写入会被阻塞。
由此可见,ConcurrentHashMap当中每个Segment各自持有一把锁。在保证线程安全的同时降低了锁的粒度,让并发操作效率更高。
Segment的锁
Segment继承了ReentrantLock,因此它实际上是一把锁。在进行put、remove、replace、clear等需要改动内部内容的操作时,都要进行加锁操作,其代码一般是这样的:
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
HashEntry<K,V> node = tryLock() ? null :
scanAndLockForPut(key, hash, value);
V oldValue;
try {
//实际代码……
}
} finally {
unlock();
}
return oldValue;
}
首先调用tryLock,如果加锁失败,则进入scanAndLockForPut(key, hash, value)
该方法实际上是先自旋等待其他线程解锁,直至指定的次数MAX_SCAN_RETRIES
若自旋过程中,其他线程释放了锁,导致本线程直接获得了锁,就避免了本线程进入等待锁的场景,提高了效率
若自旋一定次数后,仍未获取锁,则调用lock方法进入等待锁的场景
采用这种自旋锁和独占锁结合的方法,在很多场景下能够提高Segment并发操作数据的效率。