ConcurrentHashMap原理探析
HashMap
在我们的开发中可以说是很常见了,主要是用来存储键值对的数据结构,HashMap
并不是线程安全的,如果要想实现线程安全,可以使用Collections.synchronizeMap(hashMap)
的方式,当然JDK
里也以提供了另外一个可以实现并发操作的键值对存储结构,那就是ConcurrentHashMap
,那么接下来我们就来探析下他是如何实现线程安全的,看看put
方法的实现机制。
1 | public V put(K key, V value) { |
1 | static final int spread(int h) { |
首先获取key
到hashCode
,调用spread
方法,将hashCode
的高 16 位和低 16 位进行异或操作,再与HASH_BITS
进行与操作,HASH_BITS
的值是 0x7fffffff,也就是 32 位带符号的最大整数,效果等同于取余,异或主要是为了降低碰撞的几率。接着进入循环之中,首先判断table
是否为空,如果为空的话,则进行初始化
1 | private final Node<K,V>[] initTable() { |
这里其实也用到了Unsafe
和 CAS 机制,如果不了解可以自行先去看下,因为我们是用无参构造方法创建的对象,那么sizeCtl
变量的值就是 0,然后利用 CAS 操作将sizeCtl
设置为 -1,此时若有其他线程进入此方法,则会执行Thread.yield();
,使当前线程从运行状态变为就绪状态,再回到创建table
的线程,创建了一个起始大小为DEFAULT_CAPACITY = 16;
长度的Node
数组,并设置sizeCtl
的值为 12,到此table
就创建完成了。再回到循环中,然后通过与数组长度与运算,根据内存地址判断相应位置上的值是否为空,如果为空的话则通过 CAS 机制对在相应地址上进行赋值
1 | static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i, |
赋值成功则跳出当前循环,失败则继续执行循环,即当前的数组上的对应Node
已经赋过值,接下来重点看这里synchronized (f)
,f
是当前数组中的节点,这里也可以看出ConcurrentHashMap
是以数组中的每一个元素作为分段锁的,分段锁的个数即为数组的长度。if (fh >= 0)
判断数组中节点的hash
值是否大于 0,是的话表示当前的存储方式还是链表,那么会从链表中找是否有key
相同的节点,如果有则替换,没有则加入链表尾部。如果值小于 0(确切的值是 -2)的话,表示当前已经用红黑树存储了,插入TreeBin
中。接着判断binCount
的值,如果大于TREEIFY_THRESHOLD
的值即 8,那么执行treeifyBin(tab, i);
1 | private final void treeifyBin(Node<K,V>[] tab, int index) { |
先判断tab
数组的长度,如果小于MIN_TREEIFY_CAPACITY
即 64,那么不会进行红黑树的转换,而是会将数组扩容,否则会将数组index
处的链表构建成一个TreeBin
,即红黑树实例。