ConcurrentHashMap源码解析
[TOC]
jdk8之前的实现原理
jdk8的实现原理
JDK8的实现已经抛弃了Segment分段锁机制,利用CAS+Synchronized来保证并发更新的安全,底层依然采用数组+链表+红黑树的存储结构。
变量解释
table:默认为null,初始化发生在第一次插入操作,默认大小为16的数组,用来存储Node节点数据,扩容时大小总是2的幂次方。
nextTable:默认为null,扩容时新生成的数组,其大小为原数组的两倍。
sizeCtl :默认为0,用来控制table的初始化和扩容操作,具体应用在后续会体现出来。
- -1 代表table正在初始化
- -N 表示有N-1个线程正在进行扩容操作
- 其余情况:
- 1、如果table未初始化,表示table需要初始化的大小。
- 2、如果table初始化完成,表示table的容量,默认是table大小的0.75倍,居然用这个公式算0.75(n - (n >>> 2))。
- 1、如果table未初始化,表示table需要初始化的大小。
Node:保存key,value及key的hash值的数据结构。
ForwardingNode:一个特殊的Node节点,hash值为-1,其中存储nextTable的引用。只有table发生扩容的时候,ForwardingNode才会发挥作用,作为一个占位符放在table中表示当前节点为null或则已经被移动。
初始化
实例化ConcurrentHashMap时带参数时,会根据参数调整table的大小,假设参数为100,最终会调整成256,确保table的大小总是2的幂次方。
1 | private static final int tableSizeFor(int c) { |
初始化table
1 | private final Node<K,V>[] initTable() { |
put操作
1 | final V putVal(K key, V value, boolean onlyIfAbsent) { |
hash算法
1 | static final int spread(int h) {return (h ^ (h >>> 16)) & HASH_BITS;} |
获取table中对应的元素f
1 | static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) { |
Doug Lea采用Unsafe.getObjectVolatile来获取,也许有人质疑,直接table[index]不可以么,为什么要这么复杂?
在java内存模型中,我们已经知道每个线程都有一个工作内存,里面存储着table的副本,虽然table是volatile修饰的,但不能保证线程每次都拿到table中的最新元素,Unsafe.getObjectVolatile可以直接获取指定内存的数据,保证了每次拿到数据都是最新的。
链表或红黑树操作
其余情况把新的Node节点按链表或红黑树的方式插入到合适的位置,这个过程采用同步内置锁实现并发。
1 | synchronized (f) { |
table 扩容
当table容量不足的时候,即table的元素数量达到容量阈值sizeCtl,需要对table进行扩容。
整个扩容分为两部分:
- 构建一个nextTable,大小为table的两倍。
- 把table的数据复制到nextTable中。
这两个过程在单线程下实现很简单,但是ConcurrentHashMap是支持并发插入的,扩容操作自然也会有并发的出现,这种情况下,第二步可以支持节点的并发复制,这样性能自然提升不少,但实现的复杂度也上升了一个台阶。
先看第一步,构建nextTable,毫无疑问,这个过程只能只有单个线程进行nextTable的初始化,具体实现如下:
1 | private final void addCount(long x, int check) { |
get操作
1 | public V get(Object key) { |