目录
简介
ConcurrentHashMap主要应用于高并发环境下,使用了大量的lock-free技术来减轻锁竞争导致的性能下降。从JDK5开始引入,最初使用锁分段技术,即并发时不是对整个Map加锁,而是对数据所处的Segment进行加锁:(图片来自网络,侵删)
ConcurrentHashMap初始化时,计算出Segment数组的大小ssize和每个Segment中HashEntry数组的大小cap,并初始化Segment数组的0元素。Segment使用ReentrantLock可重入锁加锁。
JDK8开始,ConcurrentHashMap进行了以下三个主要改进:
- 取消分段锁机制,改用CAS
- 引入红黑树结构,元素数量达到阈值后自动进化
- 引入mappingCount方法,能够统计更多数量的元素(
)
基本认识
首先看成员变量:
//实际存放数据的数组,默认null,大小总是2的幂
//第一次插入数据时才会初始化
transient volatile Node<K,V>[] table;
//扩容时生成,大小时原数组的2倍
private transient volatile Node<K,V>[] nextTable;
//直接存储的元素数,通过CAS更新
private transient volatile long baseCount;
//用来控制table初始化和扩容
//-1:代表正在初始化
//-n:代表n-1个线程正在进行扩容
//0:默认值,将使用默认容量进行初始化
//>0:代表初始化或扩容中需要使用的容量
private transient volatile int sizeCtl;
//扩容时转移数据使用
private transient volatile int transferIndex;
//一个用于扩容的自旋锁对象
private transient volatile int cellsBusy;
//计数单元的数组,非空时数量是2的幂
//Map存储的元素真实数量等于baseCount加上每个counterCell的值
private transient volatile CounterCell[] counterCells;
// 用来支持 keySet()、entrySet()、values()等方法的视图
private transient KeySetView<K,V> keySet;
private transient ValuesView<K,V> values;
private transient EntrySetView<K,V> entrySet;
以及内部类:
- Node<K,V>:继承了Map.Entry<K,V>,是数据节点,有四个子类,分别重写了find方法:
- TreeBin:不实际存储数据,而是TreeNode的桶,维护了桶内红黑树的读写锁和节点引用,hash值固定为-2
- TreeNode:实际存储数据的节点
- ForwardingNode:扩容转发节点,,用来把原有哈希槽的操作转发到nextTable(内部记录了nextTable),hash值固定为-1。正在put的线程遇到它的find方法,就不得不协助扩容
- ReservationNode:占位加锁节点,某些方法(computeIfAbsent)用它进行加锁,hash值固定为-3
还有静态变量:
//单个table最大容量,为什么不是32呢?因为有两个bit用作哈希控制
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;
//用来与旧版本的ConcurrentHashMap保持兼容性
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;
//进化阈值,进化时必须保证table容量到达该值,否则只扩容不进化
static final int MIN_TREEIFY_CAPACITY = 64;
//要求每个线程至少调整多少个桶,用来控制大小调整时线程数量,防止过度竞争
private static final int MIN_TRANSFER_STRIDE = 16;
//用于扩容时生成一个戳,下面有用到
private static int RESIZE_STAMP_BITS = 16;
//扩容线程最多有多少个
private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;
//通过对RESIZE_STAMP_BITS移位,用来生成sizeCtl
private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;
存储结构如下:(图拍自书)
初始化:initTable方法
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
//如果表为空才初始化
while ((tab = table) == null || tab.length == 0) {
//sizeCtl<0说明已经有线程正在进行初始化操作,就不用本线程插手了
if ((sc = sizeCtl) < 0)
Thread.yield(); // lost initialization race; just spin
//否则修改sizeCtl,然后开工
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
if ((tab = table) == null || tab.le