1 为什么需要并发容器
在多线程程序中,共享数据结构几乎不可避免。最直观的做法,是在普通容器外层加锁:
Map<String, Object> map = new HashMap<>();
synchronized (map) {
map.put(key, value);
}
这种方式在低并发下可以工作,但一旦并发度上升,会迅速暴露问题:
所有操作串行化,吞吐量极低
锁粒度粗,读写无法区分
容器内部结构对并发不友好
并发容器的核心目标不是“线程安全”,而是在保证线程安全的前提下,尽可能提高并发性能。
2 并发容器的设计原则
2.1 不共享就不需要同步
并发容器并不是让所有操作都无锁,而是尽量减少共享范围。
拆分内部结构
降低锁竞争概率
让更多线程同时向前推进
2.2 用 CAS 替代互斥锁
传统锁通过阻塞来解决竞争问题,而 CAS 通过重试来解决竞争问题。
CAS 的核心思想是:
只有当内存中的值与预期一致时,才进行更新,否则认为失败并重试。
这种方式避免了线程阻塞,但引入了新的设计挑战。
3 CAS 的底层语义
3.1 CAS 的基本形式
CAS 可以抽象为如下操作:
if (memory == expected) {
memory = newValue;
return true;
}
return false;
这个比较与交换的过程是由 CPU 指令保证原子性的。
3.2 Java 中的 CAS 支持
在 Java 中,CAS 通过 Unsafe 或 VarHandle 间接完成,对外暴露为原子类 API。
AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet();
incrementAndGet 并不是简单的加 1,而是一个 CAS 自旋过程。
3.3 CAS 的典型执行流程
以 incrementAndGet 为例,逻辑等价于:
for (;;) {
int current = value;
int next = current + 1;
if (compareAndSet(current, next)) {
return next;
}
}
多个线程同时执行时:
只有一个线程 CAS 成功
失败线程读取新值并继续尝试
无线程阻塞
4 CAS 的问题与代价
4.1 ABA 问题
CAS 只能判断“值是否相等”,无法判断“是否被修改过”。
如果一个变量从 A 变成 B,又变回 A,CAS 会误认为它没有发生变化。
4.2 ABA 的解决思路
Java 通过版本号机制解决 ABA 问题。
AtomicStampedReference<Integer> ref =
new AtomicStampedReference<>(100, 0);
每次更新不仅比较值,还比较版本号,从而识别中间变化。
4.3 CAS 并不一定更快
在以下场景中,CAS 可能比锁更差:
高竞争导致大量自旋
单核 CPU
长时间失败重试
因此 CAS 适合冲突概率较低、操作简单的场景。
5 原子类体系详解
5.1 基本数值原子类
AtomicInteger
AtomicLong
AtomicBoolean
适合用于:
计数器
状态标志
序列号生成
5.2 引用类型原子类
AtomicReference<T>
AtomicStampedReference<T>
AtomicMarkableReference<T>
用于对对象引用进行 CAS 操作,常见于无锁数据结构。
5.3 字段更新器
AtomicIntegerFieldUpdater
用于在不改变对象结构的前提下,对字段进行原子更新。
字段必须是 volatile,且不能是 final。
6 ConcurrentHashMap 的并发模型演进
6.1 JDK 7 的分段锁模型
JDK 7 的 ConcurrentHashMap 将数据分为多个 Segment:
每个 Segment 内部是一个 HashMap
Segment 级别加锁
并发度取决于 Segment 数量
这种设计在当年显著优于 Hashtable。
6.2 JDK 8 的 CAS + synchronized 混合模型
JDK 8 彻底移除了 Segment,采用:
数组 + 链表 / 红黑树
CAS 控制桶级别竞争
synchronized 锁住单个桶
锁粒度进一步缩小到“单个 hash 槽”。
6.3 put 操作的关键路径
put 操作大致流程:
计算 hash
CAS 尝试放入空桶
失败则对桶加锁
链表或树中插入节点
大部分情况下,put 不需要加锁。
6.4 为什么不用 ReentrantLock
ConcurrentHashMap 使用 synchronized 而非 ReentrantLock,是因为:
synchronized 在 JDK 8 后性能足够好
锁对象生命周期更短
减少对象创建成本
7 CopyOnWrite 容器的设计思想
7.1 CopyOnWrite 的核心假设
CopyOnWrite 的前提假设是:
读操作远多于写操作
写操作可以容忍较高成本
7.2 CopyOnWriteArrayList 的写时复制
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] newArray = Arrays.copyOf(array, array.length + 1);
newArray[array.length] = e;
array = newArray;
return true;
} finally {
lock.unlock();
}
}
写操作复制整个数组,读操作完全无锁。
7.3 使用 CopyOnWrite 的限制
不适合频繁写
不适合大对象
内存占用较高
8 阻塞队列的并发模型
8.1 BlockingQueue 的设计目标
BlockingQueue 将:
并发控制
条件等待
队列语义
统一封装,避免重复造轮子。
8.2 ArrayBlockingQueue 与 LinkedBlockingQueue
ArrayBlockingQueue 使用单锁控制入队和出队。
LinkedBlockingQueue 使用两把锁,分别控制 put 和 take,提高并发度。
8.3 阻塞队列与线程池的关系
线程池的核心组件就是 BlockingQueue。
队列决定任务缓存策略
队列大小影响系统稳定性
错误配置会直接导致 OOM 或任务丢失
9 并发容器的使用误区
9.1 线程安全不等于逻辑安全
if (!map.containsKey(key)) {
map.put(key, value);
}
即使 map 是 ConcurrentHashMap,这段代码依然不是线程安全的。
9.2 复合操作必须使用原子方法
应该使用:
map.putIfAbsent(key, value);
而不是手动拆分逻辑。
10 并发容器的选择策略
10.1 选择维度
读写比例
数据规模
冲突概率
是否需要阻塞语义
10.2 常见选择建议
高并发 KV:ConcurrentHashMap
读多写少列表:CopyOnWriteArrayList
任务调度:BlockingQueue
简单计数:AtomicInteger