JDK1.8中,ConcurrentHashMap的源码竟然有6300行,作者是大名鼎鼎Doug Lea(感谢开源让我离大神可以这么近,不过代码风格明显没打算交给他人维护)。本文只讲述扩容相关的内容和get操作的并发安全实现,其他操作就放弃了。文章参考一下别人的文章,主要是以下两篇文章:
http://blog.csdn.net/u010887744/article/details/51002358
http://www.cnblogs.com/huaizuo/p/5413069.html
友情提示:看ConcurrentHashMap源码还是需要一些Java并发编程的了解,比如内存可见性、原子性、顺序性、无锁。另一点就是先阅读HashMap的源码。JDK8 HashMap数据结构
1 | static class Node<K,V> implements Map.Entry<K,V> { |
HashMap底层由数组、链表和红黑树三者组成。我们从图1和源码大改也可以推断出来。首先是HashMap使用Node组成的数组table装载所有的键值对,再者HashMap发生Hash值碰撞使用链表将它们连起来,最后是某个链表长度大于指定值链表转化为红黑树。
重要的常量
基本常量
1 | transient volatile Node<K,V>[] table; |
Unsafe相关常量
1 | // Unsafe mechanics |
重要的方法
无锁方法
1 | //内存中直接获取tab数组张第i个元素,保证内存可见性 |
扩容方法transfer
扩容过程解析
ConcurrentHashMap无锁多线程扩容,减少扩容时的时间消耗。单线程构建两倍容量的nextTable;允许多线程复制原table元素到nextTable。为每个内核均分任务,并保证其不小于16(常量MIN_TRANSFER_STRIDE)。
1)若nextTab为null,则初始化其为原table的2倍;
2)死循环遍历,直到finishing。
- 节点为空,则插入ForwardingNode;
- 链表节点(fh>=0),分别插入nextTable的i和i+n的位置;
- TreeBin节点(fh<0),判断是否需要untreefi,分别插入nextTable的i和i+n的位置;
- finishing时,nextTab赋给table,更新sizeCtl为新容量的0.75倍 ,完成扩容。
以上说的都是单线程,多线程又是如何实现的呢?遍历到ForwardingNode节点((fh = f.hash) == MOVED),说明此节点被处理过了,直接跳过。这是控制并发扩容的核心 。由于给节点上了锁,只允许当前线程完成此节点的操作,处理完毕后,将对应值设为ForwardingNode(fwd),其他线程看到forward,直接向后遍历。如此便完成了多线程的复制工作,也解决了线程安全问题。
源码解析
1 | /** |
get操作
get操作没有使用锁,怎么保证并发安全的?
线程安全基本概念可以参考这篇笔记:GYM2-Java内存模型和三个概念。
线程安全:
- 内存可见性
- 操作原子性
- 线程内代码执行顺序性
分析get在其他操作发生的线程安全保障:
- table数组使用volatile修饰,保证table数组的内存可见性。
- 线程内代码执行顺序性保证,能力有限不知道怎么分析。
- put操作将Node节点放入table[i]时(table[i]==null、table[i]是一个链表或者是一个红黑树):查找放置位置是不改变table数组,找到安放位置,使用CAS更新tab[i]保证操作原子性,没有
- remove操作与put操作类似,remove操作过程不改变table[i],改变table[i]使用CAS保证原子性。
- put操作结束时链表转换为红黑树:转化过程不会直接改变链表,而是用链表中的Node节点生成新的TreeBin节点。最后,将红黑树的根节点更新table[i],当然更新操作使用CAS以保证原子性。
- 扩容操作中table[i]移到nextTable时,这个过程使用CAS操作,get操作会在nextTable中查找。没有移到到nextTable时,不会修改table[i],不存在线程安全问题。
1 | public V get(Object key) { |