文章

Java 并发编程进阶:从线程池到 AQS

更新于 2026/09/20牛耕田

暂存笔记,持续补充中。并发是 Java 后端面试的「分水岭」,也是线上故障的高发区。

一、JMM:Java 内存模型#

JMM 规定了多线程间共享变量的可见性规则,是一套抽象规范,不是物理内存布局。

线程A 工作内存 ←→ 主内存 ←→ 线程B 工作内存
(本地缓存) (共享变量)

三大特性:

特性含义保障手段
原子性操作不可分割synchronized、Lock、AtomicXXX
可见性一个线程修改,其他线程立即可见volatile、synchronized、final
有序性禁止指令重排影响结果volatile、synchronized、happens-before

happens-before 规则(前一个操作的结果对后一个可见):

  • 程序顺序规则:单线程内前面的代码先于后面的。
  • 监视器锁规则:解锁先于后续加锁。
  • volatile 规则:写先于读。
  • 传递性:A→B,B→C,则 A→C。
  • 线程启动/终止规则:start() 先于线程内所有操作;线程内所有操作先于 join() 返回。

二、volatile 的两大作用#

public class Flag {
private volatile boolean running = true;
public void stop() { running = false; }
public void run() {
while (running) { /* ... */ } // 若无 volatile,可能永远读不到 false
}
}
  1. 保证可见性:写操作立即刷回主内存,读操作从主内存读,并使其他 CPU 缓存行失效(MESI 协议)。
  2. 禁止指令重排:通过内存屏障实现。

注意:volatile 不保证原子性。i++ 是「读-改-写」三步,多线程下仍会丢更新,必须用 AtomicInteger 或加锁。

经典应用:双重检查锁单例

public class Singleton {
private static volatile Singleton instance; // volatile 不可省!
public static Singleton getInstance() {
if (instance == null) { // 第一次检查,避免每次都加锁
synchronized (Singleton.class) {
if (instance == null) { // 第二次检查
instance = new Singleton(); // 分三步:分配→初始化→赋值
}
}
}
return instance;
}
}

若没有 volatile,new 的三步可能重排为「分配 → 赋值 → 初始化」,其他线程可能拿到未初始化完毕的对象。

三、synchronized 原理与锁升级#

字节码层面:同步代码块用 monitorenter / monitorexit 指令;同步方法用方法访问标志 ACC_SYNCHRONIZED。

对象头 Mark Word 记录了锁状态,锁升级路径(不可逆):

无锁 → 偏向锁 → 轻量级锁(CAS 自旋)→ 重量级锁(OS 互斥量)
锁状态适用场景开销
偏向锁同一线程反复进入几乎无(JDK 15 起默认禁用)
轻量级锁竞争少、交替执行CAS 自旋,无用户态/内核态切换
重量级锁竞争激烈涉及系统调用,线程阻塞挂起

JDK 15 起偏向锁默认关闭(-XX:-UseBiasedLocking),因为撤销偏向锁的 STW 代价在无竞争场景下得不偿失。

四、CAS 与 ABA 问题#

CAS(Compare And Swap):compareAndSwap(oldVal, newVal),由 CPU 的 cmpxchg 指令保证原子性。

public class Counter {
private final AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet(); // 内部就是 CAS 自旋
}
}

三个问题:

  1. ABA 问题:A→B→A,CAS 检查值相等但中间已被改过。解决:AtomicStampedReference 加版本号。
  2. 自旋开销:竞争激烈时长时间自旋浪费 CPU。解决:LongAdder 分段累加(JDK 8 推荐替代 AtomicLong)。
  3. 只能保证一个变量:多变量用 AtomicReference 封装成对象。

五、AQS:并发工具类的基石#

AQS(AbstractQueuedSynchronizer) 用一个 volatile int state + CLH 双向队列,统一实现锁与同步器。

state = 0 → 空闲
state > 0 → 已占用(可重入时递增)
acquire: CAS 抢 state,失败则入队并 park 等待
release: state 减到 0,唤醒队列头结点

基于 AQS 的组件:

组件独占/共享特点
ReentrantLock独占可重入、可公平/非公平
ReentrantReadWriteLock共享(读)/独占(写)读读不互斥
Semaphore共享限流,控制并发数
CountDownLatch共享一次性,等 N 个任务完成
CyclicBarrier—可循环,等 N 个线程到齐
// CountDownLatch 典型用法:主线程等待所有子任务
CountDownLatch latch = new CountDownLatch(3);
for (int i = 0; i < 3; i++) {
new Thread(() -> {
try { doSomething(); } finally { latch.countDown(); }
}).start();
}
latch.await(5, TimeUnit.SECONDS); // 一定要设超时!

