本文的分析的源码是JDK8的版本,与JDK6的版本有很大的差异。实现线程安全的思想也已经完全变了,它摒弃了Segment(锁段)的概念,而是启用了一种全新的方式实现,利用CAS算法。它沿用了与它同时期的HashMap版本的思想,底层依然由“数组”+链表+红黑树的方式思想,但是为了做到并发,又增加了很多辅助的类,例如TreeBin,Traverser等对象内部类。
与同是线程安全的老大哥HashTable相比,它已经更胜一筹,因此它的锁更加细化,而不是像HashTable一样为几乎每个方法都添加了synchronized锁,这样的锁无疑会影响到性能。
一、基本参数与构造函数
1 | public class ConcurrentHashMap<K,V> extends AbstractMap<K,V> |
最开始的Constants定义了一些类变量,意思基本与HashMap中的一样,就不具体看了。
1、内部类
1.1、Node类
1 | /* ---------------- Nodes -------------- */ |
Node类是最核心的类,用于存储键值对,和HashMap中的定义也很相似,但是有几点不同:
- 对value和next属性设置了volatile同步锁。
- 不允许setValue方法直接改变value属性。
- 增加find方法来辅助map.get方法。
1.2、TreeNode类
1 | static final class TreeNode<K,V> extends Node<K,V> { |
树节点类,另外一个核心的数据结构。当链表长度过长的时候,会转换为TreeNode。但是与HashMap不相同的是,它并不是直接转换为红黑树,而是把这些结点包装成TreeNode放在TreeBin对象中,由TreeBin完成对红黑树的包装。而且TreeNode在ConcurrentHashMap继承自Node类,而并非HashMap中的集成自LinkedHashMap.Entry<K,V>类,也就是说TreeNode带有next指针,这样做的目的是方便基于TreeBin的访问。
1.3、TreeBin类
这个类并不负责包装用户的key、value信息,而是包装的很多TreeNode节点。它代替了TreeNode的根节点,也就是说在实际的ConcurrentHashMap“数组”中,存放的是TreeBin对象,而不是TreeNode对象,这是与HashMap的区别。另外这个类还带有了读写锁。
这里仅贴出它的构造方法。可以看到在构造TreeBin节点时,仅仅指定了它的hash值为TREEBIN常量,这也就是个标识为。同时也看到我们熟悉的红黑树构造方法
1 | TreeBin(TreeNode<K,V> b) { |
1.4、ForwardingNode
一个用于连接两个table的节点类。它包含一个nextTable指针,用于指向下一张表。而且这个节点的key value next指针全部为null,它的hash值为-1. 这里面定义的find的方法是从nextTable里进行查询节点,而不是以自身为头节点进行查找
1 | static final class ForwardingNode<K,V> extends Node<K,V> { |
2、重要属性
1 | /* ---------------- Fields -------------- */ |
可以看出,基本所有的变量都被设置成volatile属性,
2.1、baseCount
用来记录map中的元素个数,但是返回的时候不会直接返回它,而是一个估计值,下面会详细介绍。
2.2、sizeCtl
sizeCtl是ConcurrentHashMap中出镜率很高的一个属性,是一个控制标识符,在不同的地方有不同的用途,不同的取值代表了不同的含义:
- 负数代表正在进行初始化或扩容操作
- -1代表正在初始化
- -N 表示有N-1个线程正在进行扩容操作
- 正数或0代表hash表还没有被初始化,这个数值表示初始化或下一次进行扩容的大小,这一点类似于扩容阈值的概念。还后面可以看到,它的值始终是当前ConcurrentHashMap容量的0.75倍,这与loadfactor是对应的。
3、构造函数
1 | /* ---------------- Public operations -------------- */ |
与HashMap一样,前面几个都是指定了属性,并不会初始化数组。
4、三个核心方法
1 | /* ---------------- Table element access -------------- */ |
ConcurrentHashMap定义了三个原子操作,用于对指定位置的节点进行操作。正是这些原子操作保证了ConcurrentHashMap的线程安全。
4.1、tabAt
获得在i位置上的Node节点
4.2、casTabAt
利用CAS算法设置i位置上的Node节点。之所以能实现并发是因为他指定了原来这个节点的值是多少
在CAS算法中,会比较内存中的值与你指定的这个值是否相等,如果相等才接受你的修改,否则拒绝你的修改
因此当前线程中的值并不是最新的值,这种修改可能会覆盖掉其他线程的修改结果,后面会详细分析
4.3、setTabAt
利用volatile方法设置节点位置的值
二、Table数组的初始化与扩容
关于table数组初始化与扩容的关键方法
1、初始化方法initTable
对于ConcurrentHashMap来说,调用它的构造方法仅仅是设置了一些参数而已。而整个table的初始化是在向ConcurrentHashMap中插入元素的时候发生的。如调用put、computeIfAbsent、compute、merge等方法的时候,调用时机是检查table==null。
1 |
|
初始化方法主要应用了关键属性sizeCtl 如果这个值〈0,表示其他线程正在进行初始化,就放弃这个操作。在这也可以看出ConcurrentHashMap的初始化只能由一个线程完成。如果获得了初始化权限,就用CAS方法将sizeCtl置为-1,防止其他线程进入。初始化数组后,将sizeCtl的值改为0.75*n
2、扩容方法 transfer
1 | private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) { |
整个扩容操作分为两个大部分
- 第一部分是构建一个nextTable,它的容量是原来的两倍,这个操作是单线程完成的。
- 二个部分就是将原来table中的元素复制到nextTable中,这里允许多线程进行操作。
具体的流程为:
为每个内核均分任务,并保证其不小于16
若nextTab为null,则初始化其为原table的2倍;
死循环遍历,直到finishing。
- 计算i的值,从后向前遍历数组完成复制,头结点f = tabAt(tab, i)
- 节点为空,则插入ForwardingNode,表明该位置已经处理过了
- 结点为ForwardingNode,说明已经处理过了,直接跳过
- 否则说明该链表未被处理,则对f结点上锁,处理后插入插入ForwardingNode结点表明已经处理过了
- f是链表节点(fh>=0),将原链表重新hash拆分为两部分,分别插入nextTable的i和i+n的位置
- f是TreeBin节点(fh<0),也拆分为两部分,最后要判断如果不再需要tree结构则反转会链表
- finishing时,nextTab赋给table,更新sizeCtl为新容量的0.75倍 ,完成扩容。
3、协助扩容方法helpTransfer
1 | final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) { |
这是一个协助扩容的方法。这个方法被调用的时候,当前ConcurrentHashMap一定已经有了nextTable对象,首先拿到这个nextTable对象,调用transfer方法。回看上面的transfer方法可以看到,当本线程进入扩容方法的时候会直接进入复制阶段。
三、put
1、put方法
1 | static final int HASH_BITS = 0x7fffffff; // usable bits of normal node hash |
put操作的流程:
判空: ConcurrentHashMap与HashMap不同的一点就是不允许key或value为null,如果为null则直接抛出异常
hash:计算hash值。h=key.hashcode,调用spread计算hash=(h ^(h >>>16))& HASH_BITS
遍历table:
若table为null,则调用initTable初始化table数组
根据hash值计算存放位置,即table的下标i=(n - 1) & hash
对该位置的结点f = tabAt(tab, i = (n - 1) & hash))进行判断:
如果是null, 调用casTabAt方法无锁插入
如果是ForwardingNode结点,说明正在扩容,调用helpTransfer方法帮助一起复制
剩下的情况就是非空且不是ForwardingNode节点,即碰撞了,将头节点上锁(保证了线程安全)
区分链表节点和树节点,分别插入
addCount
扩容与协助扩容方法上面已经分析过,下面看一下addCount方法。
2、addCount方法
1 | /* |
可以看出该方法大体分两步:
- 用CAS方法给baseCount加上指定的x,如果失败了,就执行fullAddCount方法。
- 如果指定的check小于0,你们在baseCount加上x后就要进行扩容检测,如果需要扩容,则调用transfer方法进行扩容。
四、其他方法
1、size与mappingCount方法
对于ConcurrentHashMap来说,这个table里到底装了多少东西其实是个不确定的数量,因为不可能在调用size()方法的时候像GC的“stop the world”一样让其他线程都停下来让你去统计,因此只能说这个数量是个估计值。对于这个估计值,ConcurrentHashMap也是大费周章才计算出来的。
2.1、size方法
1 | public final int size() { return map.size(); } |
旧版的方法。可以看出,size方法并没有直接返回,而是调用了sumCount方法。
2.1、mappingCount
mappingCount与size方法的类似 从下面的注释可以看出,应该使用mappingCount代替size方法,并且返回值至少一个估计值。如果sumCount时有线程插入或删除,实际数量是和mappingCount不同的
1 | /** |
与size方法一样,都没有直接返回basecont值,而是调用了下面的sumCount方法
2.3、sumCount方法
接下来就看一下这个sumCount方法
1 | static final class CounterCell { .misc.Contended |
2、get与contains方法
1 | public V get(Object key) { |
五、参考地址
http://blog.csdn.net/u010723709/article/details/48007881
https://yq.aliyun.com/articles/36781
http://www.cnblogs.com/huaizuo/archive/2016/04/20/5413069.html