并发编程学习笔记(二十七、ConcurrentHashMap,Java8 ConcurrentHashMap)
目录:
- 属性及内部类
- 构造函数
- 核心方法
属性及内部类
1、属性:
其实说到属性最终要的也无法就是一个hash数组桶,其它核心的属性都是大同小异。
1 transient volatile Node<K,V>[] table;
2、Node:ConcurrentHashMap存储数据的基本单元。
1 static class Node<K,V> implements Map.Entry<K,V> { 2 final int hash; 3 final K key; 4 volatile V val; 5 volatile Node<K,V> next; 6 7 Node(int hash, K key, V val, Node<K,V> next) { 8 this.hash = hash; 9 this.key = key; 10 this.val = val; 11 this.next = next; 12 } 13 14 public final K getKey() { return key; } 15 public final V getValue() { return val; } 16 public final int hashCode() { return key.hashCode() ^ val.hashCode(); } 17 public final String toString(){ return key + "=" + val; } 18 /** ConcurrentHashMap不允许更新value **/ 19 public final V setValue(V value) { 20 throw new UnsupportedOperationException(); 21 } 22 23 public final boolean equals(Object o) { 24 Object k, v, u; Map.Entry<?,?> e; 25 return ((o instanceof Map.Entry) && 26 (k = (e = (Map.Entry<?,?>)o).getKey()) != null && 27 (v = e.getValue()) != null && 28 (k == key || k.equals(key)) && 29 (v == (u = val) || v.equals(u))); 30 } 31 32 /** 33 * 用于map中的get()方法,子类重写 34 */ 35 Node<K,V> find(int h, Object k) { 36 Node<K,V> e = this; 37 if (k != null) { 38 do { 39 K ek; 40 if (e.hash == h && 41 ((ek = e.key) == k || (ek != null && k.equals(ek)))) 42 return e; 43 } while ((e = e.next) != null); 44 } 45 return null; 46 } 47 }
Node非常简单,一眼就能看出其底层用链表实现,但只允许查找数据,不允许修改。
3、TreeNode:继承自Node,但内部使用的数据结构是红黑树,当链表长度大于8时就会从Node转换成TreeNode。
4、TreeBin:存储属性结构的容器,也就是TreeNode的容器,提供了一些红黑树的条件和锁的控制。
5、ForwardingNode:支持扩容操作,将已处理的节点和空节点置为ForwardingNode,并发处理时多个线程经过ForwardingNode就表示已经遍历过了。
构造函数
Java8的构造和7类似,且ConcurrentHashMap和HashMap的无参构造也同样使用的是懒加载。
1 public ConcurrentHashMap() {}
核心方法
1、put()、putVal():
1 public V put(K key, V value) { 2 return putVal(key, value, false); 3 }
1 final V putVal(K key, V value, boolean onlyIfAbsent) { 2 if (key == null || value == null) throw new NullPointerException(); 3 // 两次hash,减少hash冲突,可让其均匀分布 4 int hash = spread(key.hashCode()); 5 int binCount = 0; 6 // 遍历table 7 for (Node<K,V>[] tab = table;;) { 8 Node<K,V> f; int n, i, fh; 9 if (tab == null || (n = tab.length) == 0) 10 // 和HashMap同样的使用懒加载的方式,在用的时候再初始化 11 tab = initTable(); 12 // 如果位置i没有数据 13 else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { 14 // 使用CAS的方式插入 15 if (casTabAt(tab, i, null, 16 new Node<K,V>(hash, key, value, null))) 17 break; // no lock when adding to empty bin 18 } 19 // 如果在进行扩容,则帮助其进行扩容操作 20 else if ((fh = f.hash) == MOVED) 21 tab = helpTransfer(tab, f); 22 // 若以上条件都不满足,则就是要进行加锁操作 23 // 也就是存在hash冲突是,需要锁住链表或红黑树的头节点 24 else { 25 V oldVal = null; 26 synchronized (f) { 27 if (tabAt(tab, i) == f) { 28 // 标识该节点是链表结构 29 if (fh >= 0) { 30 binCount = 1; 31 for (Node<K,V> e = f;; ++binCount) { 32 K ek; 33 // 若是相同的key则会覆盖老的value 34 if (e.hash == hash && 35 ((ek = e.key) == key || 36 (ek != null && key.equals(ek)))) { 37 oldVal = e.val; 38 if (!onlyIfAbsent) 39 e.val = value; 40 break; 41 } 42 // 插入到链表的尾部 43 Node<K,V> pred = e; 44 if ((e = e.next) == null) { 45 pred.next = new Node<K,V>(hash, key, 46 value, null); 47 break; 48 } 49 } 50 } 51 // 若是红黑树结构 52 else if (f instanceof TreeBin) { 53 Node<K,V> p; 54 binCount = 2; 55 if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key, 56 value)) != null) { 57 oldVal = p.val; 58 if (!onlyIfAbsent) 59 p.val = value; 60 } 61 } 62 } 63 } 64 if (binCount != 0) { 65 // 若链表长度大于8时,则会转换成红黑树 66 if (binCount >= TREEIFY_THRESHOLD) 67 treeifyBin(tab, i); 68 if (oldVal != null) 69 return oldVal; 70 break; 71 } 72 } 73 } 74 // 统计size,并检查是否需要扩容 75 addCount(1L, binCount); 76 return null; 77 }
putVal的过程其实很清晰,我来简单的总结下:
- 如果还没有初始化则初始化。
- 如果没有hash冲突则直接CAS插入。
- 如果还在进行扩容则先进行扩容。
- 如果存在hash冲突,就先加锁来保证线程安全。
- 链表就遍历插入到尾端。
- 红黑树就按照红黑树的结构插入。
- 如果链表长度大于阀值8,则先转换成红黑树,在进入一次循环。
- 如果添加成功后调用addCount()方法统计size,并检查是否需要扩容。
2、initTable():初始化ConcurrentHashMap存储槽。
1 private final Node<K,V>[] initTable() { 2 Node<K,V>[] tab; int sc;、 3 while ((tab = table) == null || tab.length == 0) { 4 if ((sc = sizeCtl) < 0) 5 Thread.yield(); // lost initialization race; just spin 6 else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) { 7 try { 8 if ((tab = table) == null || tab.length == 0) { 9 int n = (sc > 0) ? sc : DEFAULT_CAPACITY; 10 @SuppressWarnings("unchecked") 11 Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n]; 12 table = tab = nt; 13 sc = n - (n >>> 2); 14 } 15 } finally { 16 sizeCtl = sc; 17 } 18 break; 19 } 20 } 21 return tab; 22 }
这段代码虽然看起来简单,但你必须要知道sizeCtl的含义后才能弄明白。
1 /** 2 * 用于控制table初始化和resize的参数。 3 * -1:表示正在发生初始化 4 * 一个很小的负数(非固定数字):标识table正在调整大小 5 * 0:代表默认状态 6 * 16 * 2 ^ n * 0.75:下一次扩容的阀值 7 */ 8 private transient volatile int sizeCtl;
说完了sizeCtl后,你就能很轻松的看懂initTable了。
首先当sizeCtl < 0,则表示table正在调整大小,所以当前线程需要让步,也就是让在调整大小的线程先去执行;
然后会CAS的把sc调整为-1,也就是通知别的线程我要去初始化啦;
CAS成功后判断tab是否有值,还是空的话便初始化为默认容量。