目录
简介
ConcurrentHashMap是线程安全的Map,Jdk1.7之前采用的是Segment数组结构和HashEntry数组结构组成,Jdk1.8之后采用的是synchronized+数组+链表/红黑树组成。个人认为ConcurrentHashMap是JDK常用集合源码解析最难的,楼主也啃了好久,中途也放弃过,屡败屡战,终于完成了常用方法的解读,给大家分享出来,如有错误,还望不吝赐教,共同进步。
准备分几篇来介绍,首先介绍常量、成员变量、内部类和部分方法。
常量
// 散列表数组(即table)长度最大限制
private static final int MAXIMUM_CAPACITY = 1 << 30;
// table默认长度
private static final int DEFAULT_CAPACITY = 16;
// 最大可能的数组长度,toArray方法使用(官方直译),常用方法中未涉及
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;
// 链表转红黑树的阈值,9个节点开始转
static final int TREEIFY_THRESHOLD = 8;
// 红黑树转链表的阈值
static final int UNTREEIFY_THRESHOLD = 6;
// 链表转红黑树是数组最小长度, 当最小长度达到64, 并且链表长度达到8, 才开始转红黑树
static final int MIN_TREEIFY_CAPACITY = 64;
// 并发扩容时最小步长
private static final int MIN_TRANSFER_STRIDE = 16;
// 扩容相关, 计算扩容时生成的一个标识戳, 全局不变
private static int RESIZE_STAMP_BITS = 16;
// 并发扩容的最大线程数, 最大为65535
private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;
// 扩容相关, 为16
private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;
/** 节点哈希字段的编码 */
// node节点的hash为-1时, 表示当前节点是FWD节点, 已经迁移完成
static final int MOVED = -1;
// node节点的hash为-2, 表示当前节点是TreeBin节点
static final int TREEBIN = -2;
// node节点的hash为-3, 表示当前节点为ReservationNode
static final int RESERVED = -3; /
// 用来获取新的hash值, 可以将一个负数位与运算后得到正数
static final int HASH_BITS = 0x7fffffff; // usable bits of normal node hash
// 当前系统的CPU数量
static final int NCPU = Runtime.getRuntime().availableProcessors();
成员变量
// table数组
transient volatile Node<K,V>[] table;
// 扩容过程中的新table, 扩容结束后会将nextTable设置为null
private transient volatile Node<K,V>[] nextTable;
// LongAdder中的baseCount, 未发生竞争时 或者 当前LongAdder处于加锁状态时, 增量累积到baseCount中
private transient volatile long baseCount;
/**
* sizeCtl < 0
* 1. -1表示当前table正在初始化(有线程正在创建table数组), 当前线程需要自旋等待
* 2. 表示当前table数组正在进行扩容, 高16位表示扩容的标识戳, 低16位表示: (1 + nThread) 当前参与并发扩容的线程数量(这个状态最重要)
*
* sizeCtl = 0
* 表示创建table数组时, 使用DEFAULT_CAPACITY为大小
*
* sizeCtl > 0
* 1. 如果table为初始化, 表示初始化大小
* 2. 如果table已经初始化, 表示下次扩容时的阈值(n * 0.75)
*/
private transient volatile int sizeCtl;
// 扩容过程中, 记录当前进度, 所有线程都需要从transferIndex中分配区间任务, 去执行自己相应的区间
private transient volatile int transferIndex;
/**
* LongAdder中的cellBusy
* 0 表示当前LongAdder对象无锁状态
* 1 表示当前LongAdder对象加锁状态, 只有一个对象能持有加锁状态
*/
private transient volatile int cellsBusy;
/**
* LongAdder中的cells数组, 当baseCount发生竞争后, 会创建cells数组
* 线程会通过计算hash值, 取到自己的cell, 将增量累加到指定cell中
* 总数 = sum(cells) + baseCount
*/
private transient volatile CounterCell[] counterCells;
// ----------------- 变量内存地址 -------------------
// 操作内存地址
private static final sun.misc.Unsafe U;
// sizeCtl属性在ConcurrentHashMap中内存偏移地址
private static final long SIZECTL;
// transferIndex属性在ConcurrentHashMap中内存偏移地址
private static final long TRANSFERINDEX;
// baseCount属性在ConcurrentHashMap中内存偏移地址
private static final long BASECOUNT;
// cellsBusy属性在ConcurrentHashMap中内存偏移地址
private static final long CELLSBUSY;
// value属性在ConcurrentHashMap中内存偏移地址
private static final long CELLVALUE;
// table数组第一个元素的偏移地址
private static final long ABASE;
// table数组第一个元素二进制编码中第一个为1的位置
private static final int ASHIFT;
内部类
链表节点Node
static class Node<K,V> implements Map.Entry<K,V> {
// 节点hash值,经过扰动运算得出来的
final int hash;
final K key;
volatile V val;
// 后继节点
volatile Node<K,V> next;
Node(int hash, K key, V val, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.val = val;
this.next = next;
}
}
红黑树节点TreeNode
static final class TreeNode<K,V> extends Node<K,V> {
// 父节点
TreeNode<K,V> parent;
// 左子节点
TreeNode<K,V> left;
// 右子节点
TreeNode<K,V> right;
TreeNode<K,V> prev; // needed to unlink next upon deletion
boolean red;
TreeNode(int hash, K key, V val, Node<K,V> next,
TreeNode<K,V> parent) {
super(hash, key, val, next);
this.parent = parent;
}
}
红黑树链表节点TreeBin
static final class TreeBin<K,V> extends Node<K,V> {
// 红黑树根节点
TreeNode<K,V> root;
// 链表的头节点
volatile TreeNode<K,V> first;
// 等待者线程(当前lockState是读锁状态)
volatile Thread waiter;
/**
* 1. 写锁状态 写是独占状态, 以table来看, 真正进入到TreeBin中的写线程, 同一时间只有一个
* 2. 读锁状态 读锁是共享的, 同一时刻可以有多个线程, 同时进入到TreeBin对象中获取数据, 每一个线程都会给lockState + 1
* 3. 等待者状态(写线程在等待), 当TreeBin中有读线程目前正在读取数据时, 写线程无法修改数据, 那么就将lockState的最低两位设置为0b,10
*/
volatile int lockState;
// 锁状态的;值
static final int WRITER = 1;
static final int WAITER = 2;
static final int READER = 4;
}
扩容完成的红黑树节点ForwardingNode
static final class ForwardingNode<K,V> extends Node<K,V> {
// 扩容中临时table数组
final Node<K,V>[] nextTable;
// ForwardingNode是node的子类, hash值为moved(-1), nextTable指向了扩容是临时存储node的新table
// 写线程: 需要参与并发扩容 读线程: 调用find方法到新表继续查询
ForwardingNode(Node<K,V>[] tab) {
super(MOVED, null, null, null);
this.nextTable = tab;
}
}
部分方法
扰动运算
static final int spread(int h) {
// 将原hash与原hash的高16位进行异或, 然后再和HASH_BITS进行位与预算
return (h ^ (h >>> 16)) & HASH_BITS;
}
在 table 的长度较小的时候,让高位也参与运算,降低 hash 冲突的概率。
获取>=c 最小2的次方数
private static final int tableSizeFor(int c) {
int n = c - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
获取数组指定下标位置i的元素
@SuppressWarnings("unchecked")
static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
}
使用Unsafe通过内存地址的操作来完成,Unsafe类存在于sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存,Unsafe类中的所有方法都是native修饰的,也就是说Unsafe类中的方法都直接调用操作系统底层资源执行相应任务。
通过CAS将table中指定位置i设置为新值v
// c:期望值,v新值
static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i,
Node<K,V> c, Node<K,V> v) {
return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
}
table数组i位置设置值v
static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) {
U.putObjectVolatile(tab, ((long)i << ASHIFT) + ABASE, v);
}
扩容标识戳
// 扩容标识戳,标识戳一致时才能参与扩容
static final int resizeStamp(int n) {
// numberOfLeadingZeros: 这个数据的二进制串中从最左边算起连续的“0”的总数量。因为int类型的数据长度为32所以高位不足的地方会以“0”填充
return Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1));
}
部分内容参考土拨鼠饲养员博主的博客,给了楼主学习ConcurrentHashMap很大的帮助, 博客地址。