2. 题目剖析ConcurrentHashMap的底层原理是什么?
ConcurrentHashMap中涉及到了哪些数据结构?
JDK 8中的ConcurrentHashMap有哪些重要属性?
ConcurrentHashMap中sizeCtl是什么意思?
ConcurrentHashMap的最大容量、默认容量、负载因子是多少?
.......
壹哥在前面3篇文章中,给大家介绍了ConcurrentHashMap的通用功能、特点,以及JDK 7、8中ConcurrentHashMap的底层数据结构等内容,文章链接如下:
高薪程序员&面试题精讲系列45之你熟悉ConcurrentHashMap吗?
高薪程序员&面试题精讲系列46之说说JDK7中ConcurrentHashMap的底层原理,有哪些数据结构
高薪程序员&面试题精讲系列47之说说JDK8中ConcurrentHashMap的底层原理,HashMap与ConcurrentHashMap有什么区别?
从本文开始,壹哥给大家重点分析JDK 8中ConcurrentHashMap的源码,请各位童鞋拿出笔记做好记录哦!
二. ConcurrentHashMap源码核心属性及构造方法解读(重点)
接下来壹哥会分析JDK 8中的ConcurrentHashMap源码,带大家掌握ConcurrentHashMap的底层实现原理,我们一起来看看吧。
1. Node[] table分析
上面壹哥已经分析过,ConcurrentHashMap的底层数据结构是 Node数组+链表+红黑树,这种结构与HashMap是一样的,所以ConcurrentHashMap的主体也是利用一个Node数组来保存数据的。这个用来保存数据的数组就是 Node[] table,它是一个 哈希桶数组,初始容量大小默认是16,默认的 负载因子是0.75,最大的容量是 2的30次方,这些特征与HashMap都一样。
transient volatile Node[] table;
注意:
虽然ConcurrentHashMap中table数组的功能及特征与HashMap中的table数组一样,两者在定义时却是有区别的!
ConcurrentHashMap中的table数组利用了 volatile关键词 修饰,而HashMap中并没有!我们知道volatile可以实现成员变量在线程之间的共享,所以从定义开始,ConcurrentHashMap就为多线程操作做了准备。
而且ConcurrentHashMap中的table数组初始化,采用的是延迟初始化策略。也就是说只有在第一次进行put操作时才会对数组进行初始化,而HashMap则是在构建对象时立即就进行了初始化。
2. Node[] nextTable
nextTable数组用于哈希表扩容,扩容完成后会被重置为 null,它也是Node类型。
private transient volatile Node[] nextTable;
3. 几个核心常量
接下来我们看看ConcurrentHashMap中的几个核心常量,希望各位可以记住这几个关键数字。
private static final int MAXIMUM_CAPACITY = 1 << 30;
private static final int DEFAULT_CAPACITY = 16;
static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
private static final int DEFAULT_CONCURRENCY_LEVEL = 16;
private static final float LOAD_FACTOR = 0.75f;
static final int TREEIFY_THRESHOLD = 8;
static final int UNTREEIFY_THRESHOLD = 6;
static final int MIN_TREEIFY_CAPACITY = 64;
//最小转移步长
private static final int MIN_TRANSFER_STRIDE = 16;
4. 节点个数总和baseCount
该属性用于存储整个哈希表中所有结点的个数总和,有点类似于 HashMap 的 size 属性。
private transient volatile long baseCount;
5. 初始化与扩容控制标识位sizeCtl
hash表初始化或扩容时的控制标识位变量。
private transient volatile int sizeCtl;
该属性有以下几种取值:
0: 表示hash表还没被初始化,这个数值表示初始化或下次进行扩容的大小;
-1: 代表哈希表正在进行初始化;
大于0: 相当于 HashMap 中的 threshold,表示阈值;
小于-1: 代表有多个线程正在进行扩容。
6. 控制单线程扩容的变量
以下两个属性是用来控制单线程进入扩容时的变量。
private static int RESIZE_STAMP_BITS = 16;
private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;
7. 节点状态常量
以下两个变量分别用于表示不同状态的节点。
//hash值是-1,表示这是一个forwardNode节点
static final int MOVED = -1;
//hash值是-2,表示这时一个TreeBin节点
static final int TREEBIN = -2;
8. Node类
Node是ConcurrentHashMap中定义的静态内部类,本质是就是一个映射(键值对),主要包括 hash、key、value 和 next 4个属性。当我们调用HashMap的put()方法,向其中添加键值对时,这个key-value就会被转换成 Node 类型。
Node类源码如下:
static class Node implements Map.Entry {
//用来定位数组索引的位置
final int hash;
final K key;
volatile V val;
//链表的下一个node
volatile Node next;
Node(int hash, K key, V value, Node next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
......
}
Node是最核心的内部类,它包装了key-value键值对,所有插入ConcurrentHashMap的数据都包装在这里面。它与HashMap中的定义很相似,但也有一些差别。它对value和next属性设置了volatile同步锁(与JDK 7的Segment相同),但是不允许调用setValue方法直接改变Node的value域,又增加了find()方法来辅助map.get()方法的实现。
跟 HashMap 一样, key字段被 final 修饰,说明在生命周期内,key 是不可变的;val 字段被 volatile 修饰,这保证了 val 字段的可见性。
9. TreeNode类
TreeNode节点类是另外一个核心的数据结构。当HashMap位桶内的链表数量达到 8 时,就会将链表转换成红黑树。但是与HashMap不相同的是,它并不是直接转换为红黑树,而是会先把这些节点包装成TreeNode放在TreeBin对象中,然后再由TreeBin来完成对红黑树的包装。而且TreeNode继承自Node类,并非像HashMap中那样继承自linkedHashMap.Entry类。也就是说TreeNode中带有next指针,这样做的目的是方便基于TreeBin的访问。TreeNode源码如下:
static final class TreeNode extends Node {
TreeNode parent; // red-black tree links
TreeNode left;
TreeNode right;
TreeNode prev; // needed to unlink next upon deletion
boolean red;
TreeNode(int hash, K key, V val, Node next,
TreeNode parent) {
super(hash, key, val, next);
this.parent = parent;
}
Node find(int h, Object k) {
return findTreeNode(h, k, null);
}
......
}
10. TreeBin类
这个类并不负责包装用户的key、value信息,而是包装了很多TreeNode节点。它代替了TreeNode的根节点,也就是说在实际的ConcurrentHashMap “数组”中,存放的是TreeBin对象,而不是TreeNode对象,这是与HashMap的显著区别,另外这个类还带有了读写锁。TreeBin源码如下:
static final class TreeBin extends Node {
TreeNode root;
volatile TreeNode first;
volatile Thread waiter;
volatile int lockState;
// values for lockState
static final int WRITER = 1; // set while holding write lock
static final int WAITER = 2; // set when waiting for write lock
static final int READER = 4;
...
11. ForwardingNode类
ForwardingNode是用于连接两个table的节点类,它包含了一个nextTable指针,用于指向下一张表。而且这个节点的key、value、next属性全部为null,它的hash值为-1。这里面定义的find()方法是从nextTable里进行节点查询,而不是以自身为头节点进行查找。
static final class ForwardingNode extends Node {
final Node[] nextTable;
ForwardingNode(Node[] tab) {
super(MOVED, null, null, null);
this.nextTable = tab;
}
......
}
12. 构造方法
因为构造函数是公开的API,所以必须和JDK 7中保持一致,但其中部分含义可能发生了一些变化,
我们来看一下参数最全的一个构造方法。
public ConcurrentHashMap(int initialCapacity,float loadFactor, int concurrencyLevel) {
if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0)
throw new IllegalArgumentException();
if (initialCapacity < concurrencyLevel)
initialCapacity = concurrencyLevel;
long size = (long)(1.0 + (long)initialCapacity / loadFactor);
int cap = (size >= (long)MAXIMUM_CAPACITY) ?
MAXIMUM_CAPACITY : tableSizeFor((int)size);
this.sizeCtl = cap;
}
上述方法中的3个核心参数分别是初始容量initialCapacity、负载因子loadFactor和并行等级concurrenyLevel,这3个核心参数的作用及含义如下:
首先,loadFactor负载因子在JDK 8的ConcurrentHashMap运行时,已经固定为了0.75f,因此这里的参数只能在创建时用来帮助确定初始的数组容量。
由于不再使用JDK 7中的Segement实现方式,因此concurrencyLevel不再用来确定Segement的数量。对于JDK 8中的ConcurrentHashMap而言,锁的粒度是对应数组中的每个位桶(理论上可以对每个桶进行并发操作),因此concurrencyLevel的含义也就是用来确定底层数据的初始容量。
这也正是size = (long)(1.0 + (long)initialCapacity / loadFactor);这行代码的意义(这里的initialCapacity是取参数中initialCapacity和concurrenyLevel中的最大值)。
另外需要注意的是,size并不是我们数组的最终容量,ConcurrentHashMap会通过tableSizeFor()方法找出 >=size 的 最小的2的n次方 作为容量。这和HashMap是一样的,需要保证容量为2的n次方,因为之后的散列操作都是基于这一前提。
最后,在得出了初始容量后,ConcurrentHashMap仅是将容量通过sizeCtl来保存,而并没有直接初始化数组,数组的初始化会被延迟到第一次put数据时进行(这样设计可能是出于节省内存的目的)。
13. Unsafe与CAS
在ConcurrentHashMap的源码中,随处可以看到U这个变量, 内部也有大量的U.compareAndSwapXXX的方法。这种方法一般是利用CAS算法实现无锁化修改值的操作,可以大大降低锁代理的性能消耗。这种算法的基本思想就是不断地去比较当前内存中的变量值与我们指定的一个变量值是否相等,如果相等,则接受我们指定修改的值,否则拒绝我们的操作。因为当前线程中的值已经不是最新的值,我们的修改很可能会覆盖掉其他线程修改的结果。这一点与乐观锁,SVN的思想是比较类似的。
以上就是 壹哥 对ConcurrentHashMap中的几个重要属性进行的简单解读,接下来我对其中几个重要的方法进行解读。
三. 结语
本篇文章,壹哥 讲解了JDK 8版本ConcurrentHashMap源码中的核心属性、内部类及构造方法,了解完这些之后,我们就可以为接下来其他源码的阶段做好铺垫。接下来 壹哥 会再通过另一篇文章,给大家讲解 JDK 8中的ConcurrentHashMap#put()方法的源码,请做好准备哦。
nextTable数组用于哈希表扩容,扩容完成后会被重置为 null,它也是Node类型。
private transient volatile Node[] nextTable;
3. 几个核心常量
接下来我们看看ConcurrentHashMap中的几个核心常量,希望各位可以记住这几个关键数字。
private static final int MAXIMUM_CAPACITY = 1 << 30; private static final int DEFAULT_CAPACITY = 16; static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; private static final int DEFAULT_CONCURRENCY_LEVEL = 16; private static final float LOAD_FACTOR = 0.75f; static final int TREEIFY_THRESHOLD = 8; static final int UNTREEIFY_THRESHOLD = 6; static final int MIN_TREEIFY_CAPACITY = 64; //最小转移步长 private static final int MIN_TRANSFER_STRIDE = 16;
4. 节点个数总和baseCount
该属性用于存储整个哈希表中所有结点的个数总和,有点类似于 HashMap 的 size 属性。
private transient volatile long baseCount;
5. 初始化与扩容控制标识位sizeCtl
hash表初始化或扩容时的控制标识位变量。
private transient volatile int sizeCtl;
该属性有以下几种取值:
0: 表示hash表还没被初始化,这个数值表示初始化或下次进行扩容的大小;
-1: 代表哈希表正在进行初始化;
大于0: 相当于 HashMap 中的 threshold,表示阈值;
小于-1: 代表有多个线程正在进行扩容。
6. 控制单线程扩容的变量
以下两个属性是用来控制单线程进入扩容时的变量。
private static int RESIZE_STAMP_BITS = 16; private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;
7. 节点状态常量
以下两个变量分别用于表示不同状态的节点。
//hash值是-1,表示这是一个forwardNode节点 static final int MOVED = -1; //hash值是-2,表示这时一个TreeBin节点 static final int TREEBIN = -2;
8. Node类
Node是ConcurrentHashMap中定义的静态内部类,本质是就是一个映射(键值对),主要包括 hash、key、value 和 next 4个属性。当我们调用HashMap的put()方法,向其中添加键值对时,这个key-value就会被转换成 Node 类型。
Node类源码如下:
static class Nodeimplements Map.Entry { //用来定位数组索引的位置 final int hash; final K key; volatile V val; //链表的下一个node volatile Node next; Node(int hash, K key, V value, Node next) { this.hash = hash; this.key = key; this.value = value; this.next = next; } ...... }
Node是最核心的内部类,它包装了key-value键值对,所有插入ConcurrentHashMap的数据都包装在这里面。它与HashMap中的定义很相似,但也有一些差别。它对value和next属性设置了volatile同步锁(与JDK 7的Segment相同),但是不允许调用setValue方法直接改变Node的value域,又增加了find()方法来辅助map.get()方法的实现。
跟 HashMap 一样, key字段被 final 修饰,说明在生命周期内,key 是不可变的;val 字段被 volatile 修饰,这保证了 val 字段的可见性。
9. TreeNode类
TreeNode节点类是另外一个核心的数据结构。当HashMap位桶内的链表数量达到 8 时,就会将链表转换成红黑树。但是与HashMap不相同的是,它并不是直接转换为红黑树,而是会先把这些节点包装成TreeNode放在TreeBin对象中,然后再由TreeBin来完成对红黑树的包装。而且TreeNode继承自Node类,并非像HashMap中那样继承自linkedHashMap.Entry
static final class TreeNodeextends Node { TreeNode parent; // red-black tree links TreeNode left; TreeNode right; TreeNode prev; // needed to unlink next upon deletion boolean red; TreeNode(int hash, K key, V val, Node next, TreeNode parent) { super(hash, key, val, next); this.parent = parent; } Node find(int h, Object k) { return findTreeNode(h, k, null); } ...... }
10. TreeBin类
这个类并不负责包装用户的key、value信息,而是包装了很多TreeNode节点。它代替了TreeNode的根节点,也就是说在实际的ConcurrentHashMap “数组”中,存放的是TreeBin对象,而不是TreeNode对象,这是与HashMap的显著区别,另外这个类还带有了读写锁。TreeBin源码如下:
static final class TreeBinextends Node { TreeNode root; volatile TreeNode first; volatile Thread waiter; volatile int lockState; // values for lockState static final int WRITER = 1; // set while holding write lock static final int WAITER = 2; // set when waiting for write lock static final int READER = 4; ...
11. ForwardingNode类
ForwardingNode是用于连接两个table的节点类,它包含了一个nextTable指针,用于指向下一张表。而且这个节点的key、value、next属性全部为null,它的hash值为-1。这里面定义的find()方法是从nextTable里进行节点查询,而不是以自身为头节点进行查找。
static final class ForwardingNodeextends Node { final Node [] nextTable; ForwardingNode(Node [] tab) { super(MOVED, null, null, null); this.nextTable = tab; } ...... }
12. 构造方法
因为构造函数是公开的API,所以必须和JDK 7中保持一致,但其中部分含义可能发生了一些变化,
我们来看一下参数最全的一个构造方法。
public ConcurrentHashMap(int initialCapacity,float loadFactor, int concurrencyLevel) {
if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0)
throw new IllegalArgumentException();
if (initialCapacity < concurrencyLevel)
initialCapacity = concurrencyLevel;
long size = (long)(1.0 + (long)initialCapacity / loadFactor);
int cap = (size >= (long)MAXIMUM_CAPACITY) ?
MAXIMUM_CAPACITY : tableSizeFor((int)size);
this.sizeCtl = cap;
}
上述方法中的3个核心参数分别是初始容量initialCapacity、负载因子loadFactor和并行等级concurrenyLevel,这3个核心参数的作用及含义如下:
首先,loadFactor负载因子在JDK 8的ConcurrentHashMap运行时,已经固定为了0.75f,因此这里的参数只能在创建时用来帮助确定初始的数组容量。
由于不再使用JDK 7中的Segement实现方式,因此concurrencyLevel不再用来确定Segement的数量。对于JDK 8中的ConcurrentHashMap而言,锁的粒度是对应数组中的每个位桶(理论上可以对每个桶进行并发操作),因此concurrencyLevel的含义也就是用来确定底层数据的初始容量。
这也正是size = (long)(1.0 + (long)initialCapacity / loadFactor);这行代码的意义(这里的initialCapacity是取参数中initialCapacity和concurrenyLevel中的最大值)。
另外需要注意的是,size并不是我们数组的最终容量,ConcurrentHashMap会通过tableSizeFor()方法找出 >=size 的 最小的2的n次方 作为容量。这和HashMap是一样的,需要保证容量为2的n次方,因为之后的散列操作都是基于这一前提。
最后,在得出了初始容量后,ConcurrentHashMap仅是将容量通过sizeCtl来保存,而并没有直接初始化数组,数组的初始化会被延迟到第一次put数据时进行(这样设计可能是出于节省内存的目的)。
13. Unsafe与CAS
在ConcurrentHashMap的源码中,随处可以看到U这个变量, 内部也有大量的U.compareAndSwapXXX的方法。这种方法一般是利用CAS算法实现无锁化修改值的操作,可以大大降低锁代理的性能消耗。这种算法的基本思想就是不断地去比较当前内存中的变量值与我们指定的一个变量值是否相等,如果相等,则接受我们指定修改的值,否则拒绝我们的操作。因为当前线程中的值已经不是最新的值,我们的修改很可能会覆盖掉其他线程修改的结果。这一点与乐观锁,SVN的思想是比较类似的。
以上就是 壹哥 对ConcurrentHashMap中的几个重要属性进行的简单解读,接下来我对其中几个重要的方法进行解读。
三. 结语本篇文章,壹哥 讲解了JDK 8版本ConcurrentHashMap源码中的核心属性、内部类及构造方法,了解完这些之后,我们就可以为接下来其他源码的阶段做好铺垫。接下来 壹哥 会再通过另一篇文章,给大家讲解 JDK 8中的ConcurrentHashMap#put()方法的源码,请做好准备哦。



