前言
前面说过HashMap不是线程安全的,如果在高并发的情况下采用ConcurrentHashMap比较安全,它也是属于并发包中的一员,在Jdk1.8中还是采用“数组”+“链表”+“红黑树”的形式进行构造的,下面来看具体的形式。
Node类
Node类与HashMap中有一定的不同,他的next,val都是有家volatile关键字,在多个线程读的时候可以正确同步,保证值的可见性。
1 | static class Node<K,V> implements Map.Entry<K,V> { |
可以看到setValue方法会抛出异常,因为在多线程环境下不能确保数据的唯一性,新增了一个find方法找对应的Node节点,与hashMap的方法类似,通过hash和key结合的方法去寻找对应的节点。
有趣的原子操作
1 | @SuppressWarnings("unchecked") |
可以看到compareAndSwapObject调用了CAS操作来确保数据的一致性。而getObjectVolatile方法能确保当前tab上的Node是最新的。putObjectVolatile保证了其他线程对该tab上Node的可见性。其中充分运用到了Unsafe类获取地址便宜量。
1 | static { |
可以看到调用objectFieldOffset方法来获取字典在内存地址的偏移量。
put方法
来看看put方法的源码实现,首先来看看initTable方法,它是put方法的基础,用来做初始化容器的。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
while ((tab = table) == null || tab.length == 0) {
if ((sc = sizeCtl) < 0)
Thread.yield(); // lost initialization race; just spin
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
if ((tab = table) == null || tab.length == 0) {
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
table = tab = nt;
sc = n - (n >>> 2);
}
} finally {
sizeCtl = sc;
}
break;
}
}
return tab;
}
可以看到当table为空的时候,第一步首先判断sizeCtl是否小于0,小于0则表示当前已经初始化了,则让当前线程保持就绪状态,等待cpu调度。否则就调用compareAndSwapInt方法判断内存的值与sc是否一致,一致则赋值为-1,这里又判断了一次table是否为空,加强严谨性,接着初始化长度为n的Node数组给table,sizeCtl的值变为原来的0.75,变为扩容的阈值。
1 | final V putVal(K key, V value, boolean onlyIfAbsent) { |
- 可以看到在并发的情况下,put方法还是比较复杂的,因为涉及到多个线程对Node数组的操作,塞数据的时候首先判断该数据的key和val是不是空,为空直接抛出异常了。
- 接着获取到hash的值,为啥后面有一个for死循环了,因为可能存在扩容的可能性。
- 当前tab是否为空,为空则调用initTable方法。
- 不为空判断(n - 1) & hash)的位置上是否为空,为空则直接插入
- 如果f.hash为MOVED,则要进行扩容处理。
- 否则要锁住当前节点,往后续遍历,如果遇到相同的key则覆盖并返回oldVal,没有相同的值则在尾部插入新的节点。
- 如果是红黑树节点就去红黑树节点添加。
- 如果当前节点所在的链表长度超过8个了,需要将该链表转换为红黑树。
- 将当前ConcurrentHashMap的元素数量加1。
笔者好奇的是它是如何实现扩容的,进入helpTransfer看看扩容机制:
1 | final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) { |
这段代码是检测当前正在扩容时,可以帮忙去扩容,可以看到最终还是调用了transfer方法,看看transfer方法的实现:
1 | private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) { |
可以看到transfer方法是非常复杂的,但是它的思路还是和HashMap差不多的,通过将原tab的数据依次复制到新的tab上,都是通过CAS操作在nextTab中的i位置和i+n的位置插入原来的节点,完成扩容任务。
get方法
get方法的源码就比上面的简单多了,来看下具体的流程:
1 | public V get(Object key) { |
可以看到,首先去拿在table中的位置h,如果table中存在这个key,且是头节点的话直接返回val,如果h小于0,则去树中和链表中寻找。
总结
jdk1.8的ConcurrentHashMap进行大量的优化,它的锁力度更小,锁到Node上面来了,通过大量的CAS操作保证多线程数据的一致性,但是原理方面与HashMap大体是一致的,值得好好研究。