ConcurrentHashMap(JDK1.8)实现线程安全的思想也已经完全变了,它摒弃了Segment(锁段)的概念,而是启用了一种全新的方式实现,利用CAS算法。它沿用了与它同时期的HashMap版本的思想,底层依然由“数组”+链表+红黑树的方式思想。
前言
在学习 ConcurrentHashMap 源码之前你需要知道以下知识:
- HashMap 源码分析(JDK1.8)
(由于 ConcurrentHashMap 与 HashMap 基本算法一致,建议先学习 HashMap) - ConcurrentHashMap 中运用到了很多实现线程安全类的技巧:线程封闭、可见性、不变性等,可以参考:Java并发编程实战(基础篇)
1.重要属性
这些属性中需要对sizeCtl
认真理解,在后面的操作中sizeCtl
起到了非常重要的作用。
/**
* 在 ConcurrentHashMap 中将负载因子改为了常量 0.75
* 在 ConcurrentHashMap 取消了 HashMap 中 loadFactor 属性
*/
private static final float LOAD_FACTOR = 0.75f;
/**
* 单个线程在扩容时处理 table 桶的最小数量(一个线程最少复制 table 中16个桶的元素)
*/
private static final int MIN_TRANSFER_STRIDE = 16;
// 下面两个属性都是用于扩容时,对容量或者sizeCtl进行位运算移位处理的数据(只要记住为 16 即可)
/**
* The number of bits used for generation stamp in sizeCtl.
* Must be at least 6 for 32bit arrays.
*/
private static int RESIZE_STAMP_BITS = 16;
/**
* The bit shift for recording size stamp in sizeCtl.
*/
private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;
/**
* 标识了 Node hash值对应的状态
*/
static final int MOVED = -1; // hash for forwarding nodes
static final int TREEBIN = -2; // hash for roots of trees
/**
* 存储数据的数组
*/
transient volatile Node<K,V>[] table;
/**
* 扩容时使用的辅助数组(只能在扩容时使用)
*/
private transient volatile Node<K,V>[] nextTable;
/**
* 控制标识符 - 在不同的时期拥有不同的作用,取值不同也代表不同的含义
* -1: 正在初始化
* <-1: sizeCtl 低16位存储着 (扩容线程数+1)
* 0: table 还未初始化
* > 0: 初始化容量或下一次扩容的阈值 (如果为正数,始终为容量的 0.75,相当于 HashMap 的 threshold)
*/
private transient volatile int sizeCtl;
/**
* 扩容时,下一个线程复制 table 数据的开始索引
*/
private transient volatile int transferIndex;
2.重要内部类
2.1 Node
Node 是最核心的内部类,它包装了 key-value 键值对,所有插入 ConcurrentHashMap 的数据都包装在这里面。它与 HashMap 中的定义很相似,但是但是有一些差别它对 value 和 next 属性设置了 volatile 同步锁,它不允许调用 setValue 方法直接改变 Node 的 value 域,它增加了 find 方法辅助 map.get() 方法(简化了 get 方法代码)。
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
volatile V val;
volatile Node<K,V> next;
// 其他代码省略
public final V setValue(V value) {
throw new UnsupportedOperationException();
}
/**
* Virtualized support for map.get(); overridden in subclasses.
*/
Node<K,V> find(int h, Object k) {
Node<K,V> e = this;
if (k != null) {
do {
K ek;
if (e.hash == h &&
((ek = e.key) == k || (ek != null && k.equals(ek))))
return e;
} while ((e = e.next) != null);
}
return null;
}
}
2.2 TreeNode
和 HashMap 一样,当链表长度过长的时候,Node 会转为 TreeNode。但是与HashMap 不相同的是,它并不是直接转换为红黑树,而是把这些结点包装成TreeNode 放在 TreeBin 对象中,由 TreeBin 完成对红黑树的包装。而且TreeNode 在 ConcurrentHashMap 集成自 Node 类,而并非 HashMap 中的集成自 LinkedHashMap.Entry<K,V> 类。
2.3 TreeBin
这个类并不负责包装用户的key、value信息,而是包装的很多TreeNode节点。它代替了TreeNode的根节点,也就是说在实际的ConcurrentHashMap“数组”中,存放的是TreeBin对象,而不是TreeNode对象,这是与HashMap的区别。另外这个类还带有了读写锁。
可以看到在构造TreeBin节点时,仅仅指定了它的hash值为 TREEBIN(-2) 常量,这也就是个标识为。同时也看到我们熟悉的红黑树构造方法
static final class TreeBin<K,V> extends Node<K,V> {
TreeNode<K,V> root;
volatile TreeNode<K,V> 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; // increment value for setting read lock
/**
* Creates bin with initial set of nodes headed by b.
*/
TreeBin(TreeNode<K,V> b) {
super(TREEBIN, null, null, null);
// TreeNode 根结点
this.first = b;
// 红黑树根结点
TreeNode<K,V> r = null;
for (TreeNode<K,V> x = b, next; x != null; x = next) {
next = (TreeNode<K,V>)x.next;
x.left = x.right = null;
if (r == null) {
x.parent = null;
x.red = false;
r = x;
}
else {
K k = x.key;
int h = x.hash;
Class<?> kc = null;
for (TreeNode<K,V> p = r;;) {
int dir, ph;
K pk = p.key;
if ((ph = p.hash) > h)
dir = -1;
else if (ph < h)
dir = 1;
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0)
dir = tieBreakOrder(k, pk);
TreeNode<K,V> xp = p;
if ((p = (dir <= 0) ? p.left : p.right) == null) {
x.parent = xp;
if (dir <= 0)
xp.left = x;
else
xp.right = x;
r = balanceInsertion(r, x);
break;
}
}
}
}
this.root = r;
assert checkInvariants(root);
}
}
2.4 ForwardingNode
一个用于连接 table 和 nextTable 的节点类,只有在扩容时才会出现。它包含一个 nextTable 指针,用于指向下一张扩容之后的表。而且这个节点的 key、value 、next 指针全部为null,它的hash值为-1(在扩容时,表示当前桶中元素已经复制完成)。find的方法是从 nextTable 里进行查询节点。
/**
* A node inserted at head of bins during transfer operations.
*/
static final class ForwardingNode<K,V> extends Node<K,V> {
final Node<K,V>[] nextTable;
ForwardingNode(Node<K,V>[] tab) {
super(MOVED, null, null, null);
this.nextTable = tab;
}
Node<K,V> find(int h, Object k) {
// loop to avoid arbitrarily deep recursion on forwarding nodes
outer: for (Node<K,V>[] tab = nextTable;;) {
Node<K,V> e; int n;
if (k == null || tab == null || (n = tab.length) == 0 ||
(e = tabAt(tab, (n - 1) & h)) == null)
return null;
for (;;) {
int eh; K ek;
if ((eh = e.hash) == h &&
((ek = e.key) == k || (ek != null && k.equals(ek))))
return e;
if (eh < 0) {
if (e instanceof ForwardingNode) {
tab = ((ForwardingNode<K,V>)e).nextTable;
continue outer;
}
else
return e.find(h, k);
}
if ((e = e.next) == null)
return null;
}
}
}
}
3.Unsafe 与 CAS
在ConcurrentHashMap中,随处可以看到U, 大量使用了U.compareAndSwapXXX的方法,这个方法是利用一个CAS算法实现无锁化的修改值的操作,他可以大大降低锁代理的性能消耗。这个算法的基本思想就是不断地去比较当前内存中的变量值与你指定的一个变量值是否相等,如果相等,则接受你指定的修改的值,否则拒绝你的操作。因为当前线程中的值已经不是最新的值,你的修改很可能会覆盖掉其他线程修改的结果。是一种乐观锁的思想。
3.1 Unsafe 静态代码块
Unsafe.objectFieldOffset(Field var1)
方法:获取了 ConcurrentHashMap 中一些重要属性(字段)相对Java对象的“起始地址”的偏移量,这样就可以调用 CAS 方法来使用前面的偏移量访问、修改某个属性(字段),这样实现了偏移量和属性的双向绑定。
// 这段代码就是通过 sizeCtl 的偏移量 SIZECTL,来调用 compareAndSwapInt 方法
U.compareAndSwapInt(this, SIZECTL, sc