# Java 并发编程深度
用生活化的比喻,让你深入理解 AQS、线程池、锁优化和内存屏障
前置知识:第01章 JVM 深度原理(内存模型基础)
阅读指南(初学者必看)
为什么你需要学习 Java 并发编程深度?
游戏服务器天然是高并发系统——成百上千的玩家同时操作。不理解并发编程,就无法:
- 正确使用锁和同步工具,避免死锁和数据竞争
- 合理配置线程池,避免线程耗尽或资源浪费
- 理解 volatile 和内存屏障,写出正确的并发代码
学完本章,你能回答:
- AQS 是什么?ReentrantLock 和 CountDownLatch 底层怎么实现的?
- 线程池的 7 个参数分别是什么?游戏服务器该怎么配置?
- 偏向锁 → 轻量级锁 → 重量级锁的升级过程是什么?
- Happens-Before 规则有哪些?volatile 为什么能保证可见性?
- CompletableFuture 如何组合异步任务?虚拟线程是什么?
本文结构
第一部分:AQS 原理与源码分析(并发框架的基石)
第二部分:线程池深度解析(7参数 + 游戏服务器配置)
第三部分:锁优化(偏向锁 → 轻量级锁 → 重量级锁)
第四部分:内存屏障与 Happens-Before(并发正确性的保证)
第五部分:JUC 并发工具类与异步编程(CompletableFuture、虚拟线程)
一、并发编程的核心问题
1.1 为什么需要锁?
生活类比:两个厨师共用一口锅。
public class Kitchen {
private int soup = 100;
public void serve() {
if (soup > 0) {
soup = soup - 1;
}
}
}
三个核心问题:
- 原子性:操作不可中断
- 可见性:修改后其他线程立即可见
- 有序性:代码执行顺序符合预期
二、synchronized 锁升级
偏向锁 → 轻量级锁 → 重量级锁
(无竞争) (CAS自旋) (操作系统互斥)
生活类比:
- 偏向锁:办公室门上贴你的名字,你直接进
- 轻量级锁:门上贴个便签,你先 CAS 试试能不能进
- 重量级锁:门口排队,让操作系统来管
锁升级过程详解
对象头(Mark Word)中的锁状态:
┌──────────────┬─────────────────────────────────────┐
│ 锁状态 │ 存储内容 │
├──────────────┼─────────────────────────────────────┤
│ 无锁 │ 对象HashCode、分代年龄 │
│ 偏向锁 │ 线程ID、Epoch、分代年龄 │
│ 轻量级锁 │ 指向栈中锁记录的指针 │
│ 重量级锁 │ 指向Monitor对象的指针 │
└──────────────┴─────────────────────────────────────┘
升级流程:
1. 无锁 → 偏向锁:第一个线程访问时,CAS 将线程ID写入Mark Word
2. 偏向锁 → 轻量级锁:第二个线程尝试获取,偏向锁撤销,升级为轻量级锁
3. 轻量级锁 → 重量级锁:自旋超过阈值(自适应自旋),升级为重量级锁
| 锁状态 | 类比 | 适用场景 |
|---|---|---|
| 无锁 | 厕所没人,门开着 | 没有竞争 |
| 偏向锁 | 专人专用,贴个名字标签 | 只有一个线程用 |
| 轻量级锁 | 自旋(在门口转圈等) | 竞争不激烈 |
| 重量级锁 | 去大厅坐着等,轮到叫你 | 竞争激烈 |
synchronized 用法
// 1. 锁实例方法(锁this)
synchronized void method() {
// 锁住当前对象
}
// 2. 锁静态方法(锁class)
synchronized static void staticMethod() {
// 锁住Class对象
}
// 3. 锁代码块(更灵活)
void method() {
synchronized (this) { // 锁this
// 只锁这一块代码
}
}
synchronized 原理
每个对象有一个Monitor(锁)
线程进入synchronized方法/块:
→ 检查Monitor是否被占用
→ 没占用 → 设置为占用 → 进入
→ 占用了 → 阻塞等待
线程离开synchronized:
→ 释放Monitor
→ 唤醒等待的线程
三、AQS 原理与源码分析
生活类比:AQS 就像银行的取号排队系统。
// AQS 核心思想
// state 变量:表示锁的状态(0=未锁定,1=已锁定)
// CLH 队列:等待锁的线程排队
// 独占模式:ReentrantLock(一个人用)
// 共享模式:Semaphore / CountDownLatch(多人用)
// ReentrantLock 加锁简化流程
lock() {
if (CAS(state, 0, 1)) {
// 成功获取锁
setExclusiveOwnerThread(currentThread);
} else {
// 获取失败,加入 CLH 队列等待
enqueue();
park(); // 挂起线程
}
}
AQS 核心数据结构
CLH 队列(双向链表):
head tail
↓ ↓
┌─────────┐ ┌─────────┐ ┌─────────┐
│ Node │→│ Node │→│ Node │
│ SHARED │ │ EXCLUSIVE│ │ EXCLUSIVE│
│ ws=SIGNAL│ │ ws=SIGNAL│ │ ws=0 │
└─────────┘ └─────────┘ └─────────┘
↑ ↑ ↑
已获取锁 等待中 等待中
acquire 核心逻辑
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
流程:
- tryAcquire()尝试获取锁
- 失败则addWaiter()加入队列尾部
- acquireQueued()在队列中自旋/阻塞等待
- 前驱是头节点且tryAcquire成功则获得锁
基于 AQS 的并发工具
| 工具 | 模式 | state 含义 | 典型场景 |
|---|---|---|---|
| ReentrantLock | 独占 | 0=未锁,1=已锁,>1=重入 | 互斥访问 |
| ReentrantReadWriteLock | 共享+独占 | 高16位=读锁数,低16位=写锁数 | 读多写少 |
| Semaphore | 共享 | 许可证数量 | 限流 |
| CountDownLatch | 共享 | 剩余计数 | 等待N个任务完成 |
| CyclicBarrier | 不用AQS | 不用AQS | 多线程互相等待 |
游戏服务器中的 AQS 应用:
ReentrantLock:保护战斗状态变更Semaphore:限制同时进入副本的玩家数CountDownLatch:等待所有玩家加载完成
四、ReentrantLock 与 CAS
ReentrantLock 基本用法
public class ReentrantLockDemo {
private final ReentrantLock lock = new ReentrantLock();
private int count = 0;
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
}
ReentrantLock vs synchronized
| 特性 | synchronized | ReentrantLock |
|---|---|---|
| 获取方式 | JVM隐式 | 显式lock/unlock |
| 可重入 | 支持 | 支持 |
| 公平性 | 非公平 | 可选公平/非公平 |
| 可中断 | 不支持 | lockInterruptibly() |
| 超时获取 | 不支持 | tryLock(timeout) |
| 条件变量 | 一个 | 多个Condition |
CAS 原理
CAS = Compare And Swap
生活类比:抢火车票时,系统检查"现在还是有余票状态吗?是的话扣减一张"。
public class AtomicDemo {
private AtomicInteger counter = new AtomicInteger(0);
public void increment() {
counter.incrementAndGet();
}
}
ABA 问题
问题:值从A变成B又变回A,CAS检查不出来。
解决:AtomicStampedReference(带版本号)
AtomicStampedReference<Integer> money =
new AtomicStampedReference<>(100, 0);
money.compareAndSet(100, 50, 0, 1);
ReadWriteLock 读写锁
适用场景:读多写少,读读不互斥,读写互斥。
public class GameConfig {
private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
private final Lock readLock = rwLock.readLock();
private final Lock writeLock = rwLock.writeLock();
private Map<String, String> config = new HashMap<>();
public String getConfig(String key) {
readLock.lock();
try {
return config.get(key);
} finally {
readLock.unlock();
}
}
public void setConfig(String key, String value) {
writeLock.lock();
try {
config.put(key, value);
} finally {
writeLock.unlock();
}
}
}
五、线程池深度解析
// 7 个核心参数
ThreadPoolExecutor(
int corePoolSize, // 核心线程数(常驻)
int maximumPoolSize, // 最大线程数
long keepAliveTime, // 非核心线程空闲存活时间
TimeUnit unit, // 时间单位
BlockingQueue<Runnable> workQueue, // 任务队列
ThreadFactory threadFactory, // 线程工厂
RejectedExecutionHandler handler // 拒绝策略
)
// 执行流程:
// 1. 线程数 < corePoolSize → 创建新线程
// 2. 线程数 >= corePoolSize → 放入队列
// 3. 队列满 → 创建非核心线程(到 maximumPoolSize)
// 4. 线程数 >= maximumPoolSize 且队列满 → 执行拒绝策略
拒绝策略
| 策略 | 行为 | 生活类比 | 适用场景 |
|---|---|---|---|
| AbortPolicy | 抛异常 | 客满了直接拒绝 | 需要感知拒绝的场景 |
| CallerRunsPolicy | 调用者线程执行 | 老板亲自上阵 | 不想丢弃任务 |
| DiscardPolicy | 默默丢弃 | 客满了悄悄忽略 | 可容忍丢失 |
| DiscardOldestPolicy | 丢弃最老任务 | 赶走等最久的客人 | 优先新任务 |
游戏服务器线程池配置
游戏服务器线程池配置:
- 网络线程:核心数 = CPU 核心数
- 逻辑线程:核心数 = CPU 核心数 + 1(少量等待)
- DB 线程:核心数 = 2~4(IO 密集,但不需太多)
// 游戏服务器典型线程池配置
// 网络I/O线程池
ThreadPoolExecutor networkPool = new ThreadPoolExecutor(
Runtime.getRuntime().availableProcessors(), // CPU核心数
Runtime.getRuntime().availableProcessors() * 2,
60, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(1000),
new ThreadFactoryBuilder().setNameFormat("net-%d").build(),
new CallerRunsPolicy()
);
// 游戏逻辑线程池
ThreadPoolExecutor logicPool = new ThreadPoolExecutor(
Runtime.getRuntime().availableProcessors() + 1, // CPU核心+1
Runtime.getRuntime().availableProcessors() * 2,
60, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(500),
new ThreadFactoryBuilder().setNameFormat("logic-%d").build(),
new AbortPolicy() // 逻辑任务不能丢弃,抛异常好排查
);
// DB操作线程池
ThreadPoolExecutor dbPool = new ThreadPoolExecutor(
4, // 核心线程少,IO密集
8,
60, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(2000), // 队列大,DB操作可以等
new ThreadFactoryBuilder().setNameFormat("db-%d").build(),
new CallerRunsPolicy() // DB操作不能丢
);
线程池监控
// 重要监控指标
public void monitorPool(ThreadPoolExecutor pool) {
// 当前活跃线程数
int activeCount = pool.getActiveCount();
// 队列中的任务数
int queueSize = pool.getQueue().size();
// 已完成的任务总数
long completedCount = pool.getCompletedTaskCount();
// 线程池当前线程数
int poolSize = pool.getPoolSize();
// 如果队列快满了,需要告警
if (queueSize > 800) {
alert("线程池队列堆积:" + queueSize);
}
}
六、内存屏障与 Happens-Before
为什么需要内存屏障? CPU 和编译器会重排序指令,多线程下可能导致数据不一致。
// Happens-Before 规则(核心 6 条)
1. 程序顺序规则:同一线程中,前面的操作 happens-before 后面的操作
2. 锁规则:unlock happens-before 后续对同一个锁的 lock
3. volatile 规则:volatile 写 happens-before 后续对同一个变量的读
4. 线程启动规则:Thread.start() happens-before 线程内的所有操作
5. 线程终止规则:线程内所有操作 happens-before Thread.join() 返回
6. 传递性:A happens-before B,B happens-before C → A happens-before C
volatile 的底层实现
生活类比:volatile 变量就像公司公告栏——每次修改都立即通知所有人。
// volatile 写操作:
// 1. 将当前线程工作内存的值刷新到主内存
// 2. 插入 StoreLoad 屏障
// volatile 读操作:
// 1. 从主内存读取最新值到工作内存
// 2. 插入 LoadLoad 屏障
// 典型应用:状态标志
volatile boolean running = true;
// 游戏服务器中的典型场景
public class GameLoop {
private volatile boolean stopped = false;
public void stop() {
stopped = true; // volatile 写,所有线程立即可见
}
public void run() {
while (!stopped) { // volatile 读,每次都从主内存读
processTick();
}
}
}
内存屏障的类型
| 屏障类型 | 作用 | 生活类比 |
|---|---|---|
| LoadLoad | 保证 Load1 先于 Load2 | 先看公告栏再看邮件 |
| StoreStore | 保证 Store1 先于 Store2 | 先写公告栏再写邮件 |
| LoadStore | 保证 Load 先于 Store | 先看公告栏再写邮件 |
| StoreLoad | 保证 Store 先于 Load(最全能) | 先写公告栏再让所有人看 |
七、JUC 并发工具类与异步编程
CountDownLatch(倒计时门闩)
场景:等待多个线程完成后再继续
// 游戏加载场景:等待所有资源加载完成
public class GameLoading {
public void loadGame(Callback callback) {
CountDownLatch latch = new CountDownLatch(3);
executor.submit(() -> {
loadConfig();
latch.countDown();
});
executor.submit(() -> {
loadSprites();
latch.countDown();
});
executor.submit(() -> {
loadAudio();
latch.countDown();
});
try {
latch.await(30, TimeUnit.SECONDS);
callback.onComplete();
} catch (InterruptedException e) {
callback.onTimeout();
}
}
}
CyclicBarrier(循环栅栏)
场景:多个线程互相等待,到齐后一起执行
// 场景:回合制游戏中,等待所有玩家准备好再开始回合
public class TurnBasedGame {
private final CyclicBarrier barrier = new CyclicBarrier(4, () -> {
System.out.println("所有玩家准备完成,开始回合!");
executeTurn();
});
public void playerReady(int playerId) {
System.out.println("玩家" + playerId + "准备好了");
try {
barrier.await();
} catch (InterruptedException | BrokenBarrierException e) {
// 处理异常
}
}
}
Semaphore(信号量)
场景:控制同时访问资源的线程数量
// 场景:数据库连接池限制、接口限流
public class ApiRateLimiter {
private final Semaphore semaphore = new Semaphore(10);
public void callApi(Runnable task) {
try {
semaphore.acquire();
try {
task.run();
} finally {
semaphore.release();
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
CompletableFuture(强大的异步编程)
// 1. 创建CompletableFuture
CompletableFuture<Integer> cf1 = CompletableFuture.supplyAsync(() -> {
return 42;
});
// 2. 链式操作
CompletableFuture.supplyAsync(() -> 42)
.thenApply(n -> n * 2)
.thenApply(n -> "结果:" + n)
.thenAccept(s -> System.out.println(s));
// 3. 组合多个Future
CompletableFuture<List<Player>> playersFuture = getPlayers();
CompletableFuture<List<Item>> itemsFuture = getItems();
playersFuture.thenCombine(itemsFuture, (players, items) -> {
return new GameData(players, items);
}).thenAccept(data -> {
System.out.println("收到:" + data);
});
CompletableFuture 游戏实战
// 场景:玩家登录,需要加载多个数据
public class LoginService {
public PlayerData login(String userId) {
try {
CompletableFuture<Player> playerFuture = CompletableFuture.supplyAsync(() -> getPlayer(userId));
CompletableFuture<List<Item>> itemsFuture = CompletableFuture.supplyAsync(() -> getItems(userId));
CompletableFuture<List<Friend>> friendsFuture = CompletableFuture.supplyAsync(() -> getFriends(userId));
Player player = playerFuture.get();
List<Item> items = itemsFuture.get();
List<Friend> friends = friendsFuture.get();
return new PlayerData(player, items, friends);
} catch (Exception e) {
throw new RuntimeException("登录失败", e);
}
}
}
虚拟线程(Java 21 新特性)
传统线程的问题:
- 线程是操作系统资源,创建和切换成本高
- 一个线程占用1MB栈内存
虚拟线程(Virtual Threads):
- JVM层面的线程,不占用OS线程
- 创建成本极低(可以创建上百万个)
- 由JVM调度,不阻塞OS线程
// JDK21之前:使用平台线程
try (ExecutorService executor = Executors.newFixedThreadPool(100)) {
executor.submit(() -> {
// 任务
});
}
// JDK21+:使用虚拟线程
try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
executor.submit(() -> {
// 任务
});
}
| 对比项 | 平台线程 | 虚拟线程 |
|---|---|---|
| 创建成本 | 高(需OS调用) | 低(JVM内部) |
| 内存占用 | ~1MB栈 | ~几百字节 |
| 阻塞影响 | 阻塞OS线程 | 不阻塞OS线程 |
| 适用场景 | 计算密集型 | IO密集型 |
八、游戏服务器中的锁优化建议
// ❌ 不好的做法:高竞争下用 synchronized
synchronized (battleLock) {
battle.process(); // 战斗逻辑可能很耗时
}
// ✅ 更好的做法:
// 1. 减小锁粒度:只锁需要的部分
synchronized (battleState) {
battleState.update(); // 只锁状态更新
}
battle.process(); // 无锁处理
// 2. 用读写锁:读多写少场景
ReadWriteLock rwLock = new ReentrantReadWriteLock();
rwLock.readLock().lock(); // 读操作可以并发
rwLock.writeLock().lock(); // 写操作互斥
// 3. 用 LongAdder 替代 AtomicLong:高并发计数
LongAdder counter = new LongAdder(); // 分段CAS,减少竞争
counter.increment();
九、初学者常见错误
| 错误 | 后果 | 解决 |
|---|---|---|
| synchronized里调其他synchronized | 死锁 | 统一获取顺序 |
| 忘记unlock() | 其他线程永远等 | 用try-finally |
| 在锁里做IO操作 | 性能极差 | 缩小锁粒度 |
| 过度使用CAS | 空转浪费CPU | 竞争严重时用锁 |
| Executors创建线程池 | OOM | 手动创建,指定队列大小 |
| CompletableFuture不指定线程池 | 占满common pool | 传自定义executor |
十、自问自答
Q:AQS 的 state 为什么用 volatile + CAS 而不是直接用锁? A:因为 CAS 是无锁操作,比获取锁快得多。state 的竞争很短暂(读写一个 int),用 CAS 自旋比获取操作系统互斥锁高效得多。只有在 CAS 失败(竞争激烈)时才用 CLH 队列挂起线程。
Q:为什么游戏服务器不用 Executors.newFixedThreadPool()?
A:因为它的任务队列是 LinkedBlockingQueue(无界),可能堆积大量任务导致 OOM。游戏服务器必须用有界队列 + 合理的拒绝策略,确保系统不被压垮。
Q:偏向锁在实际项目中有用吗? A:有用但场景有限。偏向锁适合"一个线程反复获取同一把锁"的场景。在高并发游戏服务器中,锁竞争激烈,偏向锁很快升级为轻量级锁甚至重量级锁。JDK 15 默认禁用了偏向锁。
Q:volatile 能保证原子性吗?
A:不能。volatile 只保证可见性和有序性。volatile int count; count++ 仍然不安全,因为 count++ 是"读-改-写"三步操作。要保证原子性需要用 AtomicInteger 或加锁。
Q:synchronized 和 ReentrantLock 怎么选? A:能用synchronized就用synchronized(代码简洁,JVM持续优化)。需要公平锁、超时、中断或多条件变量时,用ReentrantLock。
Q:虚拟线程会替代线程池吗? A:不会完全替代。计算密集型还是需要线程池控制并发;虚拟线程主要解决IO密集型的并发问题。
实践任务
- 任务1:用 AQS 实现一个简易的不可重入锁,支持 lock/unlock
- 任务2:配置一个游戏逻辑线程池,用 JMeter 模拟 1000 并发请求,观察线程池行为
- 任务3:用
jstack抓取线程 dump,分析线程状态(BLOCKED/WAITING/TIMED_WAITING) - 任务4:写一个 volatile 可见性实验——一个线程修改 flag,另一个线程检测
- 任务5:制造一个死锁场景,用
jstack或jconsole定位死锁 - 任务6:用 CompletableFuture 实现异步加载玩家数据(基本信息、背包、好友并行加载)
与其他章节的关联
| 本章内容 | 关联章节 | 关联点 |
|---|---|---|
| 内存模型 | 第01章 JVM 深度原理 | JVM 内存模型是并发编程的基础 |
| 线程池 | 第03章 NIO 与 Netty | Netty 的 EventLoop 就是线程池 |
| 锁优化 | 第11章 游戏服务器架构 | 高并发下的锁策略选择 |
| 内存屏障 | 第09章 JVM 调优 | volatile 和锁的性能影响 |
| AQS | 第10章 高并发 | 秒杀场景用 Semaphore 限流 |
上一章:01-JVM深度原理 | 下一章:03-Java-NIO与Netty