前面我们学习了HashMap和Hashtable,因为多线程环境下,使用Hashmap进行put操作会引起死循环,导致CPU利用率接近100%,所以在并发情况下不能使用HashMap。
而Hashtable使用synchronized来保证线程安全,但在线程竞争激烈的情况下Hashtable的效率非常低下。因为当一个线程访问Hashtable的同步方法时,其他线程访问Hashtable的同步方法时,可能会进入阻塞或轮询状态。
jdk1.7中ConcurrentHashMap使用的锁分段技术,首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。有些方法需要跨段,比如size()和containsValue(),它们可能需要锁定整个表而而不仅仅是某个段,这需要按顺序锁定所有段,操作完毕后,又按顺序释放所有段的锁。
jdk1.7的ConcurrentHashMap实现如下:
ConcurrentHashMap是由Segment数组结构和HashEntry数组结构组成。Segment是一种可重入锁ReentrantLock,在ConcurrentHashMap里扮演锁的角色,HashEntry则用于存储键值对数据。一个ConcurrentHashMap里包含一个Segment数组,Segment的结构和HashMap类似,是一种数组和链表结构, 一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素, 每个Segment守护着一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得它对应的Segment锁。ConcurrentHashMap完全允许多个读操作并发进行,读操作并不需要加锁。
jdk1.8的ConcurrentHashMap实现:
JDK1.8的实现已经摒弃了Segment的概念,而是直接用Node数组+链表+红黑树的数据结构来实现,并发控制使用Synchronized和CAS来操作,整个看起来就像是优化过且线程安全的HashMap。
首先看ConcurrentHashMap的部分源码:
public class ConcurrentHashMap<K,V> extends AbstractMap<K,V> implements ConcurrentMap<K,V>, Serializable { ..... transient volatile Node<K,V>[] table; /** * The next table to use; non-null only while resizing. */ private transient volatile Node<K,V>[] nextTable; ...... }
Node的源码:
static class Node<K,V> implements Map.Entry<K,V> { final int hash; final K key; volatile V val; volatile Node<K,V> next; Node(int hash, K key, V val, Node<K,V> next) { this.hash = hash; this.key = key; this.val = val; this.next = next; } ...... }
注意这里用的volatile,而HashMap的Node没有使用这个。TreeNode继承与Node,但是数据结构换成了二叉树结构,它是红黑树的数据的存储结构,用于红黑树中存储数据,当链表的节点数大于8时会转换成红黑树的结构。
ConcurrentHashMap的初始化其实是一个空实现,并没有做任何事,这里后面会讲到,这也是和其他的集合类有区别的地方,初始化操作并不是在构造函数实现的,而是在put操作中实现。
get方法:
1 public V get(Object key) { 2 Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek; 3 int h = spread(key.hashCode()); 4 if ((tab = table) != null && (n = tab.length) > 0 && 5 (e = tabAt(tab, (n - 1) & h)) != null) { 6 if ((eh = e.hash) == h) { 7 if ((ek = e.key) == key || (ek != null && key.equals(ek))) 8 return e.val; 9 } 10 else if (eh < 0) 11 return (p = e.find(h, key)) != null ? p.val : null; 12 while ((e = e.next) != null) { 13 if (e.hash == h && 14 ((ek = e.key) == key || (ek != null && key.equals(ek)))) 15 return e.val; 16 } 17 } 18 return null; 19 }
get方法的步骤:
- 计算hash值,定位到该table索引位置,如果是首节点符合就返回
- 如果遇到扩容的时候,会调用标志正在扩容节点ForwardingNode的find方法,查找该节点,匹配就返回
- 以上都不符合的话,就往下遍历节点,匹配就返回,否则最后就返回null
put方法:
1 public V put(K key, V value) { 2 return putVal(key, value, false); 3 } 4 5 /** Implementation for put and putIfAbsent */ 6 final V putVal(K key, V value, boolean onlyIfAbsent) { 7 if (key == null || value == null) throw new NullPointerException(); 8 int hash = spread(key.hashCode()); 9 int binCount = 0; 10 for (Node<K,V>[] tab = table;;) { 11 Node<K,V> f; int n, i, fh; 12 if (tab == null || (n = tab.length) == 0) 13 tab = initTable(); 14 else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { 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 else if ((fh = f.hash) == MOVED) 20 tab = helpTransfer(tab, f); 21 else { 22 V oldVal = null; 23 synchronized (f) { 24 if (tabAt(tab, i) == f) { 25 if (fh >= 0) { 26 binCount = 1; 27 for (Node<K,V> e = f;; ++binCount) { 28 K ek; 29 if (e.hash == hash && 30 ((ek = e.key) == key || 31 (ek != null && key.equals(ek)))) { 32 oldVal = e.val; 33 if (!onlyIfAbsent) 34 e.val = value; 35 break; 36 } 37 Node<K,V> pred = e; 38 if ((e = e.next) == null) { 39 pred.next = new Node<K,V>(hash, key, 40 value, null); 41 break; 42 } 43 } 44 } 45 else if (f instanceof TreeBin) { 46 Node<K,V> p; 47 binCount = 2; 48 if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key, 49 value)) != null) { 50 oldVal = p.val; 51 if (!onlyIfAbsent) 52 p.val = value; 53 } 54 } 55 } 56 } 57 if (binCount != 0) { 58 if (binCount >= TREEIFY_THRESHOLD) 59 treeifyBin(tab, i); 60 if (oldVal != null) 61 return oldVal; 62 break; 63 } 64 } 65 } 66 addCount(1L, binCount); 67 return null; 68 }
put方法的步骤:
- 如果没有初始化就先调用initTable()方法来进行初始化过程
- 如果没有hash冲突就直接CAS插入
- 如果还在进行扩容操作就先进行扩容如果存在hash冲突,就加锁
synchronized
来保证线程安全,这里有两种情况,一种是链表形式就直接遍历到尾端插入,一种是红黑树就按照红黑树结构插入 - 最后一个如果该链表的数量大于阈值8,就要先转换成黑红树的结构,break再一次进入循环
- 如果添加成功就调用addCount()方法统计size,并且检查是否需要扩容
JDK1.8版本的ConcurrentHashMap的数据结构已经接近HashMap,相对而言,ConcurrentHashMap只是增加了同步的操作来控制并发。