由于HashMap 在执行并发put操作时候会引起死循环(多次添加,会覆盖掉原来的vaue值不影响),使用HashTable,在put ()和get() 上都存在线程阻塞,因此效率低下。ConcurrentHashMap的锁分段技术能够有效提升并发访问量。
ConcurrentHashMap 使用分段锁技术,将数据分成一段一段的存储,然后 给每一段数据配一把锁(reentrantlock),当一个线程占用锁访问其中一个段数据的时候,其他 段的数据也能被其他线程访问,能够实现真正的并发访问。读操作大部分时候 都不需要用到锁。只有在 size 等操作时才需要锁住整个 hash 表。 它把区间按照并发级别(concurrentLevel),分成了若干个 segment。默认情 况下内部按并发级别为 16 来创建。对于每个 segment 的容量,默认情况也是 16。当然并发级别(concurrentLevel)和每个段(segment)的初始容量都是可以通 过构造函数设定的。ConcurrentHashMap 使用 segment 来分段和管理锁,segment 继 承 自 ReentrantLock , 因 此 ConcurrentHashMap 使 用 ReentrantLock 来保证线程安全。
ConcurrentHashMap的结构
由Segment 数组结构和HashEntry 数组结构组成。segment是可重入锁。一个ConcurrentHashMap 包含一个segment数组,每个segment结构类似于HashMap,内含一个HashEntry数组,每个hashEntry是一个链表结构的元素,但对HashEntry 数组的数据进行修改时,必须首先获得它对应得segment 锁ConcurrentHashMap 的初始化
segments数组长度(ssize)是通过concurrentLevel计算得出的。为了能够按位与的散列算法来定位数组的索引,必须保证segments数组长度是2的N次方。Hashentry数组的长度cap(initialcapacity /ssize)。同时每个segment还有loadFactor转载因子。
- 定位segment
ConcurrentHashMap使用分段锁segment来保护不同段的数据,在插入或则获取时候,必须通过散列算法定位到segment.一般对元素的hashcode 进行一次再散列减少散列冲突。
从以下散列算法得知,再散列后的hash值需要与segmentMask 做与运算,而segmentmask=ssize-1,如果sszie不是2的幂次方,则segmentmask 对应的二进制位为0,则那么0001、1001、1101等尾数为1的位置就永远不可能被entry占用。这样会造成浪费,不随机等问题。
final Segment<k,v> segmentFor(int hash){
return segments[(hash>>>segmentShift)&segmentMask];
}
4.ConcurrentHshMap的操作
get():无需加锁,get方法中将要使用的变量都定义为volatile类型,之所以不会读到过期的值,是根据 java 内存模型的 happen before 原则, 对 volatile 字段的写入操作先于读操作,即使两个线程同时修改和获取 volatile 变量,get 操作也能拿到最新的值,这是用 volatile 替换锁的经典应用场景。在 ConcurrentHashMap 中,不允许用 null 作为键和值。 所以读到null 时候说明发生了重排序现象,需要加锁重新读这个值。
put():为了保证线程安全,在操作共享变量时候必须加锁,第一步判断是否需要扩容,第二步定位添加元素的位置,将其放入到HashEntry数组。具体过程为通过key 值得到hash 值,然后定位到具体的segment 中执行put 方法,与hashmap put 方法类似。