1、前提
在阅读这篇博客之前,希望你对HashMap已经是有所理解的,否则可以参考这篇博客: jdk1.8源码分析-hashMap;另外你对java的cas操作也是有一定了解的,因为在这个类中大量使用到了cas相关的操作来保证线程安全的。
2、概述
ConcurrentHashMap这个类在java.lang.current包中,这个包中的类都是线程安全的。ConcurrentHashMap底层存储数据的结构与1.8的HashMap是一样的,都是数组+链表(或红黑树)的结构。在日常的开发中,我们最长用到的键值对存储结构的是HashMap,但是我们知道,这个类是非线程安全的,在高并发的场景下,在进行put操作的时候有可能进入死循环从而使服务器的cpu使用率达到100%;sun公司因此也给出了与之对应的线程安全的类。在jdk1.5以前,使用的是HashTable,这个类为了保证线程安全,在每个类中都添加了synchronized关键字,而想而知在高并发的情景下相率是非常低下的。为了解决HashTable效率低下的问题,官网在jdk1.5后推出了ConcurrentHashMap来替代饱受诟病的HashTable。jdk1.5后ConcurrentHashMap使用了分段锁的技术。在整个数组中被分为多个segment,每次get,put,remove操作时就锁住目标元素所在的segment中,因此segment与segment之前是可以并发操作的,上述就是jdk1.5后实现线程安全的大致思想。但是,从描述中可以看出一个问题,就是如果出现比较机端的情况,所有的数据都集中在一个segment中的话,在并发的情况下相当于锁住了全表,这种情况下其实是和HashTable的效率出不多的,但总体来说相较于HashTable,效率还是有了很大的提升。jdk1.8后,ConcurrentHashMap摒弃了segment的思想,转而使用cas+synchronized组合的方式来实现并发下的线程安全的,这种实现方式比1.5的效率又有了比较大的提升。那么,它是如何整体提升效率的呢?见下文分析吧!
3、重要成员变量
1、ziseCtr:在多个方法中出现过这个变量,该变量主要是用来控制数组的初始化和扩容的,默认值为0,可以概括一下4种状态:
a、sizeCtr=0:默认值;
b、sizeCtr=-1:表示Map正在初始化中;
c、sizeCtr=-N:表示正在有N-1个线程进行扩容操作;
d、sizeCtr>0: 未初始化则表示初始化Map的大小,已初始化则表示下次进行扩容操作的阈值;
2、table:用于存储链表或红黑数的数组,初始值为null,在第一次进行put操作的时候进行初始化,默认值为16;
3、nextTable:在扩容时新生成的数组,其大小为当前table的2倍,用于存放table转移过来的值;
4、Node:该类存储数据的核心,以key-value形式来存储;
5、ForwardingNode:这是一个特殊Node节点,仅在进行扩容时用作占位符,表示当前位置已被移动或者为null,该node节点的hash值为-1;
4、put操作
先把源码摆上来:
/** Implementation for put and putIfAbsent */ final V putVal(K key, V value, boolean onlyIfAbsent) { //key和value不能为空 if (key == null || value == null) throw new NullPointerException(); //通过key来计算获得hash值 int hash = spread(key.hashCode()); //用于计算数组位置上存放的node的节点数量 //在put完成后会对这个参数判断是否需要转换成红黑树或链表 int binCount = 0; //使用自旋的方式放入数据 //这个过程是非阻塞的,放入失败会一直循环尝试,直至成功 for (Node<K,V>[] tab = table;;) { Node<K,V> f; int n, i, fh; //第一次put操作,对数组进行初始化,实现懒加载 if (tab == null || (n = tab.length) == 0) //初始化 tab = initTable(); //数组已初始化完成后 //使用cas来获取插入元素所在的数组的下标的位置,该位置为空的话就直接放进去 else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null))) break; // no lock when adding to empty bin } //hash=-1,表明该位置正在进行扩容操作,让当前线程也帮助该位置上的扩容,并发扩容提高扩容的速度 else if ((fh = f.hash) == MOVED) //帮助扩容 tab = helpTransfer(tab, f); //插入到该位置已有数据的节点上,即用hash冲突 //在这里为保证线程安全,会对当前数组位置上的第一个节点进行加锁,因此其他位置上 //仍然可以进行插入,这里就是jdk1.8相较于之前版本使用segment作为锁性能要高效的地方 else { V oldVal = null; synchronized (f) { //再一次判断f节点是否为第一个节点,防止其他线程已修改f节点 if (tabAt(tab, i) == f) { //为链表 if (fh >= 0) { binCount = 1; //将节点放入链表中 for (Node<K,V> e = f;; ++binCount) { K ek; if (e.hash == hash && ((ek = e.key) == key || (ek != null && key.equals(ek)))) { oldVal = e.val; if (!onlyIfAbsent) e.val = value; break; } Node<K,V> pred = e; if ((e = e.next) == null) { pred.next = new Node<K,V>(hash, key, value, null); break; } } } //为红黑树 else if (f instanceof TreeBin) { Node<K,V> p; binCount = 2; //将节点插入红黑树中 if ((p = ((TreeBin<K,V>)f).putTreeva l(hash, key, value)) != null) { oldVal = p.val; if (!onlyIfAbsent) p.val = value; } } } } //插入成功后判断插入数据所在位置上的节点数量, //如果数量达到了转化红黑树的阈值,则进行转换 if