主题
二、Java 并发编程(22 题)
2.1 ★★创建线程有哪几种方式?Runnable 和 Callable 有什么区别?实际开发中推荐使用哪种方式?
📖 点击查看答案
创建线程的方式:
- 继承 Thread 类:重写 run()
- 实现 Runnable 接口:传给 Thread 构造器
- 实现 Callable 接口:配合 FutureTask 获取返回值
- 线程池:
ExecutorService.submit()/execute() - CompletableFuture:
CompletableFuture.supplyAsync()
java
// 方式1:继承 Thread
class MyThread extends Thread {
public void run() { System.out.println("run"); }
}
new MyThread().start();
// 方式2:Runnable
new Thread(() -> System.out.println("run")).start();
// 方式3:Callable + FutureTask
FutureTask<String> ft = new FutureTask<>(() -> "result");
new Thread(ft).start();
String result = ft.get();
// 方式4:线程池
ExecutorService pool = Executors.newFixedThreadPool(4);
Future<String> f = pool.submit(() -> "result");Runnable vs Callable:
| 维度 | Runnable | Callable |
|---|---|---|
| 方法 | run() | call() |
| 返回值 | 无 | 有 |
| 异常 | 不能抛出受检异常 | 可以抛出受检异常 |
| 引入版本 | JDK 1.0 | JDK 1.5 |
实际推荐:线程池
- 优点:复用线程、控制并发数、统一管理、避免频繁创建销毁
- 阿里规约:禁止直接 new Thread / 用 Executors,必须用
ThreadPoolExecutor显式创建
java
ThreadPoolExecutor executor = new ThreadPoolExecutor(
8, 16, 60L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(1000),
new ThreadFactoryBuilder().setNameFormat("biz-%d").build(),
new ThreadPoolExecutor.CallerRunsPolicy()
);
Future<String> f = executor.submit(() -> doSomething());- 异步编排用
CompletableFuture.supplyAsync(task, executor)
2.2 ★★★★线程的生命周期(状态)有哪些?各状态之间如何转换?BLOCKED 和 WAITING 状态有什么区别?
📖 点击查看答案
Java 线程的 6 种状态(Thread.State 枚举):
NEW → RUNNABLE → (TERMINATED)
↑↓
BLOCKED
↑
WAITING / TIMED_WAITING| 状态 | 说明 |
|---|---|
| NEW | 已创建但未调用 start() |
| RUNNABLE | 已调用 start(),可能正在执行或等待 CPU 调度 |
| BLOCKED | 等待获取 synchronized 锁 |
| WAITING | 调用了无超时的 wait()/join()/LockSupport.park() |
| TIMED_WAITING | 调用了带超时的 sleep(t)/wait(t)/join(t) |
| TERMINATED | run() 执行完毕 |
状态转换:
NEW → RUNNABLE:调用start()RUNNABLE → BLOCKED:进入 synchronized 块但锁被占用RUNNABLE → WAITING:调用wait()/join()/LockSupport.park()RUNNABLE → TIMED_WAITING:sleep(ms)/wait(ms)/join(ms)/parkNanosBLOCKED/WAITING/TIMED_WAITING → RUNNABLE:被唤醒或获得锁RUNNABLE → TERMINATED:run() 返回
BLOCKED vs WAITING 区别:
| 维度 | BLOCKED | WAITING |
|---|---|---|
| 触发 | 等待 synchronized 锁 | wait/join/LockSupport.park |
| 唤醒 | 锁释放后自动竞争 | 需要被 notify/notifyAll/unpark |
| 是否持有锁 | 不持有(在等) | 之前调用 wait() 时已释放锁 |
| 何时进入 | synchronized 入口处 | 调用 wait() 等方法后 |
重要细节:
- Java 层没有"运行中"和"就绪"的区分,统一叫 RUNNABLE(因为取决于操作系统调度)
- 调用
Thread.sleep()不会释放任何锁 - 调用
wait()必须先持有该对象的锁,且会释放锁 LockSupport.park()会阻塞线程但不释放锁(与 wait 不同),用于 AQS 等场景- I/O 阻塞(如 socket read)在 Java 层也是 RUNNABLE(操作系统层面是阻塞)
Blocked 场景示例:
java
synchronized (lock) {
// 线程 A 持有 lock
// 线程 B 尝试 synchronized(lock) → 进入 BLOCKED
}Waiting 场景示例:
java
synchronized (lock) {
lock.wait(); // 释放 lock,进入 WAITING
// 被 notify 后重新竞争锁,竞争失败进入 BLOCKED
}2.3 ★★线程中断机制的原理是什么?interrupt()、interrupted()、isInterrupted() 三个方法的区别是什么?
📖 点击查看答案
中断机制原理:
- Java 中断是协作式的:interrupt() 只是设置中断标志,不强制停止线程
- 被中断的线程自己决定如何响应(退出、抛异常、忽略)
- 设计目的:避免强制停止导致数据不一致、资源泄漏
三个方法对比:
| 方法 | 所属 | 作用 | 是否清除标志 |
|---|---|---|---|
interrupt() | Thread 实例 | 设置中断标志 | 否(设置标志) |
isInterrupted() | Thread 实例 | 查询中断状态 | 否 |
interrupted() | Thread 静态 | 查询当前线程中断状态 | 是(清除) |
java
Thread t = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
// 执行任务
}
// 退出循环,做清理
});
t.start();
t.interrupt(); // 请求中断特殊情况:阻塞方法响应中断
Thread.sleep()、Object.wait()、Thread.join()、LockSupport.park()等会抛InterruptedException,并清除中断标志
java
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// 此时中断标志已被清除!
// 若要保留中断状态,需重新设置:
Thread.currentThread().interrupt();
}正确处理 InterruptedException 的两种方式:
- 向上抛:让调用方决定如何处理
java
public void run() throws InterruptedException {
Thread.sleep(1000);
}- 恢复中断标志后退出:在不能抛异常时(如 Runnable.run)
java
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // 恢复标志
return; // 退出
}
}错误的处理方式:
java
try { Thread.sleep(1000); }
catch (InterruptedException e) {
// 吞掉异常是反模式!丢失了中断信号
}为什么不能用 Thread.stop():
- 已废弃,强制停止可能导致锁未释放、数据不一致
- 中断机制是更安全的替代方案
2.4 ★★★★synchronized 的底层实现原理?锁升级过程(偏向锁、轻量级锁、重量级锁)是怎样的?JDK 15 之后偏向锁发生了什么变化?
📖 点击查看答案
synchronized 底层实现:
- 字节码层:同步代码块用
monitorenter/monitorexit;同步方法在方法表 flags 中标记ACC_SYNCHRONIZED - JVM 层:基于 Monitor 对象实现,每个对象头中存有指向 Monitor 的指针
- Monitor 本质是 C++ 的 ObjectMonitor,内部维护 _owner、_EntryList、_WaitSet
对象头(Mark Word)布局(JDK 8,64 位):
| 锁状态 | 25 bit | 31 bit | 1 bit | 4 bit | 1 bit | 2 bit |
|---|---|---|---|---|---|---|
| 无锁 | unused | hashCode | unused | 分代年龄 | 0 | 01 |
| 偏向锁 | Thread ID (54) + Epoch (2) | 分代年龄 | 1 | 01 | ||
| 轻量级锁 | 指向栈中 Lock Record 的指针 | 00 | ||||
| 重量级锁 | 指向 ObjectMonitor 的指针 | 10 | ||||
| GC 标记 | - | 11 |
锁升级过程:
无锁:对象刚创建,Mark Word 为无锁状态
偏向锁:
- 第一个线程访问时,CAS 把 Thread ID 写入 Mark Word
- 之后同一线程进入同步块,只需对比 Thread ID,无需 CAS
- 适合单线程反复进入同步块的场景
轻量级锁:
- 出现两个线程交替竞争(无实质性竞争)时,偏向锁撤销,升级为轻量级锁
- 在当前线程栈帧中创建 Lock Record,CAS 把 Mark Word 复制到 Lock Record
- 成功则获取锁,失败则自旋重试
- 适合临界区很短的场景
重量级锁:
- 自旋失败(超过阈值)或竞争激烈时升级
- 申请 ObjectMonitor,未获取锁的线程进入 _EntryList 阻塞(OS 层互斥量)
- 性能开销大(用户态→内核态切换)
**锁升级是单向的:**无锁 → 偏向锁 → 轻量级锁 → 重量级锁(但偏向锁可批量撤销/重偏向)
JDK 15 之后偏向锁变化:
- JDK 15 开始废弃偏向锁(JEP 374),默认关闭
- JDK 18 正式移除相关代码
- 原因:
- 偏向锁的实现复杂度高,维护成本大
- 现代应用多线程竞争更频繁,偏向锁收益下降
- 偏向锁撤销(safepoint)会引发 STW
- CAS 已经足够轻量
- 参数
-XX:+UseBiasedLocking已无效
关键点:
- synchronized 是可重入的(Monitor 计数器)
- synchronized 是非公平锁
- JDK 6 之后 synchronized 性能大幅优化,与 ReentrantLock 差距缩小
2.5 ★★★★synchronized 和 ReentrantLock 的区别是什么?各自的底层实现是什么?什么时候选择 ReentrantLock?
📖 点击查看答案
核心区别:
| 维度 | synchronized | ReentrantLock |
|---|---|---|
| 性质 | JVM 关键字 | JDK 类(java.util.concurrent.locks) |
| 实现 | Monitor(对象头) | AQS(CAS + CLH 队列) |
| 释放 | 自动(出代码块) | 手动(必须 finally unlock) |
| 公平性 | 非公平 | 可选公平/非公平 |
| 可中断 | 不可中断(不响应 interrupt) | lockInterruptibly() 可中断 |
| 超时 | 不支持 | tryLock(timeout) 支持 |
| 条件变量 | 一个(wait/notify) | 多个 Condition |
| 锁绑定 | 对象 | Lock 对象 |
| 锁状态 | 无法查询 | isLocked()、getHoldCount() |
| 公平选择 | 不可 | 构造参数选择 |
底层实现:
- synchronized:基于对象头的 Mark Word,升级到重量级锁时用 ObjectMonitor(OS Mutex)
- ReentrantLock:基于 AQS
state变量记录锁被重入次数- 抢锁用 CAS 修改 state
- 抢锁失败封装为 Node 入队(CLH 变种),
LockSupport.park()阻塞
ReentrantLock 的使用模式:
java
ReentrantLock lock = new ReentrantLock(); // 默认非公平
lock.lock();
try {
// 临界区
} finally {
lock.unlock(); // 必须在 finally
}可中断、超时尝试:
java
lock.lockInterruptibly(); // 可被 interrupt 打断
if (lock.tryLock()) { ... } // 尝试一次
if (lock.tryLock(3, TimeUnit.SECONDS)) { ... } // 超时尝试多 Condition:
java
Lock lock = new ReentrantLock();
Condition notFull = lock.newCondition();
Condition notEmpty = lock.newCondition();
// 经典:生产者-消费者(ArrayBlockingQueue 实现)何时选择 ReentrantLock:
- 需要公平锁
- 需要可中断的锁等待
- 需要超时获取锁
- 需要多个条件变量(如生产者/消费者分离等待)
- 需要尝试获取锁(避免死锁)
- 需要复杂的同步控制(读写锁、StampedLock 底层也是 AQS)
何时用 synchronized:
- 简单同步,不需要上述高级特性
- 不会忘记释放锁(避免死锁)
- JDK 6+ 性能已接近 ReentrantLock,且 JVM 持续优化
2.6 ★★公平锁与非公平锁的区别是什么?ReentrantLock 默认是哪种?底层是如何实现的?
📖 点击查看答案
核心区别:
- 公平锁:多个线程按申请锁的顺序获取锁(FIFO),先到先得
- 非公平锁:新线程直接尝试插队抢锁,失败再排队
ReentrantLock 默认是非公平锁:
java
public ReentrantLock() { sync = new NonfairSync(); } // 默认非公平
public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); }性能差异:
- 非公平锁吞吐量更高(默认)
- 原因:刚释放锁的线程很可能再次获取,避免线程切换开销
- 公平锁需要维护队列顺序,每次获取锁要检查队列是否有前驱线程,开销大
底层实现(基于 AQS):
- 非公平锁的
tryAcquire:
java
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) { // 直接 CAS,不看队列
setExclusiveOwnerThread(current);
return true;
}
} else if (current == getExclusiveOwnerThread()) {
setState(c + acquires); // 重入
return true;
}
return false;
}- 公平锁的
tryAcquire:
java
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() && // 多了一步:检查队列前驱
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
} else if (current == getExclusiveOwnerThread()) {
setState(c + acquires);
return true;
}
return false;
}hasQueuedPredecessors 的作用:
- 检查 CLH 队列中是否有当前线程之前的等待线程
- 如果有,当前线程不能插队,必须排队
何时用公平锁:
- 严格不能饿死某个线程(如交易系统)
- 响应时间敏感
- 但吞吐量要求不高
ReentrantReadWriteLock 同样默认非公平:
java
public ReentrantReadWriteLock() { this(false); }2.7 ★★StampedLock 相比 ReadWriteLock 有什么优势?它的乐观读模式是如何工作的?
📖 点击查看答案
ReadWriteLock 的痛点:
- 读锁会阻塞写锁(写饥饿问题)
- 读锁之间不互斥,但读多写少时,写线程可能长时间拿不到锁
- 读写不能相互升级
StampedLock 的优势:
- 提供乐观读模式:读操作不加锁,仅做版本校验
- 写锁和悲观读锁互斥,但乐观读不阻塞写
- 吞吐量显著高于 ReadWriteLock(读多写少场景)
三种模式:
- 写锁(Writing):独占,
writeLock()返回 stamp,unlockWrite(stamp)释放 - 悲观读锁(Reading):共享,
readLock()返回 stamp,unlockRead(stamp)释放 - 乐观读(Optimistic Reading):无锁,
tryOptimisticRead()返回 stamp,validate(stamp)校验
乐观读工作流程:
java
StampedLock sl = new StampedLock();
double x, y;
public double distanceFromOrigin() {
long stamp = sl.tryOptimisticRead(); // 1. 乐观读,返回版本号
double curX = x, curY = y; // 2. 读取数据(可能不一致)
if (!sl.validate(stamp)) { // 3. 校验:读期间是否有写操作
// 4. 校验失败:升级为悲观读锁,重新读取
stamp = sl.readLock();
try {
curX = x; curY = y;
} finally {
sl.unlockRead(stamp);
}
}
return Math.sqrt(curX * curX + curY * curY);
}乐观读原理:
tryOptimisticRead()返回当前 stamp(非 0),并保证此时没有写锁validate(stamp)检查 stamp 是否还有效(即读期间没有发生写)- 底层用 volatile 的 state 字段 + 位运算,无锁操作
StampedLock 的限制和注意:
- 不可重入:同一线程不能重复获取读锁,否则死锁
- 不支持 Condition:无法用
newCondition() - 不要调用
interrupt():在readLock()阻塞时调用 interrupt 会导致 CPU 飙升(已知 bug) - 谨慎使用:复杂度高于 ReentrantReadWriteLock
适用场景:
- 读多写少,且能容忍短期数据不一致
- 高并发场景下的高性能读
- 典型:缓存、坐标点等数据
2.8 ★★★★volatile 关键字的两大作用是什么?它能保证原子性吗?为什么?底层是通过什么机制实现的?
📖 点击查看答案
volatile 的两大作用:
- 保证可见性:一个线程修改 volatile 变量,其他线程立即可见
- 禁止指令重排序:编译器、CPU 不会对 volatile 变量的读写进行重排序
- 保证有序性:通过内存屏障实现 happens-before
volatile 不能保证原子性:
i++不是原子操作(读-改-写三步),volatile 修饰的i++在多线程下仍然不安全
java
volatile int i = 0;
// 多线程执行 i++ 仍然会丢失更新
// 因为 i++ 等价于:temp = i; temp = temp + 1; i = temp;- 想保证原子性:用
AtomicInteger、synchronized、LongAdder
为什么不能保证原子性:
- volatile 只保证单次读/写的原子性(如
i = 5是原子的) - 复合操作(
i++、i += 1)由多步组成,每步原子但整体不原子 - 线程 A 读 i=0,线程 B 读 i=0,A 写 i=1,B 写 i=1 → 丢失更新
典型使用场景:
java
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;
}
}- 双重检查锁定的 instance 必须 volatile,否则新建对象的过程可能被重排序:
new Singleton()实际是:① 分配内存 ② 初始化对象 ③ 引用赋值- 重排序可能变成 ①③②,其他线程在 ③ 后看到非 null 但未初始化的对象
状态标志位场景:
java
private volatile boolean running = true;
public void run() {
while (running) { /* 工作 */ }
}
public void shutdown() { running = false; }底层实现机制:
- 基于**内存屏障(Memory Barrier)**实现
- x86 架构:
- volatile 写之前插入
StoreStore屏障,之后插入StoreLoad屏障 - volatile 读之后插入
LoadLoad和LoadStore屏障
- volatile 写之前插入
- 汇编层面:volatile 写会生成
lock addl $0,0,(%rsp)指令(lock 前缀)- lock 前缀会:
- 锁定缓存行(或总线)
- 强制将缓存行数据写回主存
- 通过 MESI 缓存一致性协议使其他 CPU 的缓存失效
- lock 前缀会:
JMM 视角:
- volatile 写 happens-before volatile 读
- 配合 happens-before 规则的传递性,可实现线程间通信
2.9 ★★★★CAS(Compare And Swap)的原理是什么?ABA 问题是什么?Java 中如何解决 ABA 问题?
📖 点击查看答案
CAS 原理:
- CAS(V, Expected, New):如果 V 的当前值等于 Expected,则更新为 New,返回 true;否则什么都不做,返回 false
- 整个操作是原子的,由 CPU 指令保证(x86 的
cmpxchg) - 是乐观锁的核心实现
java
AtomicInteger ai = new AtomicInteger(0);
boolean success = ai.compareAndSet(0, 1); // 期望0,更新为1CAS 底层(Unsafe 类):
java
public final native boolean compareAndSwapInt(Object o, long offset, int expected, int x);
// HotSpot 实现最终调用 CPU 的 cmpxchg 指令- JDK 9+ 提供
VarHandle替代 Unsafe
ABA 问题:
- 线程1 读取值 A
- 线程2 把 A 改成 B,又改回 A
- 线程1 CAS(A → C) 成功,但中间的变化被忽略了
ABA 问题的危害:
- 对于纯数值更新(如计数器)通常无害
- 对于基于对象状态的操作(如链表节点复用)可能导致逻辑错误:
- 线程1 准备从链表弹出 A(A.next=B),CAS(head, A, B)
- 线程2 弹出 A、B,又把 A 重新压入(head=A, A.next=C)
- 线程1 CAS 成功,head=B,但 B 已被弹出,链表损坏
解决 ABA:AtomicStampedReference / AtomicMarkableReference
java
AtomicStampedReference<Integer> ref = new AtomicStampedReference<>(100, 0);
// 初始值 100,初始版本号 0
int[] stampHolder = new int[1];
Integer value = ref.get(stampHolder); // 获取值和版本号
int stamp = stampHolder[0];
// 线程2 即使把值改回 100,版本号也变了
ref.compareAndSet(100, 200, stamp, stamp + 1); // 必须传版本号- AtomicMarkableReference:用 boolean 标记代替版本号
CAS 的其他问题:
- 自旋开销大:竞争激烈时 CAS 反复失败,CPU 空转
- 只能保证单个变量原子:多个变量需用锁或 AtomicReference 包装成对象
CAS 的应用:
AtomicInteger、AtomicLong、AtomicReference等原子类ConcurrentHashMap等并发容器- AQS 中的 state 修改
LongAdder用分段 CAS 减少竞争
2.10 ★★乐观锁和悲观锁的区别是什么?各自的应用场景有哪些?
📖 点击查看答案
核心思想:
- 悲观锁:假定会发生冲突,访问数据前先加锁
- 乐观锁:假定不会冲突,操作时不加锁,提交时检查冲突
对比:
| 维度 | 悲观锁 | 乐观锁 |
|---|---|---|
| 思想 | 先加锁再操作 | 操作后校验 |
| 并发性 | 低(串行) | 高(无锁) |
| 性能 | 冲突多时好 | 冲突少时好 |
| 实现 | synchronized、ReentrantLock、数据库行锁 | CAS、版本号机制 |
| 适用 | 写多读少、冲突频繁 | 读多写少、冲突少 |
悲观锁场景:
- 数据库行锁:
select ... for update - Java 锁:synchronized、ReentrantLock
- 临界区竞争激烈:如秒杀扣库存
- 数据一致性要求高:金融账户
sql
-- 悲观锁:扣库存
SELECT stock FROM goods WHERE id=1 FOR UPDATE;
-- 业务判断
UPDATE goods SET stock = stock - 1 WHERE id=1;乐观锁场景:
- CAS 原子类:AtomicInteger、LongAdder
- 数据库版本号/时间戳:UPDATE 时校验版本
sql
-- 乐观锁:扣库存
SELECT stock, version FROM goods WHERE id=1;
-- 业务判断
UPDATE goods SET stock = stock - 1, version = version + 1
WHERE id=1 AND version = #{version};
-- affected rows = 0 → 重试- 无锁数据结构:ConcurrentHashMap、Disruptor
- 读多写少:缓存更新
CAS 实现乐观锁(Java):
java
AtomicInteger stock = new AtomicInteger(100);
public boolean deduct() {
int current;
do {
current = stock.get();
if (current <= 0) return false;
} while (!stock.compareAndSet(current, current - 1));
return true;
}版本号机制(业务):
java
// 实体类
class User {
Long id; String name; Integer version;
}
// 更新
userMapper.update(id, name, version); // WHERE id=? AND version=?
// 失败重试选择建议:
- 冲突少:乐观锁性能高
- 冲突多:悲观锁避免反复重试
- 临界区短:乐观锁更优
- 临界区长:悲观锁更稳
2.11 ★★★★AQS 的核心原理是什么?它的 state 变量和 CLH 队列分别起什么作用?如何实现独占锁与共享锁?
📖 点击查看答案
AQS(AbstractQueuedSynchronizer)概述:
- 是 J.U.C 的核心基类,为各种同步器提供框架
- 子类:ReentrantLock、ReentrantReadWriteLock、Semaphore、CountDownLatch、CyclicBarrier 等
- 模板方法模式:AQS 提供骨架,子类实现 tryAcquire/tryRelease 等
核心组成:
- state 变量(volatile int):
- 不同同步器赋予不同语义
- ReentrantLock:state 表示锁被重入的次数
- Semaphore:state 表示剩余许可数
- CountDownLatch:state 表示还剩多少个计数
- ReentrantReadWriteLock:state 高 16 位为读锁数量,低 16 位为写锁数量
- 修改 state 用 CAS 保证原子性
java
private volatile int state;
protected final int getState() { return state; }
protected final boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}- CLH 队列变种(FIFO 双向队列):
- 等待锁的线程封装为 Node 入队
- Node 维护前驱、后继指针、等待状态、线程引用
- 每个节点通过前驱节点的状态判断自己是否应该被唤醒
- 抢锁失败的线程被
LockSupport.park()阻塞,被前驱唤醒时unpark()
Node 状态:
- CANCELLED(1):节点已取消
- SIGNAL(-1):后继节点需要被唤醒
- CONDITION(-2):在 Condition 队列中
- PROPAGATE(-3):共享模式下传播唤醒
独占锁实现流程:
java
// acquire 模板
public final void acquire(int arg) {
if (!tryAcquire(arg) && // 1. 子类实现:尝试获取
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) // 2. 入队 + 阻塞
selfInterrupt(); // 3. 补上中断
}
// ReentrantLock 的 tryAcquire
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
} else if (current == getExclusiveOwnerThread()) {
setState(c + acquires); // 重入
return true;
}
return false;
}共享锁实现流程:
java
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0) // 子类实现:< 0 表示获取失败
doAcquireShared(arg); // 入队 + 阻塞
}
// Semaphore 的 tryAcquireShared
protected int tryAcquireShared(int acquires) {
for (;;) {
int available = getState();
int remaining = available - acquires;
if (remaining < 0 || compareAndSetState(available, remaining))
return remaining; // >= 0 成功,< 0 失败
}
}- 共享锁唤醒后会传播唤醒:被唤醒的线程获取成功后,继续唤醒后继共享节点
独占 vs 共享:
| 维度 | 独占 | 共享 |
|---|---|---|
| 同时持有者 | 1 个 | 多个 |
| 方法 | tryAcquire/tryRelease | tryAcquireShared/tryReleaseShared |
| 典型 | ReentrantLock | Semaphore、CountDownLatch、读锁 |
| 唤醒 | 只唤醒一个后继 | 传播唤醒 |
2.12 ★★★★什么是 Java 内存模型(JMM)?它和 JVM 内存结构有什么区别?happens-before 规则有哪些?
📖 点击查看答案
JMM(Java Memory Model):
- JSR-133 规范,定义多线程下变量的访问规则
- 解决:可见性、原子性、有序性问题
- 抽象模型:每个线程有自己的本地内存(CPU 缓存/寄存器抽象),共享变量存储在主内存
线程 A 线程 B
│ │
本地内存(副本) 本地内存(副本)
│ │
└────── 主内存(共享) ─────┘JMM vs JVM 内存结构:
| 维度 | JMM | JVM 内存结构 |
|---|---|---|
| 关注点 | 线程间通信、可见性 | 内存区域划分 |
| 抽象层 | 逻辑模型 | 物理区域 |
| 内容 | happens-before、volatile、synchronized 语义 | 堆、栈、方法区 |
| 用途 | 解决并发问题 | 描述 JVM 运行时数据区 |
JMM 解决的三大问题:
- 可见性:volatile、synchronized、final
- 原子性:synchronized、Lock、Atomic 类
- 有序性:volatile(禁止重排)、synchronized(互斥)
happens-before 规则(核心):
如果 A happens-before B,那么 A 的操作对 B 可见,且 A 的执行顺序先于 B。
8 大规则:
- 程序顺序规则:同一线程中,前面的操作 happens-before 后面的操作
- 监视器锁规则:unlock 操作 happens-before 后续对同一锁的 lock
- volatile 变量规则:volatile 写 happens-before 后续 volatile 读
- 线程启动规则:
Thread.start()happens-before 该线程的所有操作 - 线程终止规则:线程的所有操作 happens-before
Thread.join()返回 - 线程中断规则:
interrupt()调用 happens-before 被中断线程检测到中断 - 对象初始化规则:对象初始化完成 happens-before 其 finalize 方法
- 传递性:A happens-before B,B happens-before C → A happens-before C
示例:
java
// 线程 A
int x = 1; // (1)
volatile boolean flag = true; // (2) volatile 写
// 线程 B
if (flag) { // (3) volatile 读
int y = x; // (4) y == 1 一定成立
}
// 依据:(1) HB (2) [程序顺序],(2) HB (3) [volatile 规则],(3) HB (4) [程序顺序]
// 传递性:(1) HB (4),所以 x=1 对 y 可见内存屏障:
- LoadLoad:确保 Load1 完成后再 Load2
- LoadStore:确保 Load1 完成后再 Store2
- StoreStore:确保 Store1 完成后再 Store2
- StoreLoad:确保 Store 完成后再 Load(开销最大)
2.13 ★★★什么是死锁?产生死锁的四个必要条件是什么?如何排查和避免死锁?
📖 点击查看答案
死锁定义:
- 两个或多个线程互相持有对方需要的资源,导致永久阻塞
java
// 经典死锁
Object lock1 = new Object(), lock2 = new Object();
new Thread(() -> {
synchronized (lock1) {
try { Thread.sleep(100); } catch (Exception e) {}
synchronized (lock2) { } // 等待 lock2
}
}).start();
new Thread(() -> {
synchronized (lock2) {
try { Thread.sleep(100); } catch (Exception e) {}
synchronized (lock1) { } // 等待 lock1
}
}).start();死锁的四个必要条件(缺一不可):
- 互斥条件:资源同一时刻只能被一个线程占用
- 请求与保持:线程持有资源 A 的同时请求资源 B,不释放 A
- 不剥夺条件:资源不能被强制剥夺,只能由持有者主动释放
- 循环等待:多个线程形成头尾相接的循环等待关系
排查死锁的方法:
- jstack:打印线程堆栈,会自动检测死锁
bash
jstack <pid>
# 输出 "Found one Java-level deadlock"- jconsole / VisualVM:图形化工具,可视化线程状态
- Arthas:
thread -b查找阻塞线程,thread查看所有线程 - JMX:
ThreadMXBean.findDeadlockedThreads()
java
ThreadMXBean bean = ManagementFactory.getThreadMXBean();
long[] ids = bean.findDeadlockedThreads();- 日志和堆栈:业务日志 + thread dump
避免死锁的策略:
破坏请求与保持:一次性申请所有资源
java// 一次性获取多个锁(tryLock) if (lock1.tryLock() && lock2.tryLock()) { try { ... } finally { lock1.unlock(); lock2.unlock(); } }破坏不剥夺:使用
tryLock(timeout)超时获取,失败则释放已有锁重试破坏循环等待:按固定顺序获取锁(最常用)
java// 所有线程按 lock1 → lock2 顺序加锁避免嵌套锁:尽量减少锁的层级
使用更高级的并发工具:
- 用
ConcurrentHashMap替代 synchronized Map + 其他锁 - 用
BlockingQueue实现生产者-消费者(替代 wait/notify + 锁)
- 用
预防与检测:
- 代码评审:检查锁的顺序
- 单元测试:并发场景测试
- 上线监控:检测线程阻塞、响应超时
- 使用
tryLock(timeout)替代lock()防止永久阻塞
2.14 ★★★★线程池的核心参数有哪些?每个参数的含义是什么?任务提交后的执行流程是怎样的?
📖 点击查看答案
ThreadPoolExecutor 的 7 个核心参数:
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. 拒绝策略
);参数详解:
| 参数 | 含义 |
|---|---|
| corePoolSize | 核心线程数,即使空闲也不会回收(除非 allowCoreThreadTimeOut) |
| maximumPoolSize | 线程池最大线程数 |
| keepAliveTime | 非核心线程的空闲存活时间 |
| unit | 时间单位 |
| workQueue | 任务队列,保存等待执行的任务 |
| threadFactory | 创建线程的工厂,可自定义线程名 |
| handler | 饱和时的拒绝策略 |
常用的 workQueue:
LinkedBlockingQueue:无界(默认 Integer.MAX_VALUE),FIFOArrayBlockingQueue:有界,FIFOSynchronousQueue:无缓冲,直接交付PriorityBlockingQueue:优先级队列
四种拒绝策略:
AbortPolicy(默认):抛 RejectedExecutionExceptionCallerRunsPolicy:由提交任务的线程执行DiscardPolicy:直接丢弃DiscardOldestPolicy:丢弃队列最老的任务,再尝试提交
任务执行流程(关键):
- 提交任务到线程池
- 判断当前线程数是否小于 corePoolSize:
- 是:创建核心线程执行任务
- 否:进入下一步
- 尝试将任务加入 workQueue:
- 成功:等待执行
- 失败(队列满):进入下一步
- 判断当前线程数是否小于 maximumPoolSize:
- 是:创建非核心线程执行任务
- 否:执行拒绝策略
提交任务
│
▼
线程数 < corePoolSize? ──Y──> 创建核心线程执行
│ N
▼
队列未满? ──Y──> 入队等待
│ N
▼
线程数 < maxPoolSize? ──Y──> 创建非核心线程执行
│ N
▼
执行拒绝策略重要细节:
- 核心线程在 JDK 7+ 也是延迟创建(首次提交任务才创建)
- 非核心线程空闲超过 keepAliveTime 会被回收
- 任务队列满后才会创建非核心线程(很多人误以为是先到最大线程数才入队)
prestartAllCoreThreads()可预热核心线程
常见设置:
java
new ThreadPoolExecutor(
8, 16, 60L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(1000),
new ThreadFactoryBuilder().setNameFormat("order-pool-%d").build(),
new ThreadPoolExecutor.CallerRunsPolicy()
);2.15 ★★ThreadPoolExecutor 任务执行的完整流程是怎样的?
📖 点击查看答案
execute() 方法源码:
java
public void execute(Runnable command) {
int c = ctl.get();
// 1. 当前线程数 < corePoolSize,新增 worker
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true)) return;
c = ctl.get();
}
// 2. 队列未满,入队
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
if (!isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false); // 防止所有线程已死,任务在队列中无人执行
}
// 3. 队列满,尝试新增非核心线程
else if (!addWorker(command, false))
// 4. 失败(达到 maxPoolSize),拒绝
reject(command);
}addWorker 流程:
- CAS 增加 workerCount
- 创建 Worker 对象(封装 firstTask、ThreadFactory 创建线程)
- 加锁后将 Worker 加入 workers 集合
- 调用
thread.start()启动线程
Worker 线程的 run 流程:
java
final void runWorker(Worker w) {
Runnable task = w.firstTask;
while (task != null || (task = getTask()) != null) {
w.lock(); // Worker 自己有锁,用于 shutdown 时判断是否空闲
try {
beforeExecute(w.thread, task); // 钩子
try {
task.run();
afterExecute(task, null);
} catch (Throwable ex) {
afterExecute(task, ex);
}
} finally {
task = null;
w.completedTasks++;
w.unlock();
}
}
processWorkerExit(w, false); // 线程退出处理
}getTask()(核心线程的阻塞获取):
java
Runnable getTask() {
boolean timedOut = false;
for (;;) {
// 是否允许超时回收(核心线程也回收需要 allowCoreThreadTimeOut)
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
if (wc > maximumPoolSize && workQueue.isEmpty()) return null; // 退出
try {
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take(); // 核心线程永久阻塞
if (r != null) return r;
timedOut = true;
} catch (InterruptedException retry) {
timedOut = false;
}
}
}submit() 与 execute() 的区别:
execute(Runnable):无返回值submit(Callable/Runnable):返回 Future,内部包装成 FutureTask 提交
关键细节:
- 工作线程通过 Worker 的 AQS 实现(不可重入)实现"是否在执行任务"的判断
- shutdown 时通过 tryLock 判断线程是否空闲,空闲则中断
- 线程异常退出后会自动新增一个 worker(除非 shutdown 中)
2.16 ★★★★Executors 工厂方法创建的几种线程池各自有什么缺陷?为什么阿里巴巴开发规范不推荐使用 Executors?
📖 点击查看答案
Executors 提供的几种线程池:
newFixedThreadPool(int):固定大小线程池newSingleThreadExecutor():单线程池newCachedThreadPool():可缓存线程池newScheduledThreadPool(int):定时任务线程池newWorkStealingPool(int):JDK 8 引入,ForkJoinPool
各线程池的缺陷:
1. newFixedThreadPool
java
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}- 缺陷:队列无界(LinkedBlockingQueue 默认 Integer.MAX_VALUE)
- 风险:任务堆积导致 OOM
2. newSingleThreadExecutor
java
public static ExecutorService newSingleThreadExecutor() {
return new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}- 缺陷:同上,队列无界
- 风险:任务堆积导致 OOM
3. newCachedThreadPool
java
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}- 缺陷:最大线程数无界(Integer.MAX_VALUE)
- 风险:高并发下创建大量线程,导致 OOM(无法创建 native 线程)
4. newScheduledThreadPool
java
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
// 内部用 DelayedWorkQueue,也是无界- 缺陷:队列无界,最大线程数 Integer.MAX_VALUE
- 风险:任务堆积或线程爆炸
为什么不推荐用 Executors(阿里规约):
- 无界队列/无界线程导致 OOM 风险
- 无法自定义拒绝策略:默认 AbortPolicy 抛异常可能影响业务
- 无法自定义线程名:默认 pool-1-thread-1,排查问题困难
- 无法精细控制参数:corePoolSize、maxPoolSize、queue 容量都是固定的
正确做法:用 ThreadPoolExecutor 显式创建
java
ThreadPoolExecutor executor = new ThreadPoolExecutor(
8, // 核心线程数
16, // 最大线程数
60L, TimeUnit.SECONDS, // 空闲时间
new ArrayBlockingQueue<>(1000), // 有界队列
new ThreadFactoryBuilder()
.setNameFormat("order-pool-%d") // 业务可识别的线程名
.setUncaughtExceptionHandler(...)
.build(),
new ThreadPoolExecutor.CallerRunsPolicy() // 自定义拒绝策略
);线程数计算建议(参见 2.18):
- CPU 密集型:N + 1
- IO 密集型:2N 或 N × (1 + 等待时间/计算时间)
2.17 ★★线程池的四种拒绝策略分别是什么?在实际业务中如何选择或自定义拒绝策略?
📖 点击查看答案
四种内置拒绝策略:
| 策略 | 行为 | 适用场景 |
|---|---|---|
| AbortPolicy(默认) | 抛 RejectedExecutionException | 关键任务,必须感知到失败 |
| CallerRunsPolicy | 由提交任务的线程执行 | 不希望丢任务,能接受降速 |
| DiscardPolicy | 静默丢弃 | 允许丢失(如日志) |
| DiscardOldestPolicy | 丢弃队列最老的任务,重新提交 | 新任务优先级高(如最新数据) |
源码:
java
public static class AbortPolicy implements RejectedExecutionHandler {
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
throw new RejectedExecutionException("Task " + r.toString() + " rejected from " + e.toString());
}
}
public static class CallerRunsPolicy implements RejectedExecutionHandler {
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) r.run(); // 提交者执行
}
}
public static class DiscardPolicy implements RejectedExecutionHandler {
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { } // 啥也不做
}
public static class DiscardOldestPolicy implements RejectedExecutionHandler {
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
e.getQueue().poll(); // 丢最老的
e.execute(r); // 重试
}
}
}业务选择建议:
- 关键业务(订单、支付):自定义策略,记录日志 + 持久化 + 报警
java
public class PersistAndAlertPolicy implements RejectedExecutionHandler {
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
log.error("线程池拒绝任务: {}", r);
// 1. 持久化到 DB/MQ,后续重试
taskRepository.save((Task) r);
// 2. 触发告警
alertService.send("线程池饱和");
}
}非关键业务(日志、埋点):DiscardPolicy,直接丢弃
希望降速保护系统:CallerRunsPolicy,让调用方减速
实时数据(最新值优先):DiscardOldestPolicy,丢弃旧数据
自定义策略的常见模式:
java
// 1. 阻塞提交者(同步阻塞)
new RejectedExecutionHandler() {
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
try {
e.getQueue().put(r); // 阻塞直到队列有位置
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
}
};
// 2. 转移到 MQ
new RejectedExecutionHandler() {
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
mqProducer.send(r); // 转为异步处理
}
};注意:
- CallerRunsPolicy 会阻塞调用者,可能导致调用方雪崩
- DiscardPolicy/DiscardOldestPolicy 没有任何日志,慎用(业务无感知)
- 自定义策略务必记录日志,便于排查
2.18 ★★如何合理设置线程池大小?CPU 密集型与 IO 密集型有何区别?allowCoreThreadTimeOut 的作用是什么?
📖 点击查看答案
CPU 密集型 vs IO 密集型:
| 维度 | CPU 密集型 | IO 密集型 |
|---|---|---|
| 瓶颈 | CPU 计算 | 等待 IO(磁盘/网络/数据库) |
| CPU 利用率 | 高 | 低 |
| 线程数 | 少(=N+1) | 多(=2N 或更多) |
| 典型 | 加密、压缩、序列化、计算 | HTTP 调用、DB 查询、文件 IO |
线程数计算公式:
CPU 密集型:
N + 1(N = CPU 核数)- 多一个线程用于在偶发的页面错误等情况下不阻塞 CPU
IO 密集型:
- 经验公式:
2N - 精确公式(Brian Goetz):
N × (1 + 等待时间/计算时间) - 即
N × (1 + WT/CT),WT/CT 越大线程越多
- 经验公式:
java
// 获取 CPU 核数
int n = Runtime.getRuntime().availableProcessors();
// CPU 密集型
int cpuIntensiveSize = n + 1;
// IO 密集型
int ioIntensiveSize = 2 * n;实际调优建议:
- 不同任务用不同线程池:避免相互影响
- 先按公式估算,再压测调整:观察 CPU 利用率、吞吐量、响应时间
- 有界队列容量:根据任务平均处理时间和期望响应时间估算
- 队列容量 ≈ 线程数 × 单任务处理时间 × 期望等待时间因子
- 监控线程池指标:活跃线程数、队列大小、拒绝次数、任务平均耗时
混合型任务的处理:
- 拆分为 CPU 密集和 IO 密集两类,分别用不同线程池
- 或用 ForkJoinPool 让任务自己决定并行度
allowCoreThreadTimeOut 的作用:
java
public void allowCoreThreadTimeOut(boolean value);- 默认 false:核心线程即使空闲也不会被回收(一直阻塞在
workQueue.take()) - 设为 true:核心线程空闲超过 keepAliveTime 也会被回收
java
ThreadPoolExecutor pool = new ThreadPoolExecutor(8, 16, 60L, TimeUnit.SECONDS, queue);
pool.allowCoreThreadTimeOut(true); // 允许核心线程超时回收使用场景:
- 任务有明显波峰波谷(白天忙、晚上闲),希望低谷期释放线程资源
- 长期运行的服务,避免不必要的线程占用
- 突发流量场景,避免线程恢复慢
注意:
- 设为 true 后,新任务到来时需要重新创建线程,会有短暂延迟
- 设置过小的核心线程数可能影响响应时间
2.19 ★★★★CountDownLatch、CyclicBarrier、Semaphore 三者的区别和使用场景是什么?
📖 点击查看答案
三者对比:
| 维度 | CountDownLatch | CyclicBarrier | Semaphore |
|---|---|---|---|
| 作用 | 等待 N 个事件完成 | N 个线程互相等待 | 控制并发访问数 |
| 计数 | 减法 | 加法 | 增减 |
| 重置 | 不可重置 | 可重置(cyclic) | 可增可减 |
| 基于 | AQS 共享模式 | ReentrantLock + Condition | AQS 共享模式 |
| 场景 | 主线程等待子任务 | 多线程阶段性同步 | 限流、资源池 |
CountDownLatch(一次性倒计时):
java
CountDownLatch latch = new CountDownLatch(3);
for (int i = 0; i < 3; i++) {
new Thread(() -> {
try { doWork(); } finally { latch.countDown(); }
}).start();
}
latch.await(); // 阻塞到 count = 0
System.out.println("所有子任务完成");- 不可重置,计数到 0 后无法再用
- 典型场景:
- 主线程等待多个子任务并行执行
- 服务启动时检查多个依赖(DB、缓存、外部服务)
- 多线程初始化数据
CyclicBarrier(可循环屏障):
java
CyclicBarrier barrier = new CyclicBarrier(3, () -> {
System.out.println("所有线程到达屏障,执行汇总");
});
for (int i = 0; i < 3; i++) {
new Thread(() -> {
doWork();
barrier.await(); // 等待其他线程
doNextPhase();
}).start();
}- 可重置,可重复使用(cyclic)
- 所有线程都到达屏障后,可选执行一个 barrier action
- 典型场景:
- 多线程分阶段计算(如多阶段归并)
- 并行测试,所有线程准备好后同时开始
- 多线程对大数据分片处理,每阶段同步一次
Semaphore(信号量):
java
Semaphore permits = new Semaphore(5); // 5 个许可
for (int i = 0; i < 100; i++) {
new Thread(() -> {
try {
permits.acquire(); // 获取许可
doWork();
} finally {
permits.release(); // 释放许可
}
}).start();
}- 用于限流(限制同时访问某资源的线程数)
- 典型场景:
- 数据库连接池
- 限流器(如接口 QPS 限制)
- 资源池
区别细节:
- CountDownLatch 是"一个等多个"或"多个等一个",单向等待
- CyclicBarrier 是"多个互相等",对称等待
- CountDownLatch 计数到 0 后失效;CyclicBarrier 可以 reset 重新使用
- CountDownLatch 的 await 不要求所有线程都 await;CyclicBarrier 必须所有线程都 await
2.20 ★★★★ThreadLocal 的实现原理是什么?为什么会导致内存泄漏?如何正确使用 ThreadLocal?
📖 点击查看答案
ThreadLocal 的实现原理:
- 每个 Thread 对象内部有一个
ThreadLocalMap字段 - ThreadLocalMap 的 key 是 ThreadLocal 对象(弱引用),value 是用户数据
- 调用
threadLocal.get()时,实际是从当前线程的 ThreadLocalMap 中查找
java
// Thread 类
ThreadLocal.ThreadLocalMap threadLocals = null;
// ThreadLocalMap.Entry
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k); // key 是弱引用
value = v;
}
}set/get 流程:
java
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) map.set(this, value);
else createMap(t, value);
}
public T get() {
ThreadLocalMap map = getMap(Thread.currentThread());
if (map != null) {
Entry e = map.getEntry(this);
if (e != null) return (T) e.value;
}
return setInitialValue();
}为什么导致内存泄漏:
- ThreadLocal 的 key 是弱引用:ThreadLocal 外部强引用断开后,key 会被 GC 回收 → key=null
- 但 value 是强引用:Entry.value 一直引用着 value 对象
- 线程长期存活(线程池),ThreadLocalMap 也长期存活,导致 value 无法回收
Thread ──强──> ThreadLocalMap ──> Entry[]
│
key(弱) ──> ThreadLocal(被GC)
value(强) ──> 大对象(泄漏!)实际场景:
- 线程池中的线程长期存活
- 使用 ThreadLocal 存储大对象(如 Connection、UserContext)
- 业务代码忘记调用
remove() - key 被 GC 但 value 还在,无法访问也无法回收
正确使用 ThreadLocal:
- 使用完务必 remove(最重要)
java
ThreadLocal<User> ctx = new ThreadLocal<>();
try {
ctx.set(currentUser);
// 业务逻辑
} finally {
ctx.remove(); // 必须!
}- 不要存储大对象
- 避免共享 ThreadLocal 实例(每个线程应有独立副本)
- 使用 static final 修饰 ThreadLocal:避免被 GC 导致 key 失效
java
private static final ThreadLocal<User> USER_HOLDER = new ThreadLocal<>();- 使用 InheritableThreadLocal 实现父子线程传递(或 TransmittableThreadLocal 用于线程池场景)
ThreadLocal 的常见应用:
- 用户上下文(登录用户)
- 数据库连接、事务管理
- 日期格式化(SimpleDateFormat 非线程安全)
- 链路追踪 traceId
- MDC 日志上下文
最佳实践:
java
try {
UserContext.set(user);
log.info("处理用户 {}", UserContext.get().getId());
// 业务...
} finally {
UserContext.remove(); // 一定要 remove
}2.21 ★★CompletableFuture 有哪些核心方法?如何优雅地编排多个异步任务?thenApply、thenCompose、thenCombine 的区别?
📖 点击查看答案
CompletableFuture 概述:
- JDK 8 引入的异步编程工具,实现了 Future 和 CompletionStage
- 支持链式调用、组合、异常处理
- 比 Future 更强大:可主动完成、回调通知、组合多个任务
创建 CompletableFuture:
java
// 1. 异步执行(默认 ForkJoinPool)
CompletableFuture<Void> f1 = CompletableFuture.runAsync(() -> doSomething());
// 2. 异步执行带返回值
CompletableFuture<String> f2 = CompletableFuture.supplyAsync(() -> "result");
// 3. 指定线程池
CompletableFuture<String> f3 = CompletableFuture.supplyAsync(() -> "result", executor);
// 4. 已完成
CompletableFuture.completedFuture("done");核心方法分类:
| 类型 | 方法 | 说明 |
|---|---|---|
| 转换 | thenApply / thenApplyAsync | T → R |
| 消费 | thenAccept | T → void |
| 执行 | thenRun | () → void |
| 组合 | thenCompose | flatMap |
| 合并 | thenCombine | 合并两个 CF |
| 多任务 | allOf / anyOf | 全部/任一完成 |
| 异常 | exceptionally / handle / whenComplete | 异常处理 |
thenApply vs thenCompose vs thenCombine:
thenApply(fn):把当前结果转为另一个值(同步,类似 map)
java
CompletableFuture<Integer> f = CompletableFuture.supplyAsync(() -> "123")
.thenApply(Integer::parseInt); // String → IntegerthenCompose(fn):当前结果 → 另一个 CompletableFuture(异步,类似 flatMap)
java
CompletableFuture<String> f = CompletableFuture.supplyAsync(() -> getUser(1))
.thenCompose(user -> CompletableFuture.supplyAsync(() -> getOrders(user)));
// 返回 CompletableFuture<String>,而不是 CompletableFuture<CompletableFuture<String>>thenCombine(other, biFn):合并两个独立的 CompletableFuture 的结果
java
CompletableFuture<String> f1 = CompletableFuture.supplyAsync(() -> "price=100");
CompletableFuture<String> f2 = CompletableFuture.supplyAsync(() -> "user=A");
CompletableFuture<String> combined = f1.thenCombine(f2, (a, b) -> a + "&" + b);优雅编排示例:
java
// 查用户 + 查订单(并行)
CompletableFuture<User> userFuture = CompletableFuture.supplyAsync(() -> userService.findById(1), executor);
CompletableFuture<List<Order>> orderFuture = CompletableFuture.supplyAsync(() -> orderService.findByUserId(1), executor);
// 合并结果
CompletableFuture<UserDTO> result = userFuture.thenCombine(orderFuture, (user, orders) -> {
UserDTO dto = new UserDTO();
dto.setUser(user);
dto.setOrders(orders);
return dto;
}).exceptionally(ex -> {
log.error("查询失败", ex);
return UserDTO.empty();
});
// 链式:先查用户,再查订单,再查商品
CompletableFuture<List<Product>> future = CompletableFuture
.supplyAsync(() -> userService.findById(1), executor)
.thenComposeAsync(user -> CompletableFuture.supplyAsync(() -> orderService.findByUser(user), executor))
.thenComposeAsync(orders -> CompletableFuture.supplyAsync(() -> productService.findByOrders(orders), executor));
// 等待多个任务全部完成
CompletableFuture<Void> all = CompletableFuture.allOf(f1, f2, f3);
all.join();异步 vs 同步方法:
thenApply:默认在前一个任务完成的线程执行thenApplyAsync:提交到 ForkJoinPool(或指定线程池)异步执行推荐业务场景下显式传入业务线程池,避免占用 ForkJoinPool
异常处理:
java
future
.thenApply(...)
.exceptionally(ex -> fallback) // 仅异常时
.handle((result, ex) -> ex != null ? fallback : result) // 异常和正常都处理
.whenComplete((result, ex) -> log.info("完成: {}, 异常: {}", result, ex)); // 不修改结果2.22 ★★什么是伪共享(False Sharing)?在 Java 中如何解决?@Contended 注解的原理是什么?
📖 点击查看答案
伪共享定义:
- CPU 缓存以**缓存行(Cache Line)**为单位加载,通常 64 字节
- 多个变量位于同一缓存行时,一个线程修改变量 A 会让另一 CPU 上修改变量 B 的缓存行失效
- 导致频繁的缓存同步,性能急剧下降
示例:
java
class Counter {
volatile long x; // 8 字节
volatile long y; // 8 字节
}
// x 和 y 大概率位于同一缓存行
// 线程 A 修改 x,线程 B 修改 y,会互相让对方的缓存行失效为什么是"伪"共享:
- 两个变量逻辑上没有共享,但物理上位于同一缓存行
- CPU 缓存一致性协议(MESI)让缓存失效,造成不必要的总线通信
解决方法:
- 缓存行填充(Padding):在变量之间填充无用字段,确保不同变量在不同缓存行
java
class PaddedCounter {
volatile long x;
public long p1, p2, p3, p4, p5, p6, p7; // 填充 56 字节
volatile long y;
}- @Contended 注解(JDK 8+):JVM 自动填充
java
@jdk.internal.vm.annotation.Contended
class Counter {
volatile long x;
volatile long y;
}- 需要 JVM 参数
-XX:-RestrictContended才能生效(默认仅 JDK 内部可用) - 字段级别也可以用:
java
class Counter {
@Contended volatile long x;
@Contended volatile long y;
}- 使用 @Contended 的实际场景:
- JDK 内部:
AtomicLong、LongAdder的 Cell 类、Thread 类的随机数种子 - Disruptor 框架的 RingBuffer
- 高性能计数器
- JDK 内部:
@Contended 原理:
- 编译期:注解本身只是元数据
- 运行期:JVM 在对象布局时,为 @Contended 字段前后填充 padding(默认 128 字节)
- 字段分组:同组的字段会填充到一起,不同组之间填充
java
// 字段分组示例
class Example {
@Contended("group1") int a;
@Contended("group1") int b;
@Contended("group2") int c;
}
// a 和 b 在一起,c 单独填充LongAdder 的应用:
java
// Striped64.Cell 源码
@jdk.internal.vm.annotation.Contended
static final class Cell {
volatile long value;
Cell(long x) { value = x; }
// ...
}- LongAdder 用 Cell[] 分散计数,避免单个变量竞争
- 每个 Cell 用 @Contended 避免伪共享,性能远超 AtomicLong(高并发下)
何时关注伪共享:
- 高并发场景下对共享变量的频繁修改
- 性能优化已经做到极致
- 一般业务代码不需要关注