jdk1.8 ConcurrentHashMap 源码学习
上次讲到HashMap,但是HashMap并不是线程安全的,那么有哪些线程安全的Map或者是实现线程安全的map呢?
1、 HashTable(已弃用),使用的是内置对象锁对map进行同步,并发执行的效率比较低(key和value均不能为null,因为这是用在多线程的,当get返回null时,无法确定是不包含这个key还是值为null,hashMap允许key为null,因为运行在单线程,可以通过containKey来判断是否存在key,而containKey在多线程中可能刚好在你调用之前remove了当前key导致当前key为null);
2、 ConcurrentHashMap(流行使用),JDK1.7使用分段锁,JDK1.8使用的是CAS+synchronized实现并发访问(key和value均不能为null);
3、使用Collections的synchronizedMap(Map m) 进行包装,使用的是传入m的内置锁,同样并发执行效率低。
下面记录一下JDK1.8的ConcurrentHashMap的源码学习,花了挺长时间,比hashmap难多了
属性
下面只列出了一些属性
// 保存键值对总数 private transient volatile long baseCount; /* 默认为0,用来控制table的初始化和扩容操作, 小于0时代表正在扩容,并且 -n表示 * 有n – 1个线程在扩容,正数代表table容量 */ private transient volatile int sizeCtl; static final int MOVED = -1; // hash for forwarding nodes,forwarding nodes 是扩容时用到的node static final int TREEBIN = -2; // hash for roots of trees static final int RESERVED = -3; // hash for transient
构造方法
// 带容量的初始化,只是初始化了容量,并没有建立桶数组 public ConcurrentHashMap(int initialCapacity) { if (initialCapacity < 0) throw new IllegalArgumentException(); int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY : tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1)); this.sizeCtl = cap; }
这里只列出了带容量的构造方法,还有其他的没列出了。
put()方法
public V put(K key, V value) { return putVal(key, value, false); } // onlyIfAbsent 为 true的话表示若当前key-value不存在,进行插入;若存在,不对当前存在key-value进行更新 final V putVal(K key, V value, boolean onlyIfAbsent) { // key 和value不能为null if (key == null || value == null) throw new NullPointerException(); //计算key的哈希值 int hash = spread(key.hashCode()); int binCount = 0; for (Node<K,V>[] tab = table;;) { Node<K,V> f; int n, i, fh; if (tab == null || (n = tab.length) == 0) // 初始化表数组(或者叫桶数组,默认容量为16),和hashMap一样,table是延迟加载的,initTable()通过CAS机制实现同步,稍后会讲(看到这可以先到后面看看initTable()) tab = initTable(); else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { // 通过CAS机制进行并发put, /* *param1 :桶数组 *param2 :节点(node)位置偏移 *param3 :节点当前预期内存值 *param4 :要在当前节点内更新的值 */ if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null))) break; // no lock when adding to empty bin } // 常量 MOVED = -1,表示正在扩容 else if ((fh = f.hash) == MOVED) //内置锁是加在每个桶上的,扩容实际上是对每个桶上的元素重新分配桶,扩容可以在不同的桶上多线程并发执行 tab = helpTransfer(tab, f); else { V oldVal = null; // 对桶 f 进行同步put操作 synchronized (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; } } } // f < 0 else if (f instanceof TreeBin) { Node<K,V> p; // 赋值为2的意义只是为了不等于0?不太清楚,addCount(1L, binCount)看到如果在桶数组不为空时,binCount <= 1会直接返回,是这个原因? binCount = 2; // 红黑树节点,加入红黑树中 if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key, value)) != null) { oldVal = p.val; if (!onlyIfAbsent) p.val = value; } } } } if (binCount != 0) { // TREEIFY_THRESHOLD=8,树化阈值 if (binCount >= TREEIFY_THRESHOLD) treeifyBin(tab, i); if (oldVal != null) return oldVal; break; } } } // 数量+1,并进行是否扩容判断 addCount(1L, binCount); return null; }
initTable() 方法
private 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)) { //若CAS成功 把当前对象SIZECTL偏移位置修改为-1,即sizeConrol = -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; // 用减法来替代 *0.75,计算机中乘法的操作比减法耗时 sc = n - (n >>> 2); } } finally { sizeCtl = sc; } break; } } return tab; }
addCount()方法
private final void addCount(long x, int check) { // counterCells 在 currentMap 初始化和 put 过程都没有进行初始化,本人暂时也不知道这是用来干嘛的,doc注释为Table of counter cells. When non-null, size is a power of 2. CounterCell[] as; long b, s; // counterCells为null,不进入if语句 if ((as = counterCells) != null || !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) { CounterCell a; long v; int m; boolean uncontended = true; if (as == null || (m = as.length - 1) < 0 || (a = as[ThreadLocalRandom.getProbe() & m]) == null || !(uncontended = U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) { fullAddCount(x, uncontended); return; } if (check <= 1) return; s = sumCount(); } // 尝试扩容 if (check >= 0) { Node<K,V>[] tab, nt; int n, sc; while (s >= (long)(sc = sizeCtl) && (tab = table) != null && (n = tab.length) < MAXIMUM_CAPACITY) { /* resizeStamp(n){ // RESIZE_STAMP_BITS = 16 *return Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1)); *} */ int rs = resizeStamp(n); if (sc < 0) { // RESIZE_STAMP_SHIFT = 16 ,下面的if语句判断条件是jdk的bug,在网上查资料一大佬提到 oracle bug库链接:https://bugs.java.com/bugdatabase/view_bug.do?bug_id=JDK-8214427 //下列的解释参考自:https://www.jianshu.com/p/749d1b8db066 // 如果 sc 的低 16 位不等于 标识符(校验异常 sizeCtl 变化了) // 如果 sc == 标识符 + 1 (扩容结束了,不再有线程进行扩容)(默认第一个线程设置 sc ==rs 左移 16 位 + 2,当第一个线程结束扩容了,就会将 sc 减一。这个时候,sc 就等于 rs + 1) // 如果 sc == 标识符 + 65535(帮助线程数已经达到最大) // 如果 nextTable == null(结束扩容了) // 如果 transferIndex <= 0 (转移状态变化了) // 结束循环 if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 || sc == rs + MAX_RESIZERS || (nt = nextTable) == null || transferIndex <= 0) break; if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) transfer(tab, nt); // 线程加入协同扩容 } else if (U.compareAndSwapInt(this, SIZECTL, sc, (rs << RESIZE_STAMP_SHIFT) + 2)) transfer(tab, null); //扩容 s = sumCount(); } } }
transfer()方法
暂时没有认真研读,了解大概过程,这里先给参考链接,下次再仔细研读一下:
https://www.jianshu.com/p/aaf769fdbd20
总结
相比 hashMap ,concurrentHashMap复杂很多,在处理并发安全的问题上,ConcurrenthahMap用到了 CAS + syschroynized,CAS这就要求了要了解java的内存模型,计算机的底层,所以在这些上面花了一部分时间,对 synchronized现在我也还是一知半解,要去啃啃源码,下次博客就要记录synchronized 和 lock、红黑树、线程池原理等等许多java知识,任重而道远。。。
参考链接
https://www.jianshu.com/p/c0642afe03e0
https://www.jianshu.com/p/aaf769fdbd20