ConcurrentHashMap-深入理解(优化篇)
jdk1.8主要改动及使用原因
为何用ConcurrentHashMap
在并发编程中使用HashMap可能会导致死循环,而使用线程安全的HashTable效率又低下。
在多线程环境下,使用HashMap进行put操作会引起死循环,导致CPU利用率接近100%,所以在并发情况下不能使用HashMap。
参照对象为jdk1.7的ConcurrentHashMap,当然,熟悉jdk1.8的HashMap能够更好地理解一些改动,HashMap和ConcurrentHashMap本来就有很多共通的东西。
1、jdk1.8的ConcurrentHashMap不再使用Segment代理Map操作这种设计,整体结构变为HashMap这种结构,但是依旧保留分段锁的思想。之前版本是每个Segment都持有一把锁,1.8版本改为锁住恰好装在一个hash桶本身位置上的节点,也就是hash桶的第一个节点 tabAt(table, i),后面直接叫第一个节点。它可能是Node链表的头结点、保留节点ReservationNode、或者是TreeBin节点(TreeBin节点持有红黑树的根节点)。还有,1.8的节点变成了4种,这个后面细说,是个重要的知识。
2、可以多线程并发来完成扩容这个耗时耗力的操作。在之前的版本中如果Segment正在进行扩容操作,其他写线程都会被阻塞,jdk1.8改为一个写线程触发了扩容操作,其他写线程进行写入操作时,可以帮助它来完成扩容这个耗时的操作。多线程并发扩容这部分后面细说。
3、因为多线程并发扩容的存在,导致的其他操作的实现上会有比较大的改动,常见的get/put/remove/replace/clear,以及迭代操作,都要考虑并发扩容的影响。
4、使用新的计数方法。不使用Segment时,如果直接使用一个volatile类变量计数,因为每次读写volatile变量的开销很大,高并发时效率不如之前版本的使用Segment时的计数方式。jdk1.8新增了一个用与高并发情况的计数工具类java.util.concurrent.atomic.LongAdder,此类是基本思想和1.7及以前的ConcurrentHashMap一样,使用了一层中间类,叫做Cell(类似Segment这个类)的计数单元,来实现分段计数,最后合并统计一次。因为不同的计数单元可以承担不同的线程的计数要求,减少了线程之间的竞争,在1.8的ConcurrentHashMap基本结果改变时,继续保持和分段计数一样的并发计数效率。
5、同1.8版本的HashMap,当一个hash桶中的hash冲突节点太多时,把链表变为红黑树,提高冲突时的查找效率。