# 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();
}

流程:

  1. tryAcquire()尝试获取锁
  2. 失败则addWaiter()加入队列尾部
  3. acquireQueued()在队列中自旋/阻塞等待
  4. 前驱是头节点且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:制造一个死锁场景,用 jstackjconsole 定位死锁
  • 任务6:用 CompletableFuture 实现异步加载玩家数据(基本信息、背包、好友并行加载)

与其他章节的关联

本章内容 关联章节 关联点
内存模型 第01章 JVM 深度原理 JVM 内存模型是并发编程的基础
线程池 第03章 NIO 与 Netty Netty 的 EventLoop 就是线程池
锁优化 第11章 游戏服务器架构 高并发下的锁策略选择
内存屏障 第09章 JVM 调优 volatile 和锁的性能影响
AQS 第10章 高并发 秒杀场景用 Semaphore 限流

上一章:01-JVM深度原理 | 下一章:03-Java-NIO与Netty