Java 并发原理与性能优化:从 JMM 到高并发实战
一、开篇:并发编程的三大困境
并发编程被誉为 Java 开发者的”成年礼”——几乎所有经历过生产环境重大事故的工程师,最终都会回到同一个原点:对并发原理的理解不够透彻。
从 CPU 缓存一致性到指令重排序,从 volatile 的可见性语义到 synchronized 的锁升级路径,这些底层机制决定了你写出的代码究竟是在”并发执行”还是在”碰运气”。
本文将从 Java 内存模型(JMM)出发,沿着”原理 → 工具 → 模式 → 调优”的路径,系统性地梳理 Java 并发编程的核心知识体系。无论你是正在准备面试的中级工程师,还是面对高并发生产环境的高级开发者,这篇文章都能给你一张可落地的”并发地图”。
二、Java 内存模型(JMM):并发的地基
2.1 为什么需要 JMM?
现代 CPU 架构下,每个核心都有自己的 L1/L2 缓存。线程在 CPU 上执行时,操作的并不是主内存(RAM),而是 CPU 缓存。这就带来了 缓存一致性 问题:
线程 A (Core 0) 线程 B (Core 1)
│ │
▼ ▼
L1 Cache ◄──────────────► L1 Cache
│ │
└──────────┬─────────────┘
▼
Main Memory
JMM 的核心工作就是:定义一套规则,规定一个线程对共享变量的写入何时对另一个线程可见。
2.2 JMM 的三大特性
2.3 happens-before 规则(面试必考)
JMM 通过 happens-before 关系来保证有序性。如果两个操作之间存在 happens-before 关系,那么第一个操作的结果对第二个操作可见。
八大规则中,最常被问到的:
- 程序顺序规则:一个线程中的每个操作,happens-before 于该线程中的任意后续操作
- volatile 规则:对一个 volatile 变量的写,happens-before 于任意后续对这个变量的读
- 锁规则:对一个锁的解锁,happens-before 于后续对这个锁的加锁
- 传递性:如果 A happens-before B,且 B happens-before C,则 A happens-before C
// 经典示例:volatile 保证可见性
public class ThreadStopExample {
// 不加 volatile → 可能永远无法退出循环
private volatile boolean running = true;
public void run() {
new Thread(() -> {
while (running) {
// do work
}
System.out.println("线程退出");
}).start();
}
public void stop() {
running = false; // 写 volatile → 对读线程可见
}
}
三、synchronized 深度解析:从偏向锁到重量锁
3.1 锁升级过程
JDK 6 之后,synchronized 经历了大重构。不再是”重量级锁”的代名词,而是引入了锁升级机制:
无锁 → 偏向锁 → 轻量级锁 → 重量级锁
// 观察锁状态(需开启偏向锁:-XX:+UseBiasedLocking)
public class LockUpgradeDemo {
static final Object lock = new Object();
public static void main(String[] args) throws Exception {
// 1. 无锁状态:object header = 0x0000000000000001
Thread.sleep(5000); // 偏向锁激活延迟约4秒
// 2. 偏向锁:同一个线程多次获取
synchronized (lock) {
System.out.println("偏向锁");
}
// 3. 轻量级锁:另一个线程短暂竞争
Thread t2 = new Thread(() -> {
synchronized (lock) {
System.out.println("轻量级锁");
}
});
t2.start();
t2.join();
// 4. 重量级锁:长时间自旋竞争
// 使用 -XX:PreBlockSpin=10 控制自旋次数
}
}
3.2 锁消除与锁粗化
JIT 编译器在运行时还会做两个重要优化:
-
- 锁消除:检测到锁对象只在当前线程使用 → 直接去掉锁
// JIT 会消除这里的锁,因为 sb 不会逃逸
public static String concat(String s1, String s2) {
StringBuilder sb = new StringBuilder();
sb.append(s1);
sb.append(s2);
return sb.toString();
}
-
- 锁粗化:连续对同一对象加锁 → 合并为一次
// JIT 会将三次加锁合并为一次
for (int i = 0; i < 100; i++) {
synchronized (lock) {
count++;
}
}
四、AQS 架构:JUC 锁的基石
4.1 AQS 的设计哲学
AbstractQueuedSynchronizer(AQS) 是 JUC 包(java.util.concurrent)的基石。ReentrantLock、CountDownLatch、Semaphore、ThreadPoolExecutor 的核心都是 AQS。
AQS 维护两个核心变量:
public abstract class AbstractQueuedSynchronizer {
// 1. 同步状态(volatile 保证可见性)
// 0 = 未锁定, 1 = 锁定, >1 = 可重入计数
private volatile int state;
// 2. CLH 变体队列(双向链表 + CAS 入队)
private transient volatile Node head;
private transient volatile Node tail;
}
4.2 独占锁 vs 共享锁
// 独占锁模式示例:ReentrantLock
// acquire(1) → tryAcquire(1) → 失败则入队并阻塞
// release(1) → tryRelease(1) → 成功则唤醒后继节点
// 共享锁模式示例:CountDownLatch
// acquireShared(1) → tryAcquireShared(1) → 负数表示失败
// releaseShared(1) → tryReleaseShared(1) → 成功则广播唤醒
4.3 自定义同步器实战
public class SimpleLatch {
private static final class Sync extends AbstractQueuedSynchronizer {
@Override
protected int tryAcquireShared(int acquires) {
// count == 0 时通过
return getState() == 0 ? 1 : -1;
}
@Override
protected boolean tryReleaseShared(int releases) {
// 每次释放将 count -1
for (;;) {
int c = getState();
if (c == 0) return false;
int next = c - 1;
if (compareAndSetState(c, next)) {
return next == 0;
}
}
}
}
private final Sync sync = new Sync();
public void countDown() { sync.releaseShared(1); }
public void await() { sync.acquireShared(1); }
}
关键理解:AQS 的核心不是 Lock,而是一个 状态机 + 阻塞队列 的通用框架。任何需要”资源不够就排队”的场景,都可以用 AQS 实现。
五、线程池源码级剖析
5.1 ThreadPoolExecutor 的核心变量
// 用 int 高 3 位存状态,低 29 位存线程数(经典位运算)
// RUNNING = 111 | 000...0
// SHUTDOWN = 000 | 000...0
// STOP = 001 | 000...0
// TIDYING = 010 | 000...0
// TERMINATED = 011 | 000...0
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
5.2 提交任务的完整流程
public void execute(Runnable command) {
int c = ctl.get();
// Step 1: 工作线程 < corePoolSize → 新建核心线程
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
// Step 2: 线程池运行中 → 尝试入队
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
// 二次检查:状态变了则回滚
if (!isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
// Step 3: 队列满 → 尝试新建非核心线程
else if (!addWorker(command, false))
// Step 4: 也失败了 → 拒绝策略
reject(command);
}
5.3 四种拒绝策略的选择
5.4 最佳实践:动态线程池
@Slf4j
public class DynamicThreadPool {
private final ThreadPoolExecutor executor;
public DynamicThreadPool(int core, int max, int queueSize) {
this.executor = new ThreadPoolExecutor(
core, max, 60, TimeUnit.SECONDS,
new LinkedBlockingQueue(queueSize),
new ThreadPoolExecutor.CallerRunsPolicy()
);
// 开启监控线程
monitor();
}
private void monitor() {
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
scheduler.scheduleAtFixedRate(() -> {
log.info("线程池状态 - 活跃: {}, 队列: {}, 完成: {}, 峰值: {}",
executor.getActiveCount(),
executor.getQueue().size(),
executor.getCompletedTaskCount(),
executor.getLargestPoolSize()
);
// 动态调整:队列积压 → 临时扩容
int queueSize = executor.getQueue().size();
if (queueSize > 100 && executor.getMaximumPoolSize() < 200) {
executor.setMaximumPoolSize(200);
executor.setCorePoolSize(100);
log.warn("队列积压 {},临时扩容至 core=100, max=200", queueSize);
}
}, 0, 30, TimeUnit.SECONDS);
}
}
六、常见并发陷阱与最佳实践
6.1 陷阱一:this 引用逃逸
// ❌ 错误:构造期间发布 this
public class ThisEscape {
public ThisEscape() {
new Thread(() -> {
// 此时 this 可能尚未构造完成!
doSomething();
}).start();
}
}
// ✅ 正确:私有构造器 + 工厂方法
public class SafePublication {
private final int value;
private SafePublication(int v) { this.value = v; }
public static SafePublication create(int v) {
SafePublication obj = new SafePublication(v);
// 等完全构造后再启动线程
return obj;
}
}
6.2 陷阱二:双重检查锁定(DCL)与 volatile
// ❌ 错误:缺少 volatile,可能读到半构造对象
private static Singleton instance;
public static Singleton getInstance() {
if (instance == null) { // 第1次检查
synchronized (Singleton.class) {
if (instance == null) { // 第2次检查
instance = new Singleton(); // 可能指令重排序!
}
}
}
return instance;
}
// ✅ 正确:volatile 禁止重排序
private static volatile Singleton instance;
6.3 陷阱三:ConcurrentHashMap 的错误使用
// ❌ 错误:get 和 put 不是原子操作
if (!map.containsKey(key)) {
map.put(key, value); // 两个线程可能同时进入
}
// ✅ 正确:使用原子方法
map.putIfAbsent(key, value);
// 或 computeIfAbsent
map.computeIfAbsent(key, k -> computeExpensive(k));
6.4 陷阱四:ThreadLocal 内存泄漏
// ❌ 错误:线程池中使用不清理
private static final ThreadLocal ctx = new ThreadLocal();
public void handleRequest() {
ctx.set(new Context()); // 线程复用,上次的 Context 没清理
// ... 业务逻辑
// ctx.remove(); // 忘记清理 → OOM
}
// ✅ 正确:finally 块中确保清理
try {
ctx.set(context);
// ... 业务逻辑
} finally {
ctx.remove();
}
七、性能调优实战案例
7.1 场景:高并发 API 网关限流
问题:QPS 5000 的 API 网关,用 synchronized 做计数器,导致 CPU 飙升。
❌ 原始方案:synchronized 计数器
QPS 5000 → 50% 线程在竞争同一把锁 → CPU 100%
✅ 优化方案:LongAdder 分段累加
QPS 5000 → 几乎无锁竞争 → CPU 35%
public class RateLimiter {
// ❌ 错误:synchronized 全局竞争
private final AtomicLong counter = new AtomicLong(0);
// ✅ 正确:LongAdder 分段累加(Striped64 思想)
private final LongAdder counter2 = new LongAdder();
// LongAdder 内部维护一个 Cell[] 数组
// 每个线程映射到不同 Cell,最后 sum() 时汇总
}
7.2 场景:数据库连接池参数调优
生产环境实测数据(8核16G JDBC 连接池):
连接数 | TPS | 平均延迟 | P99 延迟
10 | 1200 | 5ms | 18ms
20 | 1800 | 8ms | 32ms
50 | 2100 | 18ms | 85ms ← 连接数过多导致上下文切换
100 | 1900 | 35ms | 220ms ← 明显过载
结论:连接数 ≈ (核数 × 2) 通常是最优值
7.3 场景:生产者-消费者模式优化
public class OptimizedPipeline {
// 使用 disruptor 风格的环形缓冲区思想
private final BlockingQueue queue =
// LinkedBlockingQueue: 可扩容但加锁
// ConcurrentLinkedQueue: 无锁但无阻塞
// 折中方案:有界队列 + 合理容量
new LinkedBlockingQueue(1024);
// Java 21 虚拟线程(Virtual Threads)的应用
public void processWithVT() {
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i handleTask(taskId));
}
}
}
}
八、监控与诊断工具
8.1 快速定位死锁
# 1. 获取 Java 进程 PID
jps -l
# 2. 打印线程转储(含死锁检测)
jstack
# 3. 输出示例:
Found one Java-level deadlock:
=============================
"Thread-1":
waiting to lock monitor 0x00007f... (object: LockB)
which is held by "Thread-0"
"Thread-0":
waiting to lock monitor 0x00007f... (object: LockA)
which is held by "Thread-1"
8.2 JMC(Java Mission Control)飞行记录器
# 启动时录制(生产友好,低开销)
-XX:StartFlightRecording=duration=60s,filename=recording.jfr
# 或运行时动态开启
jcmd JFR.start duration=60s filename=recording.jfr
8.3 Arthas 在线诊断
# 查看线程池状态
thread -n 5 # 显示最忙的5个线程
stack com.example.MyService#method # 查看方法调用栈
# 查看锁竞争情况
sc -d com.example.MyLock # 查看对象的 monitor 信息
九、Java 21+ 的新并发特性
9.1 虚拟线程(Virtual Threads)
Java 21 正式 GA 的虚拟线程,彻底改变了并发编程的性价比模型:
// 之前:2000个平台线程就很吃力
ExecutorService pt = Executors.newFixedThreadPool(200);
// 现在:20万个虚拟线程轻松运行
try (var vt = Executors.newVirtualThreadPerTaskExecutor()) {
IntStream.range(0, 200_000).forEach(i ->
vt.submit(() -> handleRequest(i))
);
}
适用场景:IO 密集型(数据库查询、RPC 调用、HTTP 请求)
注意事项:
- 避免 synchronized 阻塞虚拟线程(会导致 Pin 到平台线程)
- 推荐使用
ReentrantLock替代 synchronized - 虚拟线程不适用于 CPU 密集型任务
9.2 ScopedValue(结构化并发)
// 替代 ThreadLocal,更安全、更高效
private static final ScopedValue USER_ID = ScopedValue.newInstance();
public void handleRequest() {
ScopedValue.where(USER_ID, "user-001").run(() -> {
// 这里的代码可以直接访问 USER_ID
String uid = USER_ID.get(); // "user-001"
service.call();
});
// 离开 scope 后自动清理,不会泄漏
}
十、总结与推荐阅读路径
核心学习路径
第一阶段 原理篇
└─ JMM + happens-before + volatile/synchronized 原理
└─ 推荐:《Java并发编程的艺术》第1-3章
第二阶段 工具篇
└─ JUC 包:AQS + Lock + ConcurrentHashMap + ThreadPool
└─ 推荐:阅读 JDK 源码(java.util.concurrent 包)
第三阶段 实战篇
└─ 性能调优 + 故障诊断 + 监控工具
└─ 推荐:《Java性能权威指南》+ Arthas 官方文档
第四阶段 前瞻篇
└─ 虚拟线程 + 结构化并发 + Project Loom
└─ 推荐:JEP 444 (Virtual Threads) + JEP 429 (Scoped Values)
一句话记住
> 并发编程的本质,是对”可见性、原子性、有序性”的理解和权衡。理解 JMM 你就能看懂每一行同步代码的背后发生了什么,理解 AQS 你就能驾驭整个 JUC 生态。