为什么要分析ConcurrentHashMap(以下简称 CHM)呢?
HashMap-HashTable,对Java熟悉的应该知道,HashMap操作高效,在平常工作时会经常用到,但它是无锁的,没考虑多线程环境,所以对于多线程环境下不适用,操作不安全,容易造成数据混乱,为此JDK特意出了一个HashTable的工具类,HashTable采用synchronized关键字修饰方法,使得多线程环境下,对HashTable的访问及操作都是串行执行的,以此保证操作数据的安全性,这样虽然弥补了HashMap不支持多并发的缺点,但也产生了一些新的问题,比如所有方法都用synchronized修饰,这会造成未获得资源锁的线程挂起,增加线程上下文切换,消耗CPU,效率较低。
好啦,言归正传,接下来开始分析源码,来看看CHM到底是怎样高效并发的!
源码分析
1、Segment
这是CHM中的Segment定义,从中可看出Segment 继承了 j.u.c 包下的ReentrantLock,每个segment一个锁,且这个锁是可重入的,可重入意味着 它不用像 synchronized 那样,每次操作都竞争,在相同线程操作时可重入,减少了一部分线程切换。
static final class Segment<K,V> extends ReentrantLock implements Serializable {
transient volatile HashEntry<K,V>[] table;
}
Segment的 put、remove 、replace 、clear 用到了锁,无get方法,为啥没有呢,原因很简单,看上面Segment的定义,发现table 数组数据是用 volatile 关键字修饰了的,而 volatile 的特性就是一个线程修改,其他线程立即可见,所以直接将get()方法写在了CHM中,这里不得不佩服源码开发者,这个设计十分巧妙,为什么呢?因为我们一般用 CHM 是用来缓存数据的,当对 CHM 的操作中查询操作较多时,这个设计的效果就体现了,volatile 无锁,在多线程访问时,不用加锁、释放锁,提高了访问的效率,同时还降低了 CPU 切换上下文次数带来的负面影响,大大提高了程序速度。咦,到了这里,大家是否已经发现, CHM 已经对 HashTable 做了一个很重要的优化: get 不加锁。
这是 CHM 的 get() 方法源码
public V get(Object key) {
Segment<K,V> s; // manually integrate access methods to reduce overhead
HashEntry<K,V>[] tab;
int h = hash(key);
long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&
(tab = s.table) != null) {
for (HashEntry<K,V> e = (HashEntry<K,V>) UNSAFE.getObjectVolatile
(tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE);
e != null; e = e.next) {
K k;
if ((k = e.key) == key || (e.hash == h && key.equals(k)))
return e.value;
}
}
return null;
}
再看下 HashTable 的 get() 方法:
public synchronized V get(Object key) {
Entry tab[] = table;
int hash = hash(key);
int index = (hash & 0x7FFFFFFF) % tab.length;
for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) {
if ((e.hash == hash) && e.key.equals(key)) {
return e.value;
}
}
return null;
}
2、Unsafe、CAS、DCL特性
我们来看下 put() 方法:
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;
if ((s = (Segment<K,V>)UNSAFE.getObject // nonvolatile; recheck
(segments, (j << SSHIFT) + SBASE)) == null) // in ensureSegment
s = ensureSegment(j);
return s.put(key, hash, value, false);
}
首先,判断 value 为 null 时直接抛出异常,为什么没有判断 key ? CHM 不是不支持 key 为 null 吗?
别急,我们慢慢看下,往下走是根据 key 来计算 hash 值,看下 hash() 源码:
private int hash(Object k) {
int h = hashSeed;
if ((0 != h) && (k instanceof String)) {
return sun.misc.Hashing.stringHash32((String) k);
}
h ^= k.hashCode();
// Spread bits to regularize both segment and index locations,
// using variant of single-word Wang/Jenkins hash.
h += (h << 15) ^ 0xffffcd7d;
h ^= (h >>> 10);
h += (h << 3);
h ^= (h >>> 6);
h += (h << 2) + (h << 14);
return h ^ (h >>> 16);
}
这里对 k 有操作,如果 k 为 null ,那么这里将会由 JVM 直接抛出空指针异常,所以 CHM 是不支持 key 为 null 的。
好,解决了疑问,我们再接着上面的 put() 方法往下看:
int j = (hash >>> segmentShift) & segmentMask;
这里通过 key 的 hash 值,通过位操作,得到段 ID 值 j,这就是 CHM 分段,再往下看:
if ((s = (Segment<K,V>)UNSAFE.getObject // nonvolatile; recheck
(segments, (j << SSHIFT) + SBASE)) == null) // in ensureSegment
s = ensureSegment(j);
这里通过上面得到的段 ID 值 j,去得到 Segment 对象,如果 s 为 null ,就会通过 ensureSegment() 方法得到一个新的段,
内部方法如下:
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;
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);
HashEntry<K,V>[] tab = (HashEntry<K,V>[])new HashEntry[cap];
if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
== null) { // recheck
Segment<K,V> s = new Segment<K,V>(lf, threshold, tab);
while ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
== null) {
if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s))
break;
}
}
}
return seg;
}
其中,可以看到,ss = this.segments 被 final 修饰了,这里保证不可变,seg 是通过 UNSAFE.getObjectVolatile 来获取的,也就是说这个 seg 对象的修改对其他线程是立即可见的,然后通过 recheck 结合 CAS 操作,保证 segment 创建的原子性。
这里和我想的不一样,按理原子性的创建应该要用锁,但源码就偏偏不用,其实这里就是 DSL 的概念,双重检测锁。
3、分段锁
还是接着上面 put() 方法的最后一行
return s.put(key, hash, value, false);
这里是调用了 segment 的 put () 方法,再进去看下
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
HashEntry<K,V> node = tryLock() ? null :
scanAndLockForPut(key, hash, value);
V oldValue;
try {
......
// 省略put逻辑
} finally {
unlock();
}
return oldValue;
}
这里省略 put 进去的逻辑,主要看下锁的使用,看这段代码,好像没有锁定的方法是吧,其实有个隐藏的地方,scanAndLockForPut() 方法,在这方法的内部是用了 lock 的,并且 scanAndLockForPut 方法内部 lock 后没释放,在外层 put() 方法 finally 块进行释放的。这就是分段锁的使用,它锁定的并不是 CHM 对象,而是 一个 Segment ,这样就减少了锁的竞争。
总结CHM 的特性
1、不支持 为null 的Key 和 Value,与HashTable 一样
2、对HashTable 的改进版本,使用对 Segment段加锁,用锁分离技术替换单个锁,提高并发效率
3、通过key的 hash 值,按 hash值分段(Segment)存储
4、弱一致性(weakly consistent),not fail-fast
关键词:锁分离技术(每个segment一个锁,对锁进行粒化),Segment,CAS,volatile,final,Unsafe,DCL(双重检测锁)