关于ConcurentHashMap的几个问题
关于concurrentHashMap 的几个问题,基于jdk 1.8 的几个问题。
1:concurrentHashMap的数据结构是怎样的?
和HashMap 一样,concurrentHashMap的数据结构是 数组+链表,可以从它的成员变量中可以看出这样的数据结构
transient volatile Node<K,V>[] table;
内部维护的是一个 Node[] 的数组结构,其中Node又是一个单链表的结构,Node的数据结构如下所示:
其中next指向下一个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; } …… }
从以上的成员变量中可以知道,concurrentHashMap内部维护的数据+链表的结构来保存数据的。
2:concurrentHashMap的put操作是怎么进行的,怎么解决hash冲突的?
这里要搞明白一个概念: 所谓的hash冲突指的是key的 hash 值取模后的值相同,导致封装这个key value的Node 要放在Node数据的同一个索引上:
// 计算key的hash值 int hash = spread(key.hashCode()); // 相应hash值的数组索引上的节点数据 f = tabAt(tab, i = (n - 1) & hash) // 如果是同一个索引上,则添加到链表的最后 pred.next = new Node<K,V>(hash, key,value, null);
以上代码显示,使用了拉链法来解决hash冲突。
关于put的操作:
分为两种情况:
1:put 元素的key 取模后的索引上的内容为空,则通过cas 的方法,将该Node添加到数组的索引中,这个操作过程不需要加锁,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 }
2:需要put到索引的位置不为空,通过sychronized对该索引上的链表对象加锁
获取锁对象后的操作:1:如果链表中存在该节点的key,则进行覆盖 2:如果不存在,则使用拉链法将节点添加到链表的表尾 3:如果链表的长度大于等于8,将链表转为红黑树
具体代码如下:
// 链表中存在这个key if (e.hash == hash && ((ek = e.key) == key || (ek != null && key.equals(ek)))) { oldVal = e.val; if (!onlyIfAbsent) e.val = value; break; }
// 链表中不存在这个key 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; } } // 如果 binCount>=8 则将链表转化为红黑树数据结构 if (binCount != 0) { if (binCount >= TREEIFY_THRESHOLD) treeifyBin(tab, i); if (oldVal != null) return oldVal; break; }
以上就是put方法的实现代码。
3:concurrentHashMap的扩容机制是怎样的?
在concurrentHashMap中维护了两个成员变量用来实现扩容:
// 扩容因子 private transient volatile int sizeCtl; // 用于保存扩容后的Node数组 private transient volatile Node<K,V>[] nextTable;
当concurrentHashMap中的元素个数大于等于 sizeCtl 这个时则进入扩容的逻辑
// 新建一个长度为原先2倍的node数组 Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1]; // 将新建的node数组指向 nextTab nextTab = nt; // 保存到用于 扩容的成员变量中 nextTable = nextTab; // 保存需要迁移的tab长度
接下来是通过遍历的方式来将老数组上的节点元素迁移到新的扩容后的数组上。
// 需要迁移的node数组为空,则在该索引上赋予一个fwd对象 else if ((f = tabAt(tab, i)) == null) advance = casTabAt(tab, i, null, fwd); // 表明已经迁移了 else if ((fh = f.hash) == MOVED) advance = true; // already processed
如果元素不为空的情况下,则进入迁移的逻辑
// 将旧元素添加到新的node数组中 setTabAt(nextTab, i, ln); // 将旧元素添加到新的node数组中 setTabAt(nextTab, i + n, hn); // 将旧表中已经迁移的数组索引赋值 fwd 表明已经迁移了 setTabAt(tab, i, fwd);
当迁移结束后,将新的node 数组指向concurrentHashMap的成员变量table;
//当finishing为true表明迁移结束 if (finishing) { nextTable = null; // 将新的node数组指向成员变量table table = nextTab; sizeCtl = (n << 1) - (n >>> 1); return; }
试想一种情况的发生:当在扩容进行时进行put操作会怎样?
这个时候进行put操作,获取到的table数组是老的node数组:因为还没有迁移完成
table成员变量通过volatile 来保证线程之间可见性
1:当put操作的对应索引上的元素没有迁移,则在老的node数组上新增元素,这是新增 和 迁移用的都是 该索引上的链表对象,不会引发多线程问题
2:当需要put的元素对应的链表已经迁移,则该线程加入迁移的工作中。
// 已经标志位迁移了 else if ((fh = f.hash) == MOVED) tab = helpTransfer(tab, f);
3:当迁移完成之后,再在新的数据链表中进行put操作
// 遍历,可以保证扩容结束后再进行put操作 for (Node<K,V>[] tab = table;;)
以上就是 concurrentHashMap 扩容的原理
4:concurrentHashMap是如何保证线程安全的,jdk1.7和jdk1.8中的实现有什么不同?
jdk 1.7 中是通过分段锁来保证线程安全的,其实put操作中很多情况下都是往一个空的数组中添加元素的,不需要做加锁操作。
jdk 1.8 是通过cas和synchronized 的方式来实现的,当进行put时,对应数组中的链表为空则进行cas原子操作即可,如果有元素再进行对该段链表进行加锁来保证线程安全。
5:当一个线程进行扩容时,另外一个线程进行put操作会怎样?
这个问题同问题3一样的,当另外一个线程进行put时,如果扩容的部分和put操作的部分对应的都是同一个node链表,那边只有一个线程获取到锁,put线程先获取到则先进行put然后再扩容,扩容线程先获取到锁,则先进行扩容,扩容线程释放锁之后,put线程获取到锁,然后会则会进行帮助扩容的操作,等扩容完成之后,再在新的链表中进行put操作