其实ConcurrentHashMap我自己已经看过很多遍了,但是今天在面试阿里的时候自己在描述ConcurrentHashMap发现自己根本讲不清楚什么是ConcurrentHashMap,以及里面是怎么实现的,搞的我突然发现自己什么都不懂,所以我想要再次的来分析一下这个源码,完全理解ConcurrentHashMap,而不是以为自己懂了,实际上自己不懂。
首先我们看一下put方法,put方法会调用到putVal方法上面。
final V putVal(K key, V value, boolean onlyIfAbsent) { if (key == null || value == null) throw new NullPointerException(); int hash = spread(key.hashCode()); //如果put进去的是个链表,这个参数表示链表的大小 int binCount = 0; for (Node<K,V>[] tab = table;;) { Node<K,V> f; int n, i, fh; if (tab == null || (n = tab.length) == 0) //初始化链表 tab = initTable(); //如果这个槽位没有数据 else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { //使用CAS将这个新的node设置到hash桶里面去 if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null))) break; // no lock when adding to empty bin } //帮助迁移 else if ((fh = f.hash) == MOVED) tab = helpTransfer(tab, f); else { //获取锁 V oldVal = null; synchronized (f) { //双重检查锁 if (tabAt(tab, i) == f) { //如果hash值大于等于0,那么代表这个节点里的数据是链表 if (fh >= 0) { binCount = 1; //每次遍历完后binCount加1,表示链表长度 for (Node<K,V> e = f;; ++binCount) { K ek; //如果hash值和key值都相同,那么覆盖,break结束循环 if (e.hash == hash && ((ek = e.key) == key || (ek != null && key.equals(ek)))) { oldVal = e.val; if (!onlyIfAbsent) e.val = value; break; } //下一个节点为null,说明遍历到尾节点了,那么直接在尾节点设值一个新的值 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).putTreeVal(hash, key, value)) != null) { oldVal = p.val; if (!onlyIfAbsent) p.val = value; } } } } if (binCount != 0) { if (binCount >= TREEIFY_THRESHOLD) //如果链表个数大于8,那么就调用这个方法 treeifyBin(tab, i); if (oldVal != null) return oldVal; break; } } } addCount(1L, binCount); return null; }解释一下上面的源码做了什么:
首先做一下判断,不允许key和value中任意一个为空,否则抛出异常
计算key的hash值,然后遍历table数组
如果table数组为null或为空,那么就调用initTable做初始化
为了保证可见性,会使用tab去table数组里获取数据,如果没有数据,那么用casTabAt通过CAS将新Node设置到table数组里。(注:这里也体现了和hashmap不一样的地方,hashmap直接通过数据拿就好了, 这个获取数据和设值都要保证可见性和线程安全性)
如果当前槽位所对应的hash值是MOVED,说明当前的table正在扩容迁移节点,那么就调用helpTransfer帮助迁移
走到这里,说明这个槽位里面的元素不止一个,有很多个,所以给头节点加上锁
如果当前的hash所对应的的槽位不是空的,并且hash值大于等于0,那么就说明这个槽位里面的对象是一个链表,那么就遍历链表
如果所遍历的链表里面有元素的hash值并且key和当前要插入的数据的是一样的,那么就覆盖原来的值
如果遍历到最后的节点都没有元素和要插入的值key是一样的,那么就新建一个Node节点,插入到链表的最后
每遍历一个节点就把binCount+1
如果当前的节点是TreeBin,那么说明该槽位里面的数据是红黑树,那么调用相应方法插入数据
最后如果binCount已经大于或等于8了,那么就调用treeifyBin
接下来我们先看initTable 方法,再看treeifyBin和helpTransfer
private final Node<K,V>[] initTable() { Node<K,V>[] tab; int sc; while ((tab = table) == null || tab.length == 0) { //一开始的时候sizeCtl为0 if ((sc = sizeCtl) < 0) Thread.yield(); // lost initialization race; just spin //将sizeCtl用CAS设置成-1 else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) { try { if ((tab = table) == null || tab.length == 0) { //因为sc一开始为0,所以n取DEFAULT_CAPACITY为16 int n = (sc > 0) ? sc : DEFAULT_CAPACITY; @SuppressWarnings("unchecked") Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n]; //将table赋值为大小为16的Node数组 table = tab = nt; //将sc的设置为总容量的75%,如果 n 为 16 的话,那么这里 sc = 12 sc = n - (n >>> 2); } } finally { //最后将sizeCtl设置为sc的值 sizeCtl = sc; } break; } } return tab; }这个方法里面初始化了一个很重要的变量sizeCtl,初始值为总容量的75%,table初始化为一个容量为16的数组