并发集合-Concurrent系列
ConcurrentHashMap
jdk 1.7
数据结构
ConcurrentHashMap
└── segments: Segment[16] // 外层数组
│
├── Segment[0]
│ ├── table: HashEntry[2] // 内层数组
│ │ ├── [0] → HashEntry(key="a", value=1) → HashEntry(key="b", value=2) → null
│ │ └── [1] → null
│ └── lock: ReentrantLock
│
├── Segment[1]
│ ├── table: HashEntry[4] // 扩容后的数组
│ │ ├── [0] → null
│ │ ├── [1] → HashEntry(key="c", value=3) → null
│ │ └── ...
│ └── lock: ReentrantLock
└── ...
ConcurrentHashMap {
final Segment<K,V>[] segments;
}
static class Segment<K,V> extends ReentrantLock {
transient volatile HashEntry<K,V>[] table;
}
static final class HashEntry<K,V> {
final K key;
volatile V value;
final int hash;
final HashEntry<K,V> next;
}
构造方法
public ConcurrentHashMap(int initialCapacity,
float loadFactor,
int concurrencyLevel) {
// 1. 参数校验
if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
throw new IllegalArgumentException();
// 2. 确保并发级别不超过最大值(1 << 16)
if (concurrencyLevel > MAX_SEGMENTS)
concurrencyLevel = MAX_SEGMENTS;
// 3. 计算Segment数组的容量(向上取整为2的幂)
int sshift = 0;
int ssize = 1; // Segment数组的实际大小
while (ssize < concurrencyLevel) {
++sshift;
ssize <<= 1; // 通过左移保证ssize是2的幂
}
// 4. 计算用于定位Segment的掩码和位移量
this.segmentShift = 32 - sshift; // 用于计算Segment索引的高位掩码
this.segmentMask = ssize - 1; // 用于计算Segment索引的低位掩码
// 5. 计算每个Segment内部HashEntry数组的初始容量
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
int c = initialCapacity / ssize; // 平均分配到每个Segment
if (c * ssize < initialCapacity)
++c; // 向上取整
int cap = MIN_SEGMENT_TABLE_CAPACITY; // Segment内部HashEntry数组的最小容量(默认为2)
while (cap < c)
cap <<= 1; // 保证cap是2的幂
// 6. 创建第一个Segment实例(其他Segment延迟初始化)
Segment<K,V> s0 =
new Segment<K,V>(loadFactor, (int)(cap * loadFactor),
(HashEntry<K,V>[])new HashEntry[cap]);
// 7. 初始化Segment数组
Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize];
UNSAFE.putOrderedObject(ss, SBASE, s0); // 使用UNSAFE保证可见性
this.segments = ss;
}
- Segment 数组默认长度是16,但初始化时是可以指定的的,concurrencyLevel
- initialCapacity 是所有 HashEntry 数组的长度和(最小2的幂)
- initialCapacity / concurrencyLevel 是每个 HashEntry 数组的长度(向上取整也要是2的幂)
- 因为 Segment 数组不会扩容,所以初始化时只创建第一个 Segment(初始的加载因子,扩容阈值等也就是确定的,所以先创建出来后面直接使用)
特性总结
- hash 表套娃
- 第一层 数组是 Segment 类型,Segment 继承了 ReenTrantLock
- 第二次数组是 HashEntry 类型,类似一个 HashMap
- Segment 数组默认长度就是并发量,默认长度是16,一旦创建不能扩容和缩容
- segment 是段的意思,所以也叫分段锁
- 多个线程操作同一个桶要先获取 ReenTrantLock 锁,锁住的是这个桶(相比于 HashTable 锁住整个 hash 表,支持的并发更高)
- HashEntry 数组最小长度是 2
- 可以扩容
- 拉链法解决 hash 冲突,数据结构是链表,不会涉及红黑树,头插法
- 两次 hash 运算,第一次定位 Segment 数组,第二次定位 HashEntry 数组
jdk1.8
数据结构上就不是双层 hash 表了,和 HashMap 类似,没啥好说的了直接上源码
put() 添加元素
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException(); // key、value 都不允许为空,HashMap 可以一个空 key,不限 value
int hash = spread(key.hashCode()); // hash 值
int binCount = 0; // 和 HashMap 一样,用于后面是否链表转红黑树,表示链表长度
for (Node<K,V>[] tab = table;;) { // 无限循环
Node<K,V> f; int n, i, fh;
// 数组为空就初始化数组,这次循环结束,下次循环时数组就不为空了
if (tab == null || (n = tab.length) == 0)
tab = initTable(); // 初始化数组
/**
* 走到这里说明数组不为空,那就找到桶,桶里如果没有有元素的处理
* 1. 算出 key 的下标
* 2. 取出下标处的第一个元素(可能只有一个元素,可能是链表,可能是红黑树)
* 3. 如果第一个元素是空,直接 CAS 插入元素
* 4. CAS 成功,流程结束;CAS 失败,本次循环结束,进入下一次循环(下一次循环时,第一个元素就不为空了,就不会进入这个 if)
*/
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null))) // CAS,没有使用锁
break;
}
/**
* 桶里有元素的处理(第一个元素不为空就说明桶里已经有元素了),表示 hash 冲突了,但是这里没有处理 hash 冲突
* 第一个元素的 hash 如果是 -1,说明其他线程正在扩容,所以当前线程要等待,但是当前线程不会阻塞,而是去帮忙扩容
* 1. 正常情况下,hash 值是 >=0 的
* 2. MOVED(-1):正在转移元素(也就是处于扩容中,当前桶的元素正在转移到新的数组中)
* 3. TREEBIN(-2):当前桶的元素是 TreeBin(红黑树)
* 4. RESERVED(-3):保留节点
*/
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
// 这时才真正处理 hash 冲突
V oldVal = null;
synchronized (f) { // 锁住的是第一个元素,表示锁住当前桶(比 1.7 的 Segment 并发性能高)
if (tabAt(tab, i) == f) { // 再取一次,防止 synchronized 加锁时 f 被改了
if (fh >= 0) { // 根据元素的 hash 值推断出是链表,元素加入链表,尾插法(如果重复就覆盖)
// ...
}
else if (f instanceof TreeBin) { // 是红黑树(元素加入树中,重复也会覆盖)
// ...
}
}
} // 这里 synchronized 的锁就释放了,下面的树化也要保证线程安全,使用的是 CAS 方式
// 元素可能一开始链表,元素加入链表后,可能需要树化
if (binCount != 0) {
// 链表元素数量 >= 8进行树化(不是立马树化,内部先扩容,扩容后如果数组长度达到64,链表长度还是 >=8 才转红黑树)
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount); // size++ 和 扩容检查
return null;
}
如果元素是红黑树,HashMap 保存的是 TreeNode,ConcurrentHashMap 保存的是 TreeBin 对象,本质没啥区别,都是树
initTable() 初始化数组
要保证线程安全,那就只能一个线程创建数组,使用 CAS 实现的
private final Node<K,V>[] initTable() {
Node<K,V>[] tab;
int sc;
while ((tab = table) == null || tab.length == 0) { // 如果未初始化,进行初始化,但是要保证只能一个线程执行
// 抢到初始化权的线程会把 sizeCtl 改为 -1,所以这里看看 sizeCtl 是都小于 0,如果是说明当前别的线程已经在初始化了
if ((sc = sizeCtl) < 0)
Thread.yield(); // 当前线程让出 CPU,为什么要让?避免CPU空转把CPU拉满了
// 走到这里说明 sizeCtl 还不是 -1,那就要抢一下初始化权(CAS改为 - 1就表示抢到了;没抢到就进入下一次循环,会让出 CPU)
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
// 双重检查(避免其他线程已初始化完成)
if ((tab = table) == null || tab.length == 0) {
// 计算初始容量(若 sizeCtl > 0 则使用它,否则用默认值 16)
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
// 创建数组
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
table = tab = nt;
// 计算扩容阈值(0.75 * n)
sc = n - (n >>> 2);
}
} finally {
// 最终将 sizeCtl 设为阈值,此时 sizeCtl 就是正数了
sizeCtl = sc;
}
break; // 退出自旋
}
}
return tab; // 返回初始化后的 table
}
- 初始时
sizeCtl是 0,CAS 成功的线程将其设置为 -1(其他线程看到是 -1 就自旋),数组初始化后sizeCtl表示扩容阈值 - 不做初始化的线程会自旋,并且让出 CPU(
Thread.yield())避免 CPU 被打满了
addCount() 维护元素个数和扩容检查
private final void addCount(long x, int check) {
CounterCell[] as;
long b, s; // b: baseCount临时值, s: 总元素数
/*
* 第一部分:更新元素计数(分治思想)
* 1. 优先尝试更新 baseCount,失败后使用 CounterCell 数组分散竞争
* 2. ThreadLocalRandom.getProbe() 为线程分配专属槽位减少冲突
* 3. 竞争激烈时进入 fullAddCount 处理初始化/扩容/重试
* ----------------------------------------
*/
if ((as = counterCells) != null || !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
CounterCell a; long v; int m;
boolean uncontended = true;
if (as == null || (m = as.length - 1) < 0 ||
(a = as[ThreadLocalRandom.getProbe() & m]) == null ||
!(uncontended = U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
fullAddCount(x, uncontended); // 里面很多东西,有精力再看吧
return;
}
if (check <= 1)
return;
s = sumCount(); // size() 方法也会调用这个方法,分治了还要汇总求和
}
// 第二部分:检查是否需要扩容
// ----------------------------------------
if (check >= 0) {
Node<K,V>[] tab, nt; int n, sc;
// 自旋检查:元素数 >= 扩容阈值 && 表未达最大容量。需要扩容
while (s >= (long)(sc = sizeCtl) && (tab = table) != null && (n = tab.length) < MAXIMUM_CAPACITY) {
int rs = resizeStamp(n) << RESIZE_STAMP_SHIFT;
// 如果sizeCtl < 0(说明已在扩容中)
if (sc < 0) {
// 如果扩容是否已完成或无法协助,退出扩容检查
if (sc == rs + MAX_RESIZERS || sc == rs + 1 || (nt = nextTable) == null || transferIndex <= 0)
break;
// CAS 增加扩容线程数,然后协助数据迁移
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
transfer(tab, nt);
}
// 当前是首个触发扩容的线程,发起扩容
else if (U.compareAndSwapInt(this, SIZECTL, sc, rs + 2))
transfer(tab, null);
// 11. 重新计算元素总数,继续检查
s = sumCount();
}
}
}
transfer() 扩容
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
int n = tab.length, stride;
// 1. 计算每个线程处理的桶区间(最小16个桶)
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
stride = MIN_TRANSFER_STRIDE;
// 2. 初始化新表(nextTab),容量为旧表2倍
if (nextTab == null) {
try {
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
nextTab = nt;
} catch (Throwable ex) {
sizeCtl = Integer.MAX_VALUE; // 扩容失败
return;
}
nextTable = nextTab;
transferIndex = n; // 从旧表末尾开始迁移
}
int nextn = nextTab.length;
// 3. 创建ForwardingNode标记已迁移的桶
ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
// 4. 自旋迁移数据
boolean advance = true;
boolean finishing = false;
for (int i = 0, bound = 0;;) {
Node<K,V> f; int fh;
// 4.1 分配当前线程处理的桶区间 [bound, i]
while (advance) {
if (--i >= bound || finishing)
advance = false;
else if ((nextIndex = transferIndex) <= 0) {
i = -1;
advance = false;
}
// CAS更新transferIndex,领取任务区间
else if (U.compareAndSwapInt(this, TRANSFERINDEX, nextIndex, nextIndex > stride ? nextIndex - stride : 0)) {
bound = nextIndex - stride;
i = nextIndex - 1;
advance = false;
}
}
// 4.2 检查迁移是否完成
if (i < 0 || i >= n || i + n >= nextn) {
if (finishing) {
nextTable = null;
table = nextTab;
sizeCtl = (n << 1) - (n >>> 1); // 更新阈值(0.75*新容量)
return;
}
// CAS减少扩容线程数
if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
return; // 非最后一个线程直接退出
finishing = advance = true;
i = n; // 最后线程重新检查所有桶
}
}
// 4.3 处理空桶(标记为ForwardingNode)
else if ((f = tabAt(tab, i)) == null)
advance = casTabAt(tab, i, null, fwd);
// 4.4 跳过已迁移的桶
else if ((fh = f.hash) == MOVED)
advance = true;
// 4.5 迁移非空桶(链表或树)
else {
synchronized (f) {
if (tabAt(tab, i) == f) {
Node<K,V> ln, hn;
// 链表迁移(保持原顺序)
if (fh >= 0) {
// 将链表放入新表
}
// 树节点迁移
else if (f instanceof TreeBin) {
// 判断是否需要退化为链表
}
}
}
}
}
}
1.7 和 1.8 区别
| 对比项 | JDK 1.7 | JDK 1.8 |
|---|---|---|
| 数据结构 | 双层 hash 表:Segment 数组、HashEntry 数组 + 链表 | 单层 hash 表:数组+链表/红黑树 |
| 锁机制 | Segment 分段锁,ReenTrantLock 实现 | 桶级别,CAS + synchronized 实现 |
| 并发度 | Segment 数组长度 | 桶数量 |
| 扩容 | 单线程扩容、只扩容某个 Segment 内的 HashEntry 数组 | 多线程并发扩容、整体扩容(只有一个数组,类似 HashMap) |
| Hash 冲突处理 | 链表 | 链表或红黑树(当链表长度 ≥ 8 且数组长度 ≥ 64 时转换) |
| 查询性能 | 遍历链表,O(n) | 链表 O(n),红黑树 O(log n) |
| Null 值 | 不允许 key 或 value 为 null | 同样不允许 key 或 value 为 null |
- 锁粒度更细:从 Segment 级别 缩小到 Node 级别,减少锁竞争。
- 引入红黑树:解决 Hash 冲突导致的链表过长问题,提高查询效率。
- CAS 优化:减少锁的使用,提高并发性能。
- 扩容优化:支持 多线程协助扩容,提高扩容效率。
ConcurrentSkipListMap
跳表介绍
跳表由多层链表组成:
- 最底层(Level 0):完整的 有序链表,包含所有元素。
- 上层(Level 1, 2, ...):是 下层的“索引层”,节点数量逐渐减少,类似于 二分查找的跳跃路径
跳表结构:
L3: HEAD → 1 ---------------------→ 9
L2: HEAD → 1 -------→ 5 -------→ 9
L1: HEAD → 1 → 3 → 5 → 7 → 9
L0: HEAD → 1 → 2 → 3 → 4 → 5 → 6 → 7 → 8 → 9
查询路径:
1. L3: 1 → 9(跳过,9>6)
2. L2: 1 → 5 → 9(5<6<9,锁定5)
3. L1: 5 → 7(7>6,下沉)
4. L0: 5 → 6(找到!)
%% 跳表层级结构(清晰展示多层链表)
graph LR
subgraph Level 3
A3[HEAD] --> B3[1] --------> C3[9]
end
subgraph Level 2
A2[HEAD] --> B2[1] --> C2[5] --> D2[9]
end
subgraph Level 1
A1[HEAD] --> B1[1] --> C1[3] --> D1[5] --> E1[7] --> F1[9]
end
subgraph Level 0
A0[HEAD] --> B0[1] --> C0[2] --> D0[3] --> E0[4] --> F0[5] --> G0[6] --> H0[7] --> I0[8] --> J0[9]
end
%% 纵向指针连接(跨层级关系)
B3 -->|↓| B2 -->|↓| B1 -->|↓| B0
C2 -->|↓| D1 -->|↓| F0
C3 -->|↓| D2 -->|↓| F1 -->|↓| J0
阿萨德
ConcurrentSkipListMap 是 Java 并发包中的一个线程安全的有序映射实现,基于跳表(Skip List)数据结构。
ConcurrentSkipListMap 特点
- 线程安全:并发访问不需要外部同步
- 有序性:按照键的自然顺序或Comparator指定的顺序排序
- 高性能:平均时间复杂度为O(log n)
- 无锁读取:读操作不需要加锁
ConcurrentSkipListMap VS TreeMap
| 特性 | ConcurrentSkipListMap | TreeMap |
|---|---|---|
| 线程安全 | 是 | 否 |
| 底层结构 | 跳表 | 红黑树 |
| 并发性能 | 高 | 低(需外部同步) |
| 范围查询效率 | 高(链表遍历优势) | 中(需要中序遍历) |
ConcurrentSkipListMap 基本用法示例
ConcurrentSkipListMap<Integer, String> map = new ConcurrentSkipListMap<>();
// 添加元素
map.put(3, "Three");
map.put(1, "One");
map.put(2, "Two");
// 获取元素
String value = map.get(2); // "Two"
// 遍历(有序)
for (Map.Entry<Integer, String> entry : map.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}
// 输出:
// 1: One
// 2: Two
// 3: Three
// 并发操作
ExecutorService executor = Executors.newFixedThreadPool(4);
for (int i = 0; i < 10; i++) {
final int key = i;
executor.submit(() -> map.put(key, "Value-" + key));
}
ConcurrentLinkedQueue
- 无界非阻塞队列
- 基于CAS操作实现
- 高性能但size()方法开销大
ConcurrentLinkedDeque
- 无界非阻塞双端队列
- 可以在两端插入和移除元素
- 同样基于CAS实现
ConcurrentSkipListSet
- 基于ConcurrentSkipListMap实现
- 线程安全的有序Set
- 类似TreeSet的并发版本

浙公网安备 33010602011771号