写在前面
并发编程是 Java 开发者从初级迈向高级的必经之路。它不仅关乎多线程的写法,更涉及对 CPU 缓存、内存屏障、指令重排等底层原理的理解。本文从 JMM(Java 内存模型)出发,结合 JDK 各版本演进,系统梳理 Java 并发的核心机制与实战优化策略。
技术栈: Java 8~21 · JMM · JUC · JMH
难度: ⭐⭐⭐⭐
阅读时间: 30 分钟
适合人群: 2 年以上 Java 开发者、后端架构师、性能调优工程师
一、JMM 基础:并发编程的第一性原理
1.1 为什么需要内存模型?
现代 CPU 采用多级缓存架构(L1/L2/L3 Cache),每个核心拥有自己的缓存。当多个线程操作共享变量时,一个线程的修改可能对其他线程不可见——这就是可见性问题。同时,编译器和 CPU 会为了优化性能而对指令进行重排序,导致代码执行顺序与编写顺序不一致。
JMM(Java Memory Model)定义了多线程环境下变量的访问规则,核心目标有三个:
- 原子性 — 一个或多个操作要么全部执行且不被中断,要么全不执行
- 可见性 — 一个线程修改共享变量后,其他线程能立即看到
- 有序性 — 程序执行顺序按代码逻辑进行(禁止不必要的重排序)
1.2 happens-before 规则
JMM 通过 happens-before 关系来保证有序性和可见性。如果操作 A happens-before 操作 B,则 A 的结果对 B 可见,且 A 的执行顺序在 B 之前。
关键规则:
- 程序次序规则: 同一个线程中,书写在前的操作 happens-before 书写在后的操作
- 锁规则: unlock 操作 happens-before 对同一把锁的 lock 操作
- volatile 规则: 对 volatile 变量的写操作 happens-before 后续对该变量的读操作
- 传递性: 若 A happens-before B,B happens-before C,则 A happens-before C
- 线程启动规则: Thread.start() happens-before 该线程的任何操作
- 线程终止规则: 线程中所有操作 happens-before 其他线程检测到该线程终止
- 中断规则: 调用 interrupt() happens-before 被中断线程检测到中断事件
- 终结器规则: 对象的构造函数完成 happens-before finalize() 方法
// 示例:volatile 保证可见性
public class VisibilityExample {
private volatile boolean running = true;
public void worker() {
while (running) {
// 由于 volatile,worker 线程一定能看到 running 的修改
}
}
public void stop() {
running = false; // 写 volatile
}
}
1.3 内存屏障
JMM 底层通过 内存屏障(Memory Barrier)实现 happens-before 语义。常见的屏障类型:
- LoadLoad Barrier: 确保 Load1 的数据读取先于 Load2 及其后的读取操作
- StoreStore Barrier: 确保 Store1 的数据写入对其他处理器可见先于 Store2 及其后的写入
- LoadStore Barrier: 确保 Load1 的数据读取先于 Store2 及其后的写入操作
- StoreLoad Barrier: 确保 Store1 的数据写入对其他处理器可见先于 Load2 及其后的读取(最昂贵的屏障)
二、锁机制深度剖析
2.1 synchronized 的演进之路
synchronized 是 Java 最基础的同步原语,JDK 6 之后经历了巨大的性能优化:
| 锁状态 | 描述 | 开销 |
|---|---|---|
| 无锁 | 对象刚创建,无竞争 | 0 |
| 偏向锁 | 同一线程重复获取,Mark Word 记录线程 ID | 极低 |
| 轻量级锁 | 少量线程交替获取,CAS 自旋 | 低 |
| 重量级锁 | 多线程竞争激烈,线程阻塞(内核态) | 高 |
锁升级过程(不可逆): 无锁 → 偏向锁 → 轻量级锁 → 重量级锁
// JVM 参数控制锁行为
-XX:+UseBiasedLocking // 启用偏向锁(JDK 15 后默认关闭)
-XX:BiasedLockingStartupDelay=0 // 启动后立即启用偏向锁
-XX:-UseBiasedLocking // 禁用偏向锁
JDK 15 之后偏向锁被标记为废弃,JDK 21 中彻底移除。原因是偏向锁的撤销逻辑在高度竞争场景下反而增加了复杂度,且现代应用大多使用更高级的并发工具。
2.2 AQS 框架:JUC 的基石
AbstractQueuedSynchronizer(AQS)是 java.util.concurrent 包的灵魂,ReentrantLock、Semaphore、CountDownLatch、CyclicBarrier 等全部基于 AQS 实现。
核心原理:
- 一个
volatile int state表示同步状态 - 一个 CLH 变体的 FIFO 双向队列管理等待线程
- 子类通过 tryAcquire / tryRelease 等模板方法定义具体同步语义
// AQS 核心设计模式:模板方法
public abstract class AbstractQueuedSynchronizer {
// 子类需要实现的方法
protected boolean tryAcquire(int arg) { throw new UnsupportedOperationException(); }
protected boolean tryRelease(int arg) { throw new UnsupportedOperationException(); }
protected int tryAcquireShared(int arg) { throw new UnsupportedOperationException(); }
protected boolean tryReleaseShared(int arg) { throw new UnsupportedOperationException(); }
// 公开的模板方法(不可重写)
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
}
2.3 ReentrantLock vs synchronized 选择指南
| 特性 | synchronized | ReentrantLock |
|---|---|---|
| 语法简洁 | ✅ 自动释放 | ❌ 需手动 unlock |
| 可中断 | ❌ 不支持 | ✅ lockInterruptibly() |
| 超时获取 | ❌ 不支持 | ✅ tryLock(timeout, unit) |
| 公平性 | ❌ 非公平 | ✅ 可选公平/非公平 |
| 条件变量 | wait/notify(单一) | 多个 Condition 对象 |
| 读/写分离 | ❌ | ✅ ReentrantReadWriteLock |
| 性能(低竞争) | 优秀(偏向锁优化) | 优秀 |
| 性能(高竞争) | 重量级锁阻塞 | CAS + 自旋 + 阻塞 |
推荐: 默认用 synchronized(简洁安全),需要高级特性(可中断、超时、读写锁、多条件)时用 ReentrantLock。
三、无锁并发:CAS 与原子类
3.1 CAS 原理
Compare-And-Swap(CAS)是一种乐观锁机制,包含三个操作数:内存地址 V、期望值 A、新值 B。当 V 的值等于 A 时,将 V 更新为 B,否则不操作。
// CAS 的典型实现(sun.misc.Unsafe)
public final native boolean compareAndSwapObject(
Object o, long offset, Object expected, Object x);
public final native boolean compareAndSwapInt(
Object o, long offset, int expected, int x);
CAS 通过 CPU 的 cmpxchg 指令实现,是一个原子操作。但 CAS 存在三个经典问题:
- ABA 问题: 值从 A→B→A,CAS 误判未修改。解决:AtomicStampedReference 加版本号
- 自旋开销: 高竞争下大量 CAS 失败导致 CPU 空转
- 只能操作单个变量: 无法同时对多个变量做 CAS
// 手动实现一个基于 CAS 的计数器
public class CasCounter {
private final AtomicInteger count = new AtomicInteger(0);
public int increment() {
// 自旋 CAS
while (true) {
int current = count.get();
int next = current + 1;
if (count.compareAndSet(current, next)) {
return next;
}
}
}
// 等价于一行:
// public int increment() { return count.incrementAndGet(); }
}
3.2 JDK 8 的 LongAdder:高并发计数之王
AtomicLong 在高并发下 CAS 竞争极其激烈,大量线程自旋浪费 CPU。JDK 8 引入的 LongAdder 将单一热点分解为多个 Cell:
// LongAdder 核心设计
public class LongAdder extends Striped64 {
// 内部维护一个 base 和一个 Cell[] 数组
// 低竞争:直接 CAS 修改 base
// 高竞争:分散到不同的 Cell 上,最终 sum() 汇总
transient volatile long base;
transient volatile Cell[] cells;
public void add(long x) {
Cell[] as; long b, v; int m; Cell a;
if ((as = cells) != null || !casBase(b = base, b + x)) {
// cells 不为空 或 base CAS 失败 → 分散到 Cell
boolean uncontended = true;
if (as == null || (m = as.length - 1) < 0 ||
(a = as[getProbe() & m]) == null ||
!(uncontended = a.cas(v = a.value, v + x)))
longAccumulate(x, null, uncontended);
}
}
}
性能对比(JMH 压测,16 线程):
| 实现 | 吞吐量(ops/s) | 适用场景 |
|---|---|---|
| synchronized | ~500 万 | 低并发,代码简洁 |
| AtomicLong | ~2000 万 | 中等并发,需准确数值 |
| LongAdder | ~1.2 亿 | 极高并发,可接受最终一致 |
四、线程池:企业级实战与调优
4.1 ThreadPoolExecutor 核心参数
public ThreadPoolExecutor(
int corePoolSize, // 核心线程数
int maximumPoolSize, // 最大线程数
long keepAliveTime, // 非核心线程空闲存活时间
TimeUnit unit, // 时间单位
BlockingQueue<Runnable> workQueue, // 任务队列
ThreadFactory threadFactory, // 线程工厂
RejectedExecutionHandler handler // 拒绝策略
);
工作流程:
- 线程数 < corePoolSize → 创建新线程执行任务
- 线程数 ≥ corePoolSize → 任务入队
- 队列满,线程数 < maximumPoolSize → 创建新线程执行任务
- 队列满,线程数 ≥ maximumPoolSize → 执行拒绝策略
4.2 四种拒绝策略
| 策略 | 行为 | 适用场景 |
|---|---|---|
| AbortPolicy(默认) | 抛出 RejectedExecutionException | 必须处理的关键任务 |
| CallerRunsPolicy | 调用者线程执行任务 | 降低任务提交速率,背压控制 |
| DiscardPolicy | 静默丢弃 | 非关键任务,可容忍丢失 |
| DiscardOldestPolicy | 丢弃队列中最旧的任务 | 优先处理最新任务 |
4.3 线程池大小计算公式
最经典的估算公式:
// CPU 密集型
N_threads = N_CPU + 1 // +1 补偿页缺失等暂停
// IO 密集型
N_threads = N_CPU * (1 + IO_time / CPU_time)
// 示例:CPU 耗时 20ms,IO 耗时 80ms
// N_threads = 8 * (1 + 80/20) = 40
// 混合型
// 使用 CompletableFuture 拆分 CPU 和 IO 任务到不同线程池
4.4 生产级线程池配置模板
@Configuration
public class ThreadPoolConfig {
@Bean("ioThreadPool")
public ThreadPoolExecutor ioThreadPool() {
int cpuCores = Runtime.getRuntime().availableProcessors();
return new ThreadPoolExecutor(
cpuCores * 2,
cpuCores * 4,
60, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(500),
new ThreadFactoryBuilder()
.setNameFormat("io-pool-%d")
.setDaemon(true)
.build(),
new ThreadPoolExecutor.CallerRunsPolicy()
);
}
@Bean("cpuThreadPool")
public ThreadPoolExecutor cpuThreadPool() {
int cpuCores = Runtime.getRuntime().availableProcessors();
return new ThreadPoolExecutor(
cpuCores + 1,
cpuCores + 1,
0, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(200),
new ThreadFactoryBuilder()
.setNameFormat("cpu-pool-%d")
.build(),
new ThreadPoolExecutor.AbortPolicy()
);
}
@Bean("scheduledThreadPool")
public ScheduledThreadPoolExecutor scheduledThreadPool() {
return new ScheduledThreadPoolExecutor(
4,
new ThreadFactoryBuilder()
.setNameFormat("scheduled-pool-%d")
.build()
);
}
}
4.5 常见坑点与最佳实践
- 禁止使用 Executors.newFixedThreadPool(): 默认队列为 Integer.MAX_VALUE,可能 OOM
- 禁止使用 Executors.newCachedThreadPool(): 最大线程数 Integer.MAX_VALUE,可能创建过多线程
- 统一命名线程: 使用自定义 ThreadFactory,方便排查问题
- 捕获异常: 任务内部 try-catch,避免线程异常退出
- 监控告警: 暴露队列大小、活跃线程数、拒绝次数等指标
- 优雅关闭: shutdown() 后 awaitTermination(),确保任务执行完毕
// 优雅关闭模板
public void shutdownPool(ExecutorService pool, String poolName) {
pool.shutdown(); // 不再接受新任务
try {
if (!pool.awaitTermination(60, TimeUnit.SECONDS)) {
pool.shutdownNow();
if (!pool.awaitTermination(60, TimeUnit.SECONDS)) {
log.error("{} 关闭超时", poolName);
}
}
} catch (InterruptedException e) {
pool.shutdownNow();
Thread.currentThread().interrupt();
}
}
五、并发集合:选型与性能对比
| 容器 | 同步策略 | 读性能 | 写性能 | 适用场景 |
|---|---|---|---|---|
| ConcurrentHashMap | 分段锁/CAS + synchronized | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | 高并发 KV 存储 |
| CopyOnWriteArrayList | 写时复制 | ⭐⭐⭐⭐⭐ | ⭐ | 读多写极少(白名单、配置) |
| ConcurrentLinkedQueue | CAS 无锁 | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | 高吞吐消息队列 |
| ConcurrentSkipListMap | 跳表 + CAS | ⭐⭐⭐⭐ | ⭐⭐⭐ | 高并发有序 KV |
| ArrayBlockingQueue | ReentrantLock | ⭐⭐⭐ | ⭐⭐⭐ | 有界阻塞队列 |
| LinkedBlockingQueue | ReentrantLock | ⭐⭐⭐ | ⭐⭐⭐ | 可选有界/无界 |
ConcurrentHashMap 核心设计(JDK 8+)
// 核心改进:
// 1. 放弃分段锁(Segment),采用 Node 数组 + CAS + synchronized
// 2. 数组长度 2 的幂次,用 (n-1) & hash 定位
// 3. 链表超过 8 转红黑树(TREEIFY_THRESHOLD)
// 4. 扩容支持多线程协助(transfer)
// 插入逻辑(简化)
final V putVal(K key, V value, boolean onlyIfAbsent) {
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0)
tab = initTable(); // 懒初始化
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value)))
break; // 空位置直接 CAS
} else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f); // 协助扩容
else {
synchronized (f) { // 锁住桶头节点
// 链表或红黑树插入
}
}
}
}
六、性能优化实战案例
6.1 案例:高并发订单号生成器
需求: 分布式环境下生成全局唯一、趋势递增的订单号,TPS 要求 10 万+。
优化演进:
- v1 – synchronized: 性能 ~1 万 TPS,全部线程串行等待
- v2 – AtomicLong + 时间戳: ~5 万 TPS,CAS 竞争加剧
- v3 – 预分配号段(Segment ID): 每个 JVM 实例预取一段连续 ID,内存自增
public class SegmentIdGenerator {
private final AtomicLong current;
private volatile long maxId;
private final IdAllocDao dao;
private final ReentrantLock lock = new ReentrantLock();
public long nextId() {
long id = current.getAndIncrement();
if (id <= maxId) return id; // 快速路径:无需锁
// 慢速路径:需要从数据库获取新号段
lock.lock();
try {
if (current.get() > maxId) { // 双重检查
IdSegment segment = dao.nextIdSegment();
current.set(segment.getStart());
maxId = segment.getEnd();
}
return current.getAndIncrement();
} finally {
lock.unlock();
}
}
}
最终: 每个 JVM 实例单机可达 50 万+ TPS,扩容只需增加实例。
6.2 案例:高并发缓存热点 Key 优化
问题: 某个热点 Key 每秒 10 万次并发读,单机缓存被压垮。
优化方案:
- 本地缓存(Caffeine): 每个节点缓存热点数据,减少远程调用
- 读写锁: 读多写少场景使用 ReentrantReadWriteLock
- 缓存行填充(@Contended): 避免伪共享(False Sharing)
// 伪共享示例与解决
// ❌ 问题:x 和 y 在同一缓存行,多核交替修改导致缓存行失效
class FalseSharingExample {
volatile long x;
volatile long y; // 同一缓存行!
}
// ✅ 解决:@Contended 填充缓存行(JDK 8+)
@sun.misc.Contended
class PaddingExample {
volatile long x;
}
@sun.misc.Contended
class AnotherPaddingExample {
volatile long y;
}
6.3 JMH 微基准测试
// 使用 JMH 验证优化效果
@BenchmarkMode(Mode.Throughput)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@State(Scope.Thread)
public class ConcurrencyBenchmark {
private AtomicLong atomicLong = new AtomicLong();
private LongAdder longAdder = new LongAdder();
@Benchmark
public long atomicLongIncrement() {
return atomicLong.incrementAndGet();
}
@Benchmark
public long longAdderIncrement() {
longAdder.increment();
return longAdder.sum();
}
public static void main(String[] args) throws Exception {
Options opt = new OptionsBuilder()
.include(ConcurrencyBenchmark.class.getSimpleName())
.forks(1)
.threads(16)
.warmupIterations(5)
.measurementIterations(5)
.build();
new Runner(opt).run();
}
}
七、JDK 新特性中的并发进化
| JDK 版本 | 并发相关特性 | 意义 |
|---|---|---|
| JDK 5 | JUC 包诞生(AQS、线程池、并发集合) | 里程碑 |
| JDK 7 | Fork/Join 框架、Phaser | 分治并行 |
| JDK 8 | CompletableFuture、LongAdder、StampedLock | 异步编程革命 |
| JDK 9 | Reactive Streams Flow API | 响应式标准 |
| JDK 11 | Epsilon GC、Flight Recorder 增强 | 可观测性 |
| JDK 17 | 密封类(辅助不可变对象设计) | 安全并发 |
| JDK 19+ | Virtual Threads(虚拟线程/协程) | 终极简化 |
| JDK 21 | Virtual Threads GA、Scoped Values | 生产就绪 |
虚拟线程:传统线程池的终结者?
虚拟线程是 JDK 21 正式 GA 的划时代特性。它的核心思想是:一个操作系统线程(载体线程)可以承载成千上万个虚拟线程。
// 虚拟线程的使用
public class VirtualThreadDemo {
public static void main(String[] args) throws Exception {
// 方式一:直接创建
Thread vThread = Thread.startVirtualThread(() -> {
System.out.println("Hello from virtual thread: " + Thread.currentThread());
});
// 方式二:使用 Executors
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 10_000; i++) {
int taskId = i;
executor.submit(() -> {
// 每个任务一个虚拟线程,无需池化
Thread.sleep(100); // 此时虚拟线程让出载体线程
return taskId;
});
}
} // 自动关闭,等待所有任务完成
}
}
虚拟线程最佳实践:
- 不要池化虚拟线程: 创建开销极低(微秒级),池化反而增加复杂度
- 不要使用 ThreadLocal: 虚拟线程数量巨大,ThreadLocal 内存开销爆炸 → 使用 ScopedValues
- 不要使用 synchronized 块: 会固定载体线程 → 使用 ReentrantLock
- 适用于 IO 密集型任务: 大量阻塞操作时优势最明显
- CPU 密集型任务仍然需要平台线程池: 虚拟线程不能提高 CPU 利用率
八、总结:并发编程的哲学
回看整个并发编程的发展史,从 synchronized 到 AQS,从 CAS 到 LongAdder,从线程池到虚拟线程,核心追求从未改变:以最低的成本实现正确的同步。
几条贯穿始终的原则:
- 能不共享就不共享 — ThreadLocal、不可变对象、无状态设计
- 能不用锁就不用锁 — CAS、原子类、CopyOnWrite
- 要用锁就用对锁 — 细化锁粒度、避免死锁、按固定顺序加锁
- 让工具替你管理 — 使用 JUC 工具类而非手写同步
- 用数据说话 — JMH 压测、火焰图、Async Profiler 验证优化
- 拥抱新特性 — 虚拟线程将极大简化 IO 密集型并发代码
并发编程的魅力在于,它要求你同时理解硬件(CPU 缓存、内存屏障)、操作系统(线程调度、内核态切换)和语言特性(JMM、JUC)。当你能够自如地在这些层次间切换视角时,你就真正掌握了并发的精髓。
本文为每周深度技术教程系列。欢迎在评论区交流你的并发优化实战经验。