Java 并发编程全景:从底层原理到架构实战
Java 并发编程全景:从底层原理到架构实战
1. 宏观基石:进程、线程与内存模型
1.1 进程 vs 线程
- 进程:操作系统资源分配的基本单位(拥有独立的堆内存、文件句柄)。进程间通信(IPC)需借助管道、消息队列,开销大。
- 线程:CPU 调度的基本单位(共享进程资源,独有栈空间)。
1.2 上下文切换 (Context Switch)
当线程的时间片用完或被阻塞时,CPU 需保存当前线程的执行现场(寄存器、程序计数器)并加载新线程。
深度原理:切换涉及从用户态 (User Mode) 到 内核态 (Kernel Mode) 的转换,消耗大量 CPU 周期。因此,高并发不等于线程越多越好,过多的线程会导致 CPU 忙于切换而非计算。
1.3 Java 内存模型 (JMM)
JMM 屏蔽了硬件差异,定义了线程与主存的交互规范:
- 主内存:共享区域,存储所有变量。
- 工作内存:线程私有,存储变量副本。
并发三大特性:
- 原子性:操作不可分割(
i++分为读、改、写,不具备原子性)。 - 可见性:修改后其他线程立即可见。
- 有序性:禁止指令重排。
1.4 Volatile 深度解析
volatile 是轻量级同步机制,保证可见性和有序性,但不保证原子性。
- 可见性原理:写操作后强制刷新主存,读操作前强制重载主存(基于内存屏障)。
- 有序性原理:禁止 JVM 编译器和 CPU 的指令重排序。
经典案例:单例模式双重校验锁 (DCL)
Java
public class Singleton {
private static volatile Singleton instance; // 必须 volatile
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
// new 操作非原子,分为三步:
// 1. 分配内存
// 2. 初始化对象
// 3. 引用指向内存
instance = new Singleton();
}
}
}
return instance;
}
}
为什么要 volatile?
如果不加,步骤 2 和 3 可能重排序。线程 A 执行了 1->3(此时对象非 null 但未初始化),线程 B 此时判断 instance != null,直接拿走了一个半成品对象,导致系统崩溃。
2. 锁的演进:CAS、Synchronized 与锁升级
2.1 CAS (Compare And Swap)
CAS 是乐观锁的基础(如 AtomicInteger)。
- 原理:包含三个参数(内存地址 V,预期旧值 A,新值 B)。仅当 V==A 时,更新为 B。
- 缺点:
- ABA 问题:值 A->B->A,CAS 无法感知(解法:加版本号)。
- CPU 开销:自旋失败会一直占用 CPU。
2.2 Synchronized 与锁升级
JDK 1.6 后,Synchronized 不再单纯是重量级锁,引入了锁升级机制(存储在对象头 Mark Word 中):
- 偏向锁:只有一个线程访问,Mark Word 记录线程 ID,无同步开销。
- 轻量级锁:出现竞争,通过 CAS 自旋尝试获取锁(不阻塞)。
- 重量级锁:自旋失败,升级为操作系统互斥量(Mutex),线程挂起进入阻塞队列。
注意:锁只能升级,不能降级。
3. 架构核心:AQS 原理与 ReentrantLock 源码解析
AQS (AbstractQueuedSynchronizer) 是 JUC 包的基石。
3.1 AQS 核心三要素
- State (volatile int):同步状态。0=无锁,>0=有锁(体现可重入性)。
- Owner Thread:持有锁的线程。
- CLH 队列:双向链表实现的阻塞队列。
为什么用双向链表?
为了处理中断和超时。当线程在排队时被取消,需要利用前驱指针将节点从链表中间快速移除。
3.2 ReentrantLock 源码剖析
ReentrantLock 默认非公平锁,性能优于公平锁。
(1) 加锁流程 (NonfairSync)
final void lock() {
// 【步骤1】CAS 抢占(非公平体现:刚来就抢,不排队)
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1); // 抢占失败,走常规流程
}
public final void acquire(int arg) {
// tryAcquire: 再次尝试获取
// addWaiter: 放入队列尾部
// acquireQueued: 阻塞线程(LockSupport.park)
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
(2) 可重入逻辑 (nonfairTryAcquire)
Java
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) { // 无锁,尝试 CAS
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
// 锁被占了,但持有者是自己(可重入)
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires; // state + 1
setState(nextc); // 无需 CAS,因为无竞争
return true;
}
return false;
}
(3) 解锁流程 (tryRelease)
Java
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException(); // 只有持锁者才能解锁
boolean free = false;
if (c == 0) { // 只有 state 减到 0,才真正释放
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free; // 之后唤醒队列头结点的后继者
}
4. 资源调度:线程池设计与源码深度剖析
4.1 七大参数与配置
corePoolSize:核心线程数。maximumPoolSize:最大线程数。keepAliveTime:非核心线程空闲存活时间。unit:时间单位。workQueue:阻塞队列。threadFactory:线程工厂(建议自定义命名)。handler:拒绝策略。
配置公式:
- CPU 密集型:CPU 核数 + 1
- IO 密集型:CPU 核数 * 2(或 CPU / (1 - 阻塞系数))
4.2 ThreadPoolExecutor 源码剖析
(1) ctl 变量:位运算的艺术
Java
// 高3位表示状态(RUNNING等),低29位表示线程数
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
一次 CAS 即可同时控制状态和数量,保证并发安全。
(2) execute 调度逻辑
Java
public void execute(Runnable command) {
int c = ctl.get();
// 1. 线程数 < 核心数:创建核心线程
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true)) return;
c = ctl.get();
}
// 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);
}
// 3. 队列满了:创建非核心线程(由 maxPoolSize 限制)
else if (!addWorker(command, false))
reject(command); // 4. 都不行:拒绝策略
}
(3) runWorker:线程复用原理
线程启动后进入 while 循环,不断从队列拿任务,这就是线程不死的秘密。
Java
final void runWorker(Worker w) {
try {
// getTask() 是阻塞的:核心线程 take(),非核心 poll(time)
while (task != null || (task = getTask()) != null) {
w.lock();
try {
task.run(); // 直接调用 run,而非 start
} finally {
task = null;
}
}
} finally {
processWorkerExit(w, completedAbruptly);
}
}
5. 生产实战:线上故障排查与隐患治理
5.1 线上 OOM (内存溢出)
- 原因:使用
Executors.newFixedThreadPool,其底层队列是无界的LinkedBlockingQueue。请求堆积导致堆爆满。 - 解决:强制使用
ThreadPoolExecutor手动创建,并设置有界队列和拒绝策略(推荐CallerRunsPolicy,由调用者执行任务,起削峰填谷作用)。
5.2 CPU 100% 排查步骤
top:找到 CPU 最高的进程 PID。top -H -p PID:找到该进程下 CPU 最高的线程 TID。printf "%x\n" TID:转十六进制(如 0x1a)。jstack PID | grep 0x1a -A 20:查看堆栈。- 若状态是
RUNNABLE:检查死循环或复杂计算。 - 若状态是
BLOCKED:检查锁竞争。
- 若状态是
5.3 ThreadLocal 内存泄漏
-
原理:
ThreadLocalMap的 Key 是弱引用,Value 是强引用。线程长期存活(如线程池)时,Key 被 GC 回收为 null,但 Value 无法回收。 -
解决:严格遵守 “谁 set,谁 remove”。
Java
try { holder.set(val); // do something } finally { holder.remove(); // 必须清除 }
5.4 死锁与预防
- 条件:互斥、不可抢占、请求保持、循环等待。
- 解决:破坏循环等待。
- 案例:转账时(A->B, B->A),强制按账户 ID 大小顺序加锁(先锁小 ID,再锁大 ID)。
6. 工具对比:关键并发工具差异速查
| 维度 | Synchronized | Lock (ReentrantLock) |
|---|---|---|
| 实现 | JVM 关键字 (C++ Monitor) | Java 类 (AQS) |
| 释放 | 异常/结束自动释放 | 必须在 finally 中 unlock() |
| 功能 | 只有非公平,不可中断 | 可中断、可公平/非公平 |
| 等待 | wait/notify |
Condition (支持多路通知) |
| 维度 | sleep() | wait() |
|---|---|---|
| 来源 | Thread 类 | Object 类 |
| 锁处理 | 不释放锁 | 释放锁 |
| 场景 | 模拟延迟、轮询 | 线程间通信、同步 |
| 工具类 | 作用 | 场景 |
|---|---|---|
| CountDownLatch | 减法计数,等 N 个任务完成 | 启动服务前等待组件加载 |
| CyclicBarrier | 加法计数,集齐 N 个线程才出发 | 多线程计算后合并结果 |
| Semaphore | 信号量,控制并发数 | 限流(如数据库连接池) |
更多推荐




所有评论(0)