五 AQS 与 JUC 显式锁体系
AQS:JUC 的灵魂
本质 :AbstractQueuedSynchronizer = 一个 volatile int state(资源状态) + 一个 FIFO 双向等待队列(CLH 变体) + park/unpark 阻塞恢复机制 ,用模板方法模式让你只写"怎么抢资源",其余排队/唤醒由框架代劳。
| 组成 | 说明 |
|---|---|
state |
32 位 volatile int,用 CAS 更新(早期试过 long,跨平台表现不好) |
| 同步队列 | 双向链表,借鉴 CLH;head 是 Dummy 哨兵节点(不关联线程) |
| 阻塞原语 | LockSupport.park/unpark(不是 suspend/resume) |
| 两种模式 | 独占(EXCLUSIVE)/ 共享(SHARED) |
子类需实现的方法 (默认都抛 UnsupportedOperationException):
tryAcquire / tryRelease / tryAcquireShared / tryReleaseShared / isHeldExclusively
Node 的 waitStatus 取值:
| 值 | 常量 | 含义 |
|---|---|---|
1 |
CANCELLED | 已取消(超时/中断) |
-1 |
SIGNAL | 后继节点待唤醒(释放锁时要 unpark 它) |
-2 |
CONDITION | 在条件队列里等 |
-3 |
PROPAGATE | 共享模式下唤醒需向后传播 |
0 |
--- | 初始状态 |
ReentrantLock vs synchronized
| 维度 | synchronized |
ReentrantLock |
|---|---|---|
| 层面 | JVM 关键字,自动释放 | JDK API,必须 finally 手动 unlock |
| 可重入 | ✅ | ✅(state++/state--) |
| 可中断 | ❌ | ✅ lockInterruptibly() |
| 超时放弃 | ❌ | ✅ tryLock(timeout) |
| 公平性 | 仅非公平 | ✅ new ReentrantLock(true) 公平锁 |
| 条件变量 | 1 个(WaitSet) | 多个 newCondition() |
| 读写分离 | ❌ | 配合 ReentrantReadWriteLock |
java
lock.lock();
try { /* 临界区 */ } finally { lock.unlock(); } // ❗unlock 必须放 finally
非公平锁加锁流程:
lock() → CAS(state 0→1) 成功? → setExclusiveOwnerThread(自己),结束
│ 失败
▼
acquire(1) → tryAcquire → addWaiter(EXCLUSIVE) 入队尾
→ acquireQueued 死循环:
前驱是 head 且 tryAcquire 成功 → 出队、设自己为 head
否则 shouldParkAfterFailedAcquire 把前驱 waitStatus 改成 -1
再次失败 → park() 阻塞
六个源码细节:
- 非公平体现在 :
nonfairTryAcquire里state==0时直接 CAS 抢 ,完全不看队列里有没有人排队 ;公平锁则先hasQueuedPredecessors()。 - 是否需要 unpark 取决于前驱节点的
waitStatus == SIGNAL(-1),而不是本节点状态。 shouldParkAfterFailedAcquire第一次返回 false(把前驱改成 -1),第二次才可能返回 true 真正 park。- 解锁必须
state减到 0 才算释放成功,重入一次就要 unlock 一次。 unparkSuccessor时从队列尾部向前找最近的有效节点(因为入队时 next 指针可能还没连好)。- 不可打断模式 下,被 interrupt 的线程会继续待在队列里抢锁,抢到后才通过
selfInterrupt()补上中断;可打断模式lockInterruptibly()则在 park 中被打断时直接抛InterruptedException并取消排队。
Condition(条件变量):
- 每个 Condition 一条独立的条件队列(
firstWaiter/lastWaiter) await()= 建Node.CONDITION(-2)入条件队列 →fullyRelease一次性释放全部重入的 state → parksignal()= 把条件队列首节点转移到同步队列尾部,它还要重新抢锁- 中断模式常量 :
THROW_IE(-1)抛异常,REINTERRUPT(1)重设中断标记
ReentrantReadWriteLock
核心:state 高 16 位 = 读锁计数,低 16 位 = 写锁计数 (sharedCount(c) = c >>> 16,exclusiveCount(c) = c & 0xFFFF)。
| 组合 | 是否并发 |
|---|---|
| 读-读 | ✅ 并发 |
| 读-写 | ❌ 互斥 |
| 写-写 | ❌ 互斥 |
三条必背结论:
- 读锁不支持条件变量 (
newCondition()抛异常) - 不支持锁升级 :持有读锁再去拿写锁 → 死锁
- 支持锁降级:持有写锁 → 拿读锁 → 释放写锁(保证数据可见性的常用手法)
java
// 锁降级示例(保证本线程修改后的数据对后续读可见)
w.lock();
try {
data = update(); // 写
r.lock(); // 降级:先拿读锁
} finally { w.unlock(); } // 再放写锁
try { use(data); } finally { r.unlock(); }
StampedLock(JDK 8)
三种模式:写锁 / 悲观读锁 / 乐观读。
java
long stamp = lock.tryOptimisticRead(); // 乐观读,不加锁
int cur = x, curY = y;
if (!lock.validate(stamp)) { // 期间有没有人写过?
stamp = lock.readLock(); // 有 → 升级为悲观读
try { cur = x; curY = y; } finally { lock.unlockRead(stamp); }
}
特点 :不可重入 、不支持条件变量 ;乐观读完全无锁,读极多写极少时性能最好。
三把"工具锁"对比
| 工具 | 核心 | 典型用途 | 可重用 |
|---|---|---|---|
Semaphore |
permits 即 state,acquire/release |
限流、资源池、连接池 | ✅ |
CountDownLatch |
计数递减,await() 阻塞到 0 |
主线程等 N 个子任务完成 | ❌ 一次性 |
CyclicBarrier |
线程 await() 到齐后放行 |
多线程分阶段同步(如多人加载完再开局) | ✅ reset() |
提醒 :
CyclicBarrier构造时设置的数量必须与实际线程数一致,否则永远凑不满,达不到"人满发车"的效果。
CountDownLatch 与 CyclicBarrier 的本质区别:前者是一个/少数线程等别人(事件计数),后者是线程之间互相等(线程计数)且可循环使用。
六 无锁:CAS 与原子类
CAS 本质
CAS(Compare And Swap) = compareAndSet(expect, update):内存值等于期望值才更新,否则失败重试。底层是 CPU 原子指令 cmpxchg(多核下加 lock 前缀锁总线/缓存行)。
两个前提:
- CAS 必须在多核 CPU 下使用 ,且最好是 CPU 核数 ≥ 线程数------单核下自旋重试纯属空转。
- CAS 要配合
volatile读,才能拿到最新值。
CAS 的三大问题:
| 问题 | 表现 | 解法 |
|---|---|---|
| ABA | 值从 A→B→A,CAS 以为没变过 | AtomicStampedReference(带版本号)/ AtomicMarkableReference(带 boolean 标记) |
| 自旋开销 | 高竞争下 CPU 空转 | LongAdder 分段;或退化为锁 |
| 只能操作一个变量 | 无法原子更新多个字段 | 封装成对象用 AtomicReference |
原子类全家桶
| 类别 | 代表 | 用途 |
|---|---|---|
| 原子基本类型 | AtomicInteger / AtomicLong / AtomicBoolean |
单变量原子更新 |
| 原子引用 | AtomicReference<T> |
对象引用原子更新 |
| 带版本号引用 | AtomicStampedReference<T> |
解决 ABA (getStamp()) |
| 带标记引用 | AtomicMarkableReference<T> |
只关心"是否被改过" |
| 原子数组 | AtomicIntegerArray / AtomicLongArray / AtomicReferenceArray |
保护数组元素(不是数组本身) |
| 字段更新器 | AtomicIntegerFieldUpdater / AtomicReferenceFieldUpdater |
对对象的某个 volatile 字段 原子更新(省内存;字段必须 volatile、不能是 static final) |
| 原子累加器 | LongAdder / DoubleAdder(JDK 8) |
高并发计数场景首选 ,远快于 AtomicLong |
LongAdder 为什么快 ------分段 + 伪共享
AtomicLong:所有线程 CAS 同一个 value → 竞争激烈,大量失败重试
LongAdder :base + Cell[] 数组 → 不同线程打散到不同 Cell,最后 sum() 求和
- 无竞争时直接累加
base;有竞争才创建CounterCell[](初始 2 个,竞争激烈再扩容)。 - 伪共享(False Sharing) :CPU 缓存行 64 字节 ,多个 Cell 挨在一起会互相失效 → 用
@sun.misc.Contended注解填充(需配合-XX:-RestrictContended)。 - 代价:
sum()结果不保证强一致 (累加过程中可能有并发写),适合计数统计而非精确控制。
选型 :追求精确 (如库存、序列号)用 AtomicLong;追求吞吐 (如 QPS 统计、词频统计)用 LongAdder。
Unsafe 与 VarHandle
Unsafe:objectFieldOffset()/compareAndSwapInt()等,所有原子类的底层实现;getUnsafe()受类加载器限制,通常用反射获取。- 【版本边界】JDK 9+ 官方推荐
VarHandle(MethodHandles.lookup().findVarHandle(...)),支持compareAndSet、getAndAdd以及更细的内存屏障语义 (getVolatile/setOpaque/setRelease等),是Unsafe的替代品。
乐观 vs 悲观
| 方案 | 实现 | 适合 |
|---|---|---|
| 悲观互斥 | synchronized / Lock |
冲突频繁、临界区大 |
| 乐观重试 | CAS 自旋(如 AccountCas 用 AtomicInteger + while(true) compareAndSet) |
冲突少、临界区小(如余额扣减) |
七、不可变、享元与线程安全设计
不可变(Immutable)
实现要点 :类 final(防子类破坏)、字段 private final、不提供 setter、构造期间不 this 逃逸、内部可变对象做保护性拷贝。
提醒 :"不可变类"在单个线程里是线程安全的,但在多线程里不能自动保证线程安全 ------因为引用本身的可变性 不被控制(例如
AtomicReference<String>里的引用仍可能被改)。不可变保证的是"对象内容不变",不保证"引用不变"。
典型:
| 类 | 说明 |
|---|---|
String |
不可变,天然线程安全 |
LocalDateTime / DateTimeFormatter |
不可变,可共享 |
SimpleDateFormat |
❌ 非线程安全 (内部共享 Calendar),必须每次 new 或放 ThreadLocal |
享元模式(Flyweight):复用有限对象
| 类型 | 缓存范围 |
|---|---|
Byte / Short / Long |
-128 ~ 127 |
Integer |
默认 -128 ~ 127 ,上限可用 -XX:AutoBoxCacheMax= 或 -Djava.lang.Integer.IntegerCache.high= 调 |
Character |
0 ~ 127 |
Boolean |
TRUE / FALSE |
经典应用:数据库连接池。讲义给出两种实现:
- wait/notify 版 :
AtomicIntegerArray states(0 空闲 / 1 繁忙) +synchronized + wait/notify - Semaphore 版(更优) :
new Semaphore(poolSize)控制许可 + CAS 标记状态,性能和可读性更好
生产别手写:关系库用 HikariCP / Druid ,通用对象池用 Apache Commons Pool。
无状态类
不含成员变量的类(如典型的无状态 Servlet / Controller)天然线程安全。
八 线程池(面试第一大考点)
七大参数
java
new ThreadPoolExecutor(
int corePoolSize, // 1 核心线程数(即使空闲也保留,除非 allowCoreThreadTimeOut)
int maximumPoolSize, // 2 最大线程数
long keepAliveTime, // 3 非核心线程空闲存活时间
TimeUnit unit, // 4 时间单位
BlockingQueue<Runnable> workQueue, // 5 任务阻塞队列
ThreadFactory threadFactory, // 6 线程工厂(务必自定义线程名!)
RejectedExecutionHandler handler // 7 拒绝策略
);
执行流程(重点)
提交任务
│
├─ 运行中线程数 < corePoolSize ? ──是──> 创建核心线程执行
│ │否
│ ▼
├─ 工作队列未满 ? ──是──> 入队等待
│ │否
│ ▼
├─ 线程数 < maximumPoolSize ? ──是──> 创建非核心(救急)线程执行
│ │否
│ ▼
└────────────────> 执行拒绝策略
易错 :队列满了才会创建非核心线程,不是"线程数到 core 就立刻扩到 max"。
ctl 字段 :一个 AtomicInteger,高 3 位存线程池状态 (RUNNING / SHUTDOWN / STOP / TIDYING / TERMINATED),低 29 位存工作线程数------用一次 CAS 同时维护两个值。
四种拒绝策略
| 策略 | 行为 | 适用 |
|---|---|---|
AbortPolicy(默认) |
抛 RejectedExecutionException |
常规业务,快速失败 |
CallerRunsPolicy |
让提交任务的线程自己执行 | 重要任务、天然削峰(反馈式降速) |
DiscardPolicy |
静默丢弃 | 无所谓的任务 |
DiscardOldestPolicy |
丢弃队列里最老的任务,再尝试提交 | 允许丢弃旧数据 |
为什么禁止用 Executors 创建线程池?(必考)
| 工厂方法 | 隐患 |
|---|---|
newFixedThreadPool(n) |
用无界 LinkedBlockingQueue(默认容量 Integer.MAX_VALUE)→ 任务堆积 OOM |
newCachedThreadPool() |
核心 0、最大 Integer.MAX_VALUE,SynchronousQueue → 线程数暴涨 OOM |
newSingleThreadExecutor() |
同样无界队列 |
newScheduledThreadPool(n) |
最大线程数 Integer.MAX_VALUE,同样有 OOM 风险 |
结论 :生产必须 new ThreadPoolExecutor(...) 显式指定有界队列、线程名、拒绝策略。
线程数怎么定?(有公式)
-
CPU 密集型 :
线程数 ≈ CPU 核数 + 1(+1 用于应对页缺失等暂停时顶上) -
I/O 密集型(通用公式):
线程数 = CPU核数 × 期望CPU利用率 × (CPU计算时间 + 等待时间) / CPU计算时间
例:4 核,CPU 占 50%、等待 50%,期望 100% 利用 →
4 × 100% × 100% / 50% = 8例:4 核,CPU 占 10%、等待 90% →
4 × 100% × 100% / 10% = 40
- 笔记补充:CPU 空闲(等待)时间越长,应创建的线程数越多。
- 实践 :先按公式给初值,再压测 + 监控 调整;IO 密集可配动态线程池(如 Hippo4j / Dynamic TP)。
优雅关闭与异常处理
| 方法 | 行为 |
|---|---|
shutdown() |
平缓关闭:不再接新任务,执行完队列中的任务 |
shutdownNow() |
立刻:中断所有线程,返回队列中未执行的任务 |
异常处理的坑:
execute()提交:异常会打印堆栈 并且该线程直接死亡(但线程池会补一个新线程)submit()提交:异常被封装进Future,不调用get()就永远看不到("异常被吞")- 正确做法 :任务内部
try-catch兜底 + 记录日志;或用ThreadFactory设置UncaughtExceptionHandler
线程池的"饥饿"问题
现象:固定大小线程池里,任务 A 需要等待它提交的子任务 B 的结果,如果所有线程都在等 B,B 永远得不到线程执行 → 死锁。
newFixedThreadPool(2):两个工人都去"点餐",点完餐要等"做菜"的结果 → 没人做菜 → 饥饿
解法 :不同任务类型使用不同线程池 (服务员池 + 厨师池),这也是生产上"线程池隔离"的依据。
Fork/Join 与工作窃取(JDK 7)
java
class AddTask extends RecursiveTask<Integer> { // 有返回值用 RecursiveTask,无返回值用 RecursiveAction
public Integer compute() {
if (end - begin == 1) return end + begin;
int mid = (begin + end) / 2;
AddTask t1 = new AddTask(begin, mid).fork();
AddTask t2 = new AddTask(mid + 1, end).fork();
return t1.join() + t2.join();
}
}
new ForkJoinPool(4).invoke(new AddTask(1, 10)); // 55
工作窃取(Work-Stealing) :每个线程有自己的双端队列,空闲线程从其他队列尾部"偷"任务执行,提高 CPU 利用率。
ScheduledThreadPoolExecutor vs Timer
| 维度 | Timer |
ScheduledThreadPoolExecutor |
|---|---|---|
| 线程模型 | 单线程,串行 | 多线程池 |
| 异常 | 一个任务抛异常 → 整个 Timer 线程死掉 | 互不影响 |
| 时间基准 | 依赖绝对系统时间(改系统时间会出问题) | 相对时间 |
| 结论 | ❌ 已废弃 | ✅ 推荐 |
java
// 每周四 18:00 执行(用 Duration 精确计算首次延迟)
long initDelay = Duration.between(now, nextThursday18).toMillis();
executor.scheduleAtFixedRate(task, initDelay, 7*24*3600*1000, TimeUnit.MILLISECONDS);
// scheduleAtFixedRate:以上一次"开始时间"为基准(固定频率)
// scheduleWithFixedDelay:以上一次"结束时间"为基准(固定间隔)
CompletableFuture(JDK 8,异步编排)
java
CompletableFuture.supplyAsync(() -> remoteCall(), computePool) // 有返回值
.thenApply(r -> transform(r)) // 同步转换
.thenAcceptAsync(r -> save(r), ioPool) // 异步消费(换池)
.exceptionally(e -> { log(e); return null; }); // 异常兜底
CompletableFuture.allOf(f1, f2, f3).join(); // 等全部完成
- 默认使用
ForkJoinPool.commonPool()(共用池,易互相干扰,建议显式传自定义池) thenApplyvsthenApplyAsync:前者可能复用同一线程,后者一定提交给线程池- 【版本边界】JDK 21+ IO 密集场景可以不用 CompletableFuture 编排------直接"每请求一条虚拟线程 + 同步代码"更简单
未完待续