CountDownLatch vs CyclicBarrier:前者是「一个等多个」,计数归零后不可复用;后者是「多个互相等」,可循环使用。

六、线程池:生产环境必懂#

核心参数(7 个):

new ThreadPoolExecutor(
5, // corePoolSize 核心线程数
20, // maximumPoolSize 最大线程数
60L, TimeUnit.SECONDS, // keepAliveTime 非核心线程空闲存活
new ArrayBlockingQueue<>(200), // workQueue 任务队列
new ThreadFactoryBuilder().setNameFormat("order-pool-%d").build(), // 线程工厂
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
);

执行流程(重要):

提交任务
├─ 核心线程未满 → 创建核心线程执行
├─ 核心线程已满 → 任务入队列
├─ 队列已满 → 创建非核心线程
└─ 线程数达上限 → 执行拒绝策略

四种拒绝策略:

策略行为适用
AbortPolicy(默认)抛 RejectedExecutionException需要感知失败
CallerRunsPolicy调用者线程执行可降级、削峰
DiscardPolicy静默丢弃允许丢任务(如日志)
DiscardOldestPolicy丢最旧任务保留最新

⚠️ 禁止用 Executors.newFixedThreadPool() / newCachedThreadPool():

  • newFixedThreadPool 用无界队列 LinkedBlockingQueue(容量 Integer.MAX_VALUE),任务堆积会导致 OOM。
  • newCachedThreadPool 最大线程数为 Integer.MAX_VALUE,可能创建海量线程导致 OOM。
  • 必须手写 ThreadPoolExecutor,明确队列容量与拒绝策略。

参数怎么定:

  • CPU 密集型:核心数 = CPU 核数 + 1。
  • IO 密集型:核心数 = CPU 核数 × (1 + 平均等待时间 / 平均计算时间),经验值 2N ~ 4N。
  • 最终以压测为准。

七、并发容器与 ThreadLocal#

容器机制
ConcurrentHashMapJDK 8:CAS + synchronized 锁单个桶
CopyOnWriteArrayList写时复制,读多写少场景
BlockingQueue生产者-消费者模型的基础

ThreadLocal 与内存泄漏:

Thread → ThreadLocalMap → Entry(弱引用 Key, 强引用 Value)
  • Key 是弱引用,GC 后 Key 变 null,但 Value 是强引用,导致 Entry 无法回收。
  • 必须用 try-finally 手动 remove():
private static final ThreadLocal<User> CURRENT_USER = new ThreadLocal<>();
public void handle() {
try {
CURRENT_USER.set(user);
// 业务逻辑
} finally {
CURRENT_USER.remove(); // 不写这行,线程池复用时会串数据 + 内存泄漏
}
}

线程池 + ThreadLocal = 双重陷阱:线程被复用,上一个请求的数据会残留给下一个请求,造成用户数据串号这种严重事故。

八、实战避坑清单#

  1. 加锁顺序一致,否则死锁。
  2. 缩小锁粒度,不要锁整个方法(尤其别锁 IO 操作)。
  3. 避免在锁内调用外部方法(可能被阻塞)。
  4. wait() 必须写在 while 循环里,防止虚假唤醒。
  5. 线程池必须给线程命名,否则 jstack 时全是 pool-1-thread-1,排查困难。
  6. 用 CompletableFuture 编排异步任务,比手动 Future.get() 更灵活:
CompletableFuture.supplyAsync(() -> queryUser(id), executor)
.thenCombine(CompletableFuture.supplyAsync(() -> queryOrder(id), executor),
(user, orders) -> buildVO(user, orders))
.exceptionally(e -> { log.error("聚合失败", e); return null; })
.join();

九、小结#

  • JMM 三特性:原子、可见、有序。volatile 保可见与有序,不保原子。
  • 锁升级:偏向 → 轻量 → 重量,理解 Mark Word 是基础。
  • AQS 是 ReentrantLock、Semaphore、CountDownLatch 的共同底座。
  • 线程池:手写 ThreadPoolExecutor,明确队列容量与拒绝策略,按 CPU/IO 类型估算核心数。
  • ThreadLocal:用完必 remove(),线程池下尤其致命。
返回主页