目录
JUC(java.util.concurrent)并发编程总结
[一、核心认知:JUC 到底是什么](#一、核心认知:JUC 到底是什么)
[1.1 本质定位](#1.1 本质定位)
[1.2 JUC 整体体系](#1.2 JUC 整体体系)
[2.1 核心原理:CAS](#2.1 核心原理:CAS)
[2.2 常用原子类](#2.2 常用原子类)
[2.3 AtomicInteger 实战与源码解析](#2.3 AtomicInteger 实战与源码解析)
[2.4 LongAdder 为什么更快](#2.4 LongAdder 为什么更快)
[三、锁体系:比 synchronized 更灵活](#三、锁体系:比 synchronized 更灵活)
[3.1 Lock 接口总览](#3.1 Lock 接口总览)
[3.2 ReentrantLock 重入锁](#3.2 ReentrantLock 重入锁)
[公平锁 vs 非公平锁](#公平锁 vs 非公平锁)
[3.3 ReentrantReadWriteLock 读写锁](#3.3 ReentrantReadWriteLock 读写锁)
[3.4 ReentrantLock 和 synchronized 对比](#3.4 ReentrantLock 和 synchronized 对比)
[4.1 CountDownLatch:倒计时门闩](#4.1 CountDownLatch:倒计时门闩)
[4.2 CyclicBarrier:循环栅栏](#4.2 CyclicBarrier:循环栅栏)
[4.3 CountDownLatch vs CyclicBarrier](#4.3 CountDownLatch vs CyclicBarrier)
[4.4 Semaphore:信号量](#4.4 Semaphore:信号量)
[5.1 为什么要用线程池](#5.1 为什么要用线程池)
[5.2 ThreadPoolExecutor 七大核心参数](#5.2 ThreadPoolExecutor 七大核心参数)
[5.3 线程池执行流程](#5.3 线程池执行流程)
[5.4 四种拒绝策略](#5.4 四种拒绝策略)
[5.5 Executors 自带线程池为什么不推荐](#5.5 Executors 自带线程池为什么不推荐)
[5.6 自定义线程池最佳实践](#5.6 自定义线程池最佳实践)
[6.1 ConcurrentHashMap](#6.1 ConcurrentHashMap)
[6.2 CopyOnWriteArrayList](#6.2 CopyOnWriteArrayList)
[6.3 其他并发容器](#6.3 其他并发容器)
[七、阻塞队列 BlockingQueue](#七、阻塞队列 BlockingQueue)
[7.1 核心特性](#7.1 核心特性)
[7.2 四组操作方法](#7.2 四组操作方法)
[7.3 常用实现类对比](#7.3 常用实现类对比)
[八、底层基石:AQS 抽象队列同步器](#八、底层基石:AQS 抽象队列同步器)
[8.1 什么是 AQS](#8.1 什么是 AQS)
[8.2 核心思想](#8.2 核心思想)
[8.3 两种模式](#8.3 两种模式)
[8.4 为什么 AQS 这么重要](#8.4 为什么 AQS 这么重要)
[1. 锁相关踩坑](#1. 锁相关踩坑)
[2. 线程池踩坑](#2. 线程池踩坑)
[3. 并发容器踩坑](#3. 并发容器踩坑)
[4. 通用最佳实践](#4. 通用最佳实践)
JUC(java.util.concurrent)并发编程总结
基于 JDK 1.8 标准 API 整理,覆盖核心工具、底层原理、实战用法与踩坑
一、核心认知:JUC 到底是什么
1.1 本质定位
JUC 就是 java.util.concurrent 包的简称,是 JDK 官方提供的并发编程工具集 。在 JUC 出现之前,Java 做并发只能靠 synchronized、wait()、notify() 这些底层原语,写起来麻烦、功能单一、容易出错。JUC 把并发开发中常用的锁、原子类、线程池、同步工具、并发容器、阻塞队列等能力全部封装好,让开发者不用自己从零实现并发控制,大大降低了并发编程的门槛。
它解决的核心痛点:
synchronized是独占锁,不支持读写分离、不支持尝试获取锁、不支持中断等待- 手动写
wait/notify很容易写错,导致假唤醒、死锁 - 没有统一的线程池管理,手动创建线程容易失控
- 没有现成的并发容器,手动写线程安全集合容易出问题
- 没有同步工具类,多线程协作逻辑要自己反复造轮子
1.2 JUC 整体体系
整个 JUC 包可以分成六大核心模块:
- 原子类 :
java.util.concurrent.atomic包下,基于 CAS 实现无锁原子操作 - 锁体系 :
locks包下,Lock接口、重入锁、读写锁等,基于 AQS 实现 - 同步工具 :
CountDownLatch、CyclicBarrier、Semaphore等,用于多线程协作 - 线程池 :
Executor体系,统一管理线程生命周期,复用线程 - 并发容器 :
ConcurrentHashMap、CopyOnWriteArrayList等线程安全集合 - 阻塞队列 :
BlockingQueue体系,用于生产者消费者场景、线程池任务队列
二、原子类:无锁原子操作
2.1 核心原理:CAS
原子类能实现线程安全,核心靠 CAS(Compare And Swap,比较并交换),这是一种无锁并发方案。
- 执行更新操作时,先拿到旧值,真正写入前比较当前内存值是不是和旧值一致
- 一致就把新值写进去,返回成功
- 不一致就说明中间被别的线程改过了,返回失败,一般会自旋重试
CAS 是 CPU 级别的原子指令,全程没有加锁,性能远高于锁,特别适合并发量不极端高的热点数据更新。
ABA 问题 :CAS 只看值有没有变,不看值的变化过程。如果一个值从 A 改成 B,又改回 A,CAS 会认为它没变过,但实际已经被修改过。解决办法是加版本号,每次修改版本号加一,比较的时候连版本号一起比,JDK 提供了 AtomicStampedReference 就是干这个的。
2.2 常用原子类
| 类型 | 代表类 | 作用 |
|---|---|---|
| 基本类型 | AtomicInteger、AtomicLong、AtomicBoolean | 原子更新基本类型 |
| 引用类型 | AtomicReference、AtomicStampedReference | 原子更新对象引用,带版本号解决 ABA |
| 数组类型 | AtomicIntegerArray、AtomicLongArray | 原子更新数组里的元素 |
| 对象属性更新 | AtomicIntegerFieldUpdater | 原子更新对象的某个字段,不用改原类 |
| 累加器 | LongAdder、DoubleAdder | 高并发下累加性能比 AtomicLong 好,适合统计计数 |
2.3 AtomicInteger 实战与源码解析
最常用的原子类,用于原子计数,比如接口访问量统计、分布式序号生成。
java
public class AtomicIntegerDemo {
// 初始值为0的原子整型
private static final AtomicInteger count = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
// 10个线程,每个加1000次
for (int i = 0; i < 10; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
// 原子自增1,等价于count++,但线程安全
count.incrementAndGet();
}
}).start();
}
Thread.sleep(2000);
// 最终结果一定是10000,不会出现并发丢失更新的问题
System.out.println(count.get());
}
}
底层源码拆解(incrementAndGet)
java
// Unsafe是CAS的核心类,直接操作内存,所有原子类都靠它
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
// Unsafe里的核心方法,自旋+CAS
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
// 从内存中拿到当前值
var5 = this.getIntVolatile(var1, var2);
// CAS尝试更新,成功就返回旧值,失败就循环重试
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
本质就是自旋 CAS:拿值 → 尝试更新 → 失败就再拿再试,直到成功为止。并发不高的时候很快,冲突特别激烈的时候,大量线程自旋会浪费 CPU。
2.4 LongAdder 为什么更快
高并发下大量线程同时 CAS 一个变量,冲突严重,很多线程空转重试,性能会下降。 LongAdder 做了优化:把一个值拆成多个 Cell 单元格,每个线程去更新自己对应的 Cell,最后求和的时候把所有 Cell 加起来。这样大大降低了冲突概率,并发越高优势越明显。 代价是数据有短暂的弱一致性,求和不一定完全精准,适合统计计数、监控指标这种不需要绝对精确的场景。
三、锁体系:比 synchronized 更灵活
3.1 Lock 接口总览
Lock 是 JUC 锁的顶层接口,最常用的实现是 ReentrantLock(重入锁)和 ReentrantReadWriteLock(读写锁)。
和 synchronized 相比,Lock 的优势:
- 可以尝试获取锁,拿不到就返回,不会一直死等
- 可以响应中断,等待锁的过程中可以被打断
- 支持公平锁和非公平锁切换
- 支持读写分离,读多写少场景性能更好
- 可以绑定多个条件变量,精确唤醒不同的线程
3.2 ReentrantLock 重入锁
和 synchronized 一样,都是可重入的:同一个线程可以多次获取同一把锁,不会自己把自己锁住。
基础用法
java
public class ReentrantLockDemo {
// 创建重入锁,默认是非公平锁
private final Lock lock = new ReentrantLock();
private int count = 0;
public void add() {
// 加锁,和synchronized进入代码块等价
lock.lock();
try {
// 业务代码放try里
count++;
} finally {
// 必须在finally里手动释放锁,不然异常了锁就泄露了
lock.unlock();
}
}
}
注意:Lock 必须手动解锁,而且一定要写在 finally 里,不然代码抛异常了锁永远不会释放,别的线程永远拿不到,最终死锁。这是最容易犯的错。
尝试获取锁:tryLock
这是最实用的特性,避免线程无限阻塞:
java
public boolean tryAdd() {
// 尝试获取锁,等3秒还拿不到就放弃,返回false
try {
if (lock.tryLock(3, TimeUnit.SECONDS)) {
try {
count++;
return true;
} finally {
lock.unlock();
}
}
} catch (InterruptedException e) {
// 等待过程中被中断的处理
Thread.currentThread().interrupt();
}
// 拿锁失败,返回失败,业务可以做降级处理
return false;
}
公平锁 vs 非公平锁
- 非公平锁(默认):来抢锁的线程不管队列里有没有人在等,直接先尝试抢,抢不到再去排队。优点是吞吐量高,缺点是可能出现线程饥饿,有的线程一直抢不到。
- 公平锁:严格按先来后到排队,谁等得久谁先拿。优点是公平,不会饥饿;缺点是吞吐量低,大量线程切换开销大。
java
// true就是公平锁,false非公平
Lock fairLock = new ReentrantLock(true);
3.3 ReentrantReadWriteLock 读写锁
现实中很多场景是读多写少,普通锁不管读写都独占,读的时候也不能并发读,浪费性能。 读写锁把锁分成读锁和写锁:
- 读锁:共享锁,多个线程可以同时持有读锁,并发读不互斥
- 写锁:独占锁,写的时候不能有任何读或写
- 规则:读 - 读不互斥,读 - 写互斥,写 - 写互斥
java
public class ReadWriteLockDemo {
private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
private final Lock readLock = rwLock.readLock();
private final Lock writeLock = rwLock.writeLock();
private String data;
// 读操作,加读锁,多线程可以同时读
public String read() {
readLock.lock();
try {
return data;
} finally {
readLock.unlock();
}
}
// 写操作,加写锁,同一时间只能一个写
public void write(String newData) {
writeLock.lock();
try {
this.data = newData;
} finally {
writeLock.unlock();
}
}
}
适用场景:读多写少,比如配置缓存、元数据管理,读的频率远高于写,用读写锁性能提升非常明显。
3.4 ReentrantLock 和 synchronized 对比
| 对比项 | ReentrantLock | synchronized |
|---|---|---|
| 实现层面 | JDK 层面实现的 API | JVM 层面关键字,底层靠 monitor |
| 锁释放 | 必须手动 unlock,一般放 finally | 自动释放,代码块结束或异常都自动释放 |
| 锁特性 | 支持尝试获取、可中断、公平 / 非公平切换 | 非公平,不可中断,不能尝试 |
| 锁类型 | 独占锁,也有读写锁扩展 | 独占锁,只有一种 |
| 条件变量 | 支持多个 Condition,精确唤醒 | 只有一个 waitSet,只能全部唤醒 |
| 性能 | 高并发下更好,功能灵活 | 低并发下够用,JDK1.6 后优化了很多,性能差距不大 |
| 易用性 | 容易写错,忘记释放锁会死锁 | 简单,JVM 兜底 |
总结:普通同步场景用 synchronized 就够了,简单不容易错;需要高级特性(尝试锁、可中断、公平锁、读写锁)的时候再用 ReentrantLock。
四、三大常用同步工具类
4.1 CountDownLatch:倒计时门闩
作用:一个线程等待多个线程完成任务后,自己再继续执行。相当于发令枪:所有人都到终点了,裁判才宣布比赛结束。
- 只能用一次,计数减到 0 就不能再用了
- 调用
countDown()计数减 1,调用await()阻塞等待计数归零
代码示例:等待所有子任务完成再汇总
java
public class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {
// 初始化计数器,数值为3,代表要等3个线程完成
CountDownLatch latch = new CountDownLatch(3);
for (int i = 0; i < 3; i++) {
new Thread(() -> {
try {
System.out.println(Thread.currentThread().getName() + " 执行任务");
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
// 任务完成,计数减1,必须放finally保证一定执行
latch.countDown();
}
}).start();
}
System.out.println("主线程等待所有子任务完成...");
// 主线程阻塞,直到计数变成0
latch.await();
System.out.println("所有任务完成,主线程继续执行");
}
}
典型场景:并行计算,多个线程分别算不同部分,全部算完主线程汇总结果;启动时等待多个资源初始化完成再对外提供服务。
4.2 CyclicBarrier:循环栅栏
作用:多个线程互相等待,所有线程都到达栅栏位置后,再一起继续执行。可以循环使用,重置后能反复用。
- 每个线程调用
await()就到达栅栏,阻塞等待 - 最后一个线程到达后,所有线程同时被唤醒,继续执行
- 可以设置一个栅栏动作,最后一个线程到达时自动执行
代码示例:运动员到齐了一起起跑
java
public class CyclicBarrierDemo {
public static void main(String[] args) {
// 等待3个线程,全部到齐后执行后面的Runnable
CyclicBarrier barrier = new CyclicBarrier(3, () -> {
System.out.println("所有人都到齐了,预备,跑!");
});
for (int i = 0; i < 3; i++) {
new Thread(() -> {
try {
System.out.println(Thread.currentThread().getName() + " 到达起跑线");
// 到达栅栏,等待其他人
barrier.await();
System.out.println(Thread.currentThread().getName() + " 开始跑");
} catch (InterruptedException | BrokenBarrierException e) {
Thread.currentThread().interrupt();
}
}).start();
}
}
}
4.3 CountDownLatch vs CyclicBarrier
| 区别 | CountDownLatch | CyclicBarrier |
|---|---|---|
| 等待关系 | 一个线程等多个线程 | 多个线程互相等 |
| 能否复用 | 只能用一次,计数到 0 就废了 | 可以循环使用,重置后反复用 |
| 计数方式 | 调用 countDown 减 1 | 调用 await 计数加 1,到阈值释放 |
| 额外动作 | 没有内置回调 | 支持设置栅栏动作,最后一个线程到达时执行 |
| 异常处理 | 中断抛 InterruptedException | 栅栏破了抛 BrokenBarrierException |
4.4 Semaphore:信号量
作用:控制同时访问某个资源的线程数量,相当于限流。有 N 个许可,线程拿一个许可才能执行,执行完归还许可,许可用完了其他线程就阻塞等。
代码示例:限流器,最多 3 个线程同时执行
java
public class SemaphoreDemo {
public static void main(String[] args) {
// 初始化3个许可,最多3个线程同时执行
Semaphore semaphore = new Semaphore(3);
for (int i = 0; i < 10; i++) {
new Thread(() -> {
try {
// 获取一个许可,拿不到就阻塞
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + " 获得许可,开始执行");
Thread.sleep(2000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
// 释放许可
semaphore.release();
System.out.println(Thread.currentThread().getName() + " 释放许可");
}
}).start();
}
}
}
典型场景:接口限流、数据库连接池控制、资源访问并发数控制。
五、线程池:线程复用管理
5.1 为什么要用线程池
手动 new Thread 弊端很多:
- 每次创建销毁线程开销大,频繁创建销毁性能差
- 线程数量不受控,高并发下创建几百上千个线程,内存和 CPU 都扛不住
- 不好管理,没法统一监控、统计、调优
线程池的核心思想就是线程复用:提前创建好一批线程,任务来了直接用线程执行,执行完不销毁,继续等下一个任务。既降低了创建销毁开销,又能控制并发数量。
5.2 ThreadPoolExecutor 七大核心参数
这是面试必考点,也是自定义线程池的基础。
java
public ThreadPoolExecutor(
int corePoolSize, // 1. 核心线程数
int maximumPoolSize, // 2. 最大线程数
long keepAliveTime, // 3. 空闲线程存活时间
TimeUnit unit, // 4. 存活时间单位
BlockingQueue<Runnable> workQueue, // 5. 任务队列
ThreadFactory threadFactory, // 6. 线程工厂
RejectedExecutionHandler handler // 7. 拒绝策略
)
逐个解释:
- 核心线程数:线程池长期保留的线程数,即使空闲也不会销毁,除非设置了允许核心线程超时。
- 最大线程数:线程池最多能创建的线程总数,核心线程 + 非核心线程。
- 空闲存活时间:非核心线程空闲超过这个时间就会被销毁。核心线程默认不回收。
- 时间单位:配合存活时间使用,秒、毫秒等。
- 任务队列:核心线程都忙的时候,新任务放到队列里等待。
- 线程工厂:创建线程的工厂,可以自定义线程名、是否守护线程、优先级等,方便排查问题。
- 拒绝策略:线程和队列都满了,新任务来的时候怎么处理。
5.3 线程池执行流程
新任务提交到线程池,执行顺序是:

记忆顺序:核心线程 → 队列 → 非核心线程 → 拒绝策略。很多人会搞错,以为先开非核心线程再进队列,实际是先进队列,队列满了才开非核心。
5.4 四种拒绝策略
| 策略 | 行为 | 适用场景 |
|---|---|---|
| AbortPolicy(默认) | 直接抛出 RejectedExecutionException 异常 | 大多数场景,异常及时暴露 |
| CallerRunsPolicy | 由提交任务的主线程自己执行这个任务 | 不能丢任务,流量平稳的场景 |
| DiscardPolicy | 直接丢掉任务,什么都不做 | 不重要的任务,丢了也无所谓 |
| DiscardOldestPolicy | 丢掉队列里最老的任务,把新任务加进去 | 最新任务最重要的场景 |
5.5 Executors 自带线程池为什么不推荐
JDK 提供了 Executors 工具类快速创建线程池,但《阿里巴巴开发规范》明确禁止用它创建线程池,原因:
- FixedThreadPool 和 SingleThreadPool :队列用的是无界的
LinkedBlockingQueue,任务多了会无限堆积,内存溢出。 - CachedThreadPool:最大线程数是 Integer.MAX_VALUE,高并发下会创建无数线程,系统卡死。
- ScheduledThreadPool:同样是无界队列,容易 OOM。
生产环境必须用 ThreadPoolExecutor 手动指定参数,根据业务场景设置合理的核心线程数、队列大小、最大线程数、拒绝策略。
5.6 自定义线程池最佳实践
java
public class ThreadPoolUtil {
private static final ThreadPoolExecutor EXECUTOR;
static {
// 核心线程数根据业务来:CPU密集型设为CPU核数,IO密集型设为2*CPU核数
int coreSize = Runtime.getRuntime().availableProcessors() * 2;
EXECUTOR = new ThreadPoolExecutor(
coreSize,
coreSize * 2,
60,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(500),
// 自定义线程工厂,给线程起名字,排查问题方便
new ThreadFactory() {
private final AtomicInteger threadNum = new AtomicInteger(1);
@Override
public Thread newThread(Runnable r) {
return new Thread(r, "biz-pool-" + threadNum.getAndIncrement());
}
},
// 自定义拒绝策略,记录日志+降级处理
new ThreadPoolExecutor.CallerRunsPolicy()
);
// 允许核心线程超时回收,长期空闲可以释放资源
EXECUTOR.allowCoreThreadTimeOut(true);
}
public static void execute(Runnable task) {
EXECUTOR.execute(task);
}
}
六、并发容器
6.1 ConcurrentHashMap
线程安全的 HashMap,是并发场景下最常用的集合。
演进历程
- JDK 1.7:分段锁(Segment),把整个 Map 分成 16 段,每段一把锁,不同段之间可以并发写。缺点是分段粒度还是大,同一段内还是串行,并发度最高 16。
- JDK 1.8 :废弃分段锁,改用 CAS + synchronized,锁的粒度是每个数组元素(每个桶)。没有哈希冲突的时候用 CAS 放元素,有冲突或者扩容的时候才加 synchronized 锁。粒度更细,并发性能更好,同时结构和普通 HashMap 保持一致,都是数组 + 链表 + 红黑树。
关键特性
- 不允许 null 键和 null 值,和 HashMap 不一样
- 迭代器是弱一致性的,遍历过程中数据修改不会抛并发修改异常,但遍历的是某个时间点的快照,不一定能看到最新修改
- size 计算是估算值,不是绝对精确,并发场景下没有绝对准确的 size
6.2 CopyOnWriteArrayList
写时复制的 ArrayList,核心思想:
- 读操作完全不加锁,直接读原数组,性能很高
- 写操作的时候,复制一份新数组,在新数组上修改,改完把数组引用指向新数组
- 写的时候加锁,防止并发写复制多份
优点:读性能极高,完全无锁,适合读多写极少的场景
缺点:写的时候要复制数组,内存开销大;数据有延迟,写的内容不能立刻被读到,弱一致性
适用场景:配置列表、黑白名单、订阅关系,读非常多,写非常少,允许短暂不一致的场景。
6.3 其他并发容器
- ConcurrentSkipListMap:线程安全的跳表实现,有序,对应 TreeMap 的并发版
- ConcurrentLinkedQueue:无界非阻塞并发队列,CAS 实现,高并发性能好
- BlockingQueue:阻塞队列,下面单独讲
七、阻塞队列 BlockingQueue
7.1 核心特性
阻塞队列是特殊的队列,支持阻塞式的插入和移除:
- 队列满的时候,插入元素的线程会阻塞,直到队列有空位
- 队列空的时候,取出元素的线程会阻塞,直到队列有元素
天生适合生产者消费者模式,也是线程池任务队列的核心。
7.2 四组操作方法
| 操作方式 | 抛出异常 | 返回特殊值 | 阻塞等待 | 超时等待 |
|---|---|---|---|---|
| 插入 | add(e) | offer(e) | put(e) | offer(e, time, unit) |
| 移除 | remove() | poll() | take() | poll(time, unit) |
| 检查队首 | element() | peek() | - | - |
7.3 常用实现类对比
| 实现类 | 底层结构 | 是否有界 | 特点 | 适用场景 |
|---|---|---|---|---|
| ArrayBlockingQueue | 数组 | 有界,创建时必须指定大小 | 固定容量,公平 / 非公平模式 | 固定容量的生产者消费者、线程池队列 |
| LinkedBlockingQueue | 链表 | 默认无界,也可指定容量 | 吞吐量比数组版高,头尾两把锁 | 大多数阻塞场景,FixedThreadPool 用的就是它 |
| SynchronousQueue | 不存储元素,直接传递 | 容量为 0 | 来了任务必须有线程接,不然就阻塞 | CachedThreadPool 用的就是它,直接递交给线程 |
| DelayQueue | 优先级队列 | 无界 | 元素必须实现 Delayed 接口,到时间才能取出 | 定时任务、延迟消息、超时订单关闭 |
| PriorityBlockingQueue | 优先级队列 | 无界 | 按优先级出队,不是先进先出 | 需要按优先级处理任务的场景 |
八、底层基石:AQS 抽象队列同步器
8.1 什么是 AQS
AQS 全称 AbstractQueuedSynchronizer,是 JUC 里几乎所有锁和同步工具的底层框架。ReentrantLock、CountDownLatch、Semaphore、ReentrantReadWriteLock 全都是基于 AQS 实现的。
它把并发同步的通用逻辑全部抽出来了:
- 用一个
volatile int state变量表示同步状态 - 用一个 CLH 变体的双向链表做等待队列,抢不到锁的线程排到队列里
- 提供了独占模式和共享模式两种同步方式
- 子类只需要实现几个简单的方法,就能轻松实现自己的同步器
8.2 核心思想
抢锁失败的线程,不会一直自旋浪费 CPU,而是封装成节点放到等待队列里挂起,锁释放的时候再唤醒队列里的下一个线程。

队列是双向链表,每个节点保存线程引用、等待状态、前驱后继指针。
- 入队:新节点加到队尾,用 CAS 保证原子性
- 出队:头节点释放锁后,唤醒下一个节点的线程去抢锁
8.3 两种模式
- 独占模式 :同一时间只能有一个线程持有锁,比如 ReentrantLock。代表方法
acquire()、release()。 - 共享模式 :多个线程可以同时持有锁,比如 Semaphore、读写锁的读锁。代表方法
acquireShared()、releaseShared()。
8.4 为什么 AQS 这么重要
它是 JUC 的骨架,把所有同步器通用的「排队、阻塞、唤醒、状态管理」逻辑都封装好了,上层工具不用重复实现。理解了 AQS,再看各种锁和同步工具,本质都是在操作 state 变量:
- ReentrantLock:state=0 表示锁空闲,state>0 表示被占用,重入一次加 1
- CountDownLatch:state 初始是计数,countDown 一次减 1,减到 0 就释放所有等待线程
- Semaphore:state 是剩余许可数,acquire 减 1,release 加 1
九、踩坑与实践
1. 锁相关踩坑
- ReentrantLock 忘记 unlock:必须放 finally,不然异常了锁泄露,最终死锁
- 锁重入次数不匹配:加了几次锁就要解几次锁,多解会抛异常,少解锁不释放
- 读写锁写锁饥饿:读特别多的时候,写锁一直抢不到,会饥饿。可以用公平模式缓解
- synchronized 锁范围太大 / 太小:太大并发差,太小不安全,刚好包住共享变量操作就行
2. 线程池踩坑
- 用 Executors 创建线程池:无界队列 / 无线程上限,生产环境必炸,一定要手动 new ThreadPoolExecutor
- 核心线程数设置不合理:CPU 密集型(计算多)设为 CPU 核数;IO 密集型(数据库、网络调用多)设为 2*CPU 核数,当然洛需要根据压测调整,毕竟只是理论数据,还得看实际情况
- 线程池不关闭:程序退出线程池还在,导致进程关不掉。优雅停机要调用 shutdown ()
- 任务异常吞掉:execute 提交的任务抛异常会导致线程销毁,最好自己在任务里 try-catch 处理
3. 并发容器踩坑
- ConcurrentHashMap 不是万能的:单个操作是原子的,但多步组合操作不是,比如先判断存在再更新,这两步之间可能被别的线程改,需要自己加锁或者用 compute 等原子方法
- CopyOnWrite 写太多性能差:写一次复制一次数组,写多了内存和性能都扛不住,只适合读多写极少
- 遍历集合时修改:并发容器不会抛并发修改异常,但不代表就可以随便边遍历边改,结果是不可预期的
4. 通用最佳实践
- 优先用工具类,不要自己手写 wait/notify,很容易写错
- 优先降低锁粒度,能锁代码块就别锁整个方法,能锁局部变量就别锁整个对象
- 能无锁就不用锁,原子类能解决的就别用锁
- 并发场景下优先考虑最终一致,不要强求强一致,性能差很多
- 给线程起有意义的名字,排查问题的时候一眼就能认出是哪个池的线程
- 避免死锁:固定加锁顺序,避免循环依赖;设置锁超时时间;尽量不要嵌套加锁
十、复习速记
- JUC 六大模块:原子类、锁、同步工具、线程池、并发容器、阻塞队列。
- 原子类核心:CAS + 自旋,CPU 级原子指令,无锁并发;高并发计数用 LongAdder 性能更好。
- 锁体系:ReentrantLock 灵活可中断、支持尝试锁;读写锁适合读多写少;synchronized 简单不易错,普通场景够用。
- 三大工具:CountDownLatch 一等多,一次性;CyclicBarrier 多等一,可复用;Semaphore 限并发数量。
- 线程池七大参数:核心数、最大数、存活时间、单位、队列、线程工厂、拒绝策略;执行顺序:核心→队列→非核心→拒绝。
- ConcurrentHashMap:1.8 用 CAS + synchronized,桶粒度锁,数组 + 链表 + 红黑树,比 1.7 分段锁性能好。
- AQS 基石:state 状态变量 + CLH 等待队列,独占 / 共享两种模式,是所有锁和同步工具的底层。
- 核心原则:优先用 JDK 现成工具,不重复造轮子;锁粒度尽量小;线程池必须自定义参数。