Java后端专题-并发与多线程(约1w字,最全!)
本文汇总了Java并发与多线程专题的核心知识点,包含线程状态与生命周期、锁机制、线程池等关键内容。主要内容包括: 线程6种状态(NEW/RUNNABLE/BLOCKED/WAITING/TIMED_WAITING/TERMINATED)及其转换条件 synchronized原理与锁升级过程(偏向锁→轻量级锁→重量级锁) ReentrantLock与synchronized的核心区别(可中断、超时、
03-并发与多线程
前言
本文件汇总专题「03-并发与多线程」,收录该目录下的所有 Markdown 原文,并提供可点击目录便于跳转查阅。
目录
- Java面试题合集-20-线程状态与生命周期.md
- 1)一句话结论
- 2)六大状态速记(先把图画出来)
- 3)RUNNABLE 为什么看起来“很笼统”?
- 4)用代码把每个状态“抓”出来(可在本机跑)
- 5)面试追问:WAITING vs BLOCKED 有啥区别?
- 6)排查建议:看到状态你应该联想到什么?
- 7)一句话收尾(面试可直接用)
- Java面试题合集-21-synchronized原理与锁升级.md
- 1)一句话结论
- 2)synchronized 锁的到底是什么?
- 3)为什么 synchronized 同时保证“互斥”和“可见性”?
- 4)锁升级:偏向锁 → 轻量级锁 → 重量级锁(讲清“为什么升级”)
- 5)面试追问:偏向锁是不是“必然存在”?
- 6)常见追问:synchronized 和 ReentrantLock 怎么选?
- 7)一句话收尾(面试可直接用)
- Java面试题合集-22-ReentrantLock与synchronized区别.md
- 1)一句话结论
- 2)最核心区别一览
- 3)一个“不会忘记关门”的 ReentrantLock 正确写法
- 4)可中断与超时:这就是它“值钱”的地方
- 5)Condition:一个锁上挂多个“等待室”
- 6)什么时候选谁?(给出清晰选型)
- 7)一句话收尾(面试可直接用)
- Java面试题合集-23-volatile能解决什么问题.md
- 1)一句话结论
- 2)volatile 解决的第一个问题:可见性
- 3)volatile 解决的第二个问题:有序性(禁止重排序)
- 4)volatile 不解决:原子性(最常见坑)
- 5)volatile 的典型使用场景(说这几个很稳)
- 6)一句话收尾(面试可直接用)
- Java面试题合集-24-CAS与ABA问题.md
- 1)一句话结论
- 2)CAS 的核心伪代码
- 3)CAS 的优点与代价(面试讲这两点很稳)
- 4)什么是 ABA?为什么是问题?
- 5)ABA 怎么解决?——给“值”加一个版本号
- 6)一句话收尾(面试可直接用)
- Java面试题合集-25-AQS原理.md
- 1)一句话结论
- 2)AQS 解决了什么“通用难题”?
- 3)AQS 的三个核心概念
- 4)你需要实现什么?(面试讲到这里很加分)
- 5)独占 vs 共享:AQS 的两种模式
- 6)面试追问:AQS 为什么用队列?不用忙等行不行?
- 7)一句话收尾(面试可直接用)
- Java面试题合集-26-线程池参数与设置.md
- 1)一句话结论
- 2)先把 ThreadPoolExecutor 的“决策流程”讲清楚
- 3)CPU 密集 vs IO 密集:核心线程数怎么估?
- 4)队列怎么选?(这比“线程数”更关键)
- 5)最大线程数与 keepAlive:峰值兜底与回收策略
- 6)线程工厂与命名:排障��备(强烈建议)
- 7)一个相对“生产友好”的线程池示例
- 8)一句话收尾(面试可直接用)
- Java面试题合集-27-线程池拒绝策略.md
- 1)一句话结论
- 2)什么时候会拒绝?(先把触发条件讲清楚)
- 3)四种内置拒绝策略(面试必背但要会解释)
- 4)生产兜底怎么做?(高质量回答)
- 5)配套三件套:没有它们拒绝策略等于裸奔
- 6)一句话收尾(面试可直接用)
- Java面试题合集-28-Future与CompletableFuture.md
- 1)一句话结论
- 2)Future 的典型用法(简单但“等得难受”)
- 3)CompletableFuture 的核心优势:可组合、可回调、可链式异常处理
- 4)并行组合:allOf / anyOf(面试最常问)
- 5)异常处理:handle / exceptionally(写出来就很加分)
- 6)线程池:不要默认“全用 commonPool”(生产建议)
- 7)一句话收尾(面试可直接用)
- Java面试题合集-29-ThreadLocal原理与内存泄漏.md
- 1)一句话���论
- 2)ThreadLocal 的“存放位置”到底在哪?
- 3)为什么 key 用弱引用?为什么还会泄漏?
- 4)最典型的事故现场:线程池复用线程
- 5)正确姿势:一定要 finally remove(面试必答)
- 6)除了内存泄漏,还有一个更隐蔽的问题:上下文串线
- 7)一句话收尾(面试可直接用)
- Java面试题合集-30-死锁定位与避免.md
- 1)一句话结论
- 2)死锁的四个必要条件(面试官爱问)
- 3)最小复现:两把锁 + 反着拿
- 4)怎么定位死锁?(生产最实用)
- 5)怎么避免死锁?(给出可执行的工程策略)
- 6)一句话收尾(面试可直接用)
- Java面试题合集-31-常用并发容器.md
- 1)一句话结论
- 2)Map:
ConcurrentHashMap(最常用) - 3)List:
CopyOnWriteArrayList(读多写少) - 4)Queue:无锁队列 vs 阻塞队列
- 5)Set:
ConcurrentSkipListSet/CopyOnWriteArraySet - 6)面试加分:并发容器也不是“万能”
- 7)一句话收尾(面试可直接用)
Java面试题合集-20-线程状态与生命周期.md
Java 线程有哪些状态?生命周期怎么流转?
面试官问线程状态,表面是在考概念,实际是在考你能不能把“线程卡住/线程不跑/线程跑不满”这些线上问题讲明白。
这篇用“状态图 + 最小可运行例子”把它讲清楚。
1)一句话结论
Java 线程在
Thread.State中有 6 种状态:NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED;常见流转由 start、获取锁、wait/join/park、sleep、以及执行结束触发。注意:Java 的RUNNABLE包含“正在运行”和“就绪等待 CPU”两种含义。
2)六大状态速记(先把图画出来)
NEW --start()--> RUNNABLE --run结束--> TERMINATED
|
| 竞争锁失败
v
BLOCKED --拿到锁--> RUNNABLE
RUNNABLE --wait()/join()/park()--> WAITING --notify/notifyAll/unpark/被join线程结束--> RUNNABLE
RUNNABLE --sleep(t)/wait(t)/join(t)/parkNanos--> TIMED_WAITING --时间到/唤醒--> RUNNABLE
3)RUNNABLE 为什么看起来“很笼统”?
在 OS 里常见状态会分:
- Ready(就绪,等 CPU)
- Running(正在跑)
但在 Java 的 Thread.State 里,这两个通常都映射成 RUNNABLE。
所以你看到线程是 RUNNABLE,不代表它此刻一定在占用 CPU,只能说明:它不在 Java 层面的等待/阻塞状态。
4)用代码把每个状态“抓”出来(可在本机跑)
4.1 NEW / RUNNABLE / TERMINATED
public class StateNewRunnableTerminated {
public static void main(String[] args) throws Exception {
Thread t = new Thread(() -> {});
System.out.println(t.getState()); // NEW
t.start();
t.join();
System.out.println(t.getState()); // TERMINATED
}
}
4.2 TIMED_WAITING:sleep
public class StateTimedWaiting {
public static void main(String[] args) throws Exception {
Thread t = new Thread(() -> {
try { Thread.sleep(10_000); } catch (InterruptedException ignored) {}
});
t.start();
Thread.sleep(100);
System.out.println(t.getState()); // TIMED_WAITING
}
}
4.3 WAITING:wait / join
public class StateWaiting {
static final Object lock = new Object();
public static void main(String[] args) throws Exception {
Thread t = new Thread(() -> {
synchronized (lock) {
try { lock.wait(); } catch (InterruptedException ignored) {}
}
});
t.start();
Thread.sleep(100);
System.out.println(t.getState()); // WAITING
}
}
4.4 BLOCKED:争夺同一把锁
public class StateBlocked {
static final Object lock = new Object();
public static void main(String[] args) throws Exception {
Thread t1 = new Thread(() -> {
synchronized (lock) {
try { Thread.sleep(10_000); } catch (InterruptedException ignored) {}
}
});
Thread t2 = new Thread(() -> {
synchronized (lock) {
System.out.println("t2 got lock");
}
});
t1.start();
Thread.sleep(100); // 确保 t1 先拿到锁
t2.start();
Thread.sleep(100);
System.out.println(t2.getState()); // BLOCKED
}
}
5)面试追问:WAITING vs BLOCKED 有啥区别?
你可以这样回答(很清晰):
BLOCKED:等锁(进入synchronized但没抢到监视器)WAITING/TIMED_WAITING:不等锁,等“某个条件/事件/时间”(比如wait/join/park/sleep)
6)排查建议:看到状态你应该联想到什么?
- 大量
BLOCKED:锁竞争严重,检查锁粒度/锁范围/是否热点锁 - 大量
WAITING:线程可能在等任务/等通知/被 join,检查是否正常设计 - 大量
TIMED_WAITING:可能在 sleep/超时等待,检查重试策略和时间设置
7)一句话收尾(面试可直接用)
Java 线程有 6 种状态:NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED;BLOCKED 是等锁,WAITING/TIMED_WAITING 是等条件/事件/时间;RUNNABLE 在 Java 里同时包含“就绪”和“运行中”。
Java面试题合集-21-synchronized原理与锁升级.md
synchronized 的底层原理是什么?锁升级过程(偏向/轻量/重量)?
你可以把 synchronized 想成“门口的一把锁”:
- 只有一个人用:就贴个“这是我的”标签(偏向锁)
- 偶尔两个人抢:用更轻的机制协商(轻量级锁)
- 很多人抢:干脆上“真正的互斥锁”,排队进门(重量级锁)
1)一句话结论
synchronized基于对象监视器(monitor)实现互斥与内存可见性;JVM 会根据竞争程度进行锁优化与升级:偏向锁(无竞争快速进入)→ 轻量级锁(CAS 自旋)→ 重量级锁(进入 monitor 阻塞),以在不同竞争场景下平衡性能与开销。
2)synchronized 锁的到底是什么?
锁的是“对象的监视器”:
synchronized (obj) {}:锁住obj关联的 monitorsynchronized实例方法:锁住thisstatic synchronized:锁住Class对象(例如Foo.class)
class Demo {
public synchronized void f() {} // 等价于 synchronized(this)
public static synchronized void g() {} // 等价于 synchronized(Demo.class)
}
3)为什么 synchronized 同时保证“互斥”和“可见性”?
面试表达可以用这句话:
进入 synchronized 相当于“获取锁”,退出相当于“释放锁”;JMM 规定释放锁前会把工作内存的修改刷新到主内存,获取锁后会使读取到的值保持可见,从而形成 happens-before 关系。
不用背术语也行,抓住:锁释放前写入对之后获得同一把锁的线程可见。
4)锁升级:偏向锁 → 轻量级锁 → 重量级锁(讲清“为什么升级”)
4.1 偏向锁(Bias Lock)
适合:大多数时候只有同一线程进入临界区。
做法:在对象头里记录“偏向的线程 ID”,后续同线程进入几乎不用 CAS。
升级触发:出现另一个线程也来抢这把锁(竞争发生),偏向锁会撤销。
4.2 轻量级锁(Lightweight Lock)
适合:短时间竞争,线程很快能拿到锁。
做法:通过 CAS 尝试把对象头替换成指向线程栈中 Lock Record 的指针,并可能自旋等待。
升级触发:竞争激烈/自旋多次仍拿不到 → 升级重量级锁。
4.3 重量级锁(Heavyweight Lock)
适合:竞争激烈或临界区较长。
做法:线程会阻塞挂起,等待唤醒(OS 层面的互斥与调度参与)。
一句话总结升级策略:
能不阻塞就不阻塞:先用“偏向/自旋”赌短临界区,赌输了再阻塞排队。
5)面试追问:偏向锁是不是“必然存在”?
不同 JDK 版本与运行参数会影响偏向锁行为(有的版本默认关闭/逐步弃用方向)。
稳妥说法:
偏向锁是 JVM 的一种优化手段,具体启用与策略受 JDK 版本和参数影响,但锁升级思想依旧成立:根据竞争程度选择不同实现以平衡开销。
6)常见追问:synchronized 和 ReentrantLock 怎么选?
这题下一篇讲更全。这里给一句“不过度承诺”的答法:
synchronized 语义简单、JVM 优化成熟,适合大多数场景;ReentrantLock 提供更丰富能力(可中断、超时、公平锁、多条件队列),需要高级控制时更合适。
7)一句话收尾(面试可直接用)
synchronized基于 monitor 实现互斥与可见性;JVM 会根据竞争从偏向锁(无竞争快)→ 轻量级锁(CAS 自旋)→ 重量级锁(阻塞等待)逐步升级,在不同场景下兼顾性能与正确性。
Java面试题合集-22-ReentrantLock与synchronized区别.md
ReentrantLock 与 synchronized 的区别?什么时候选谁?
把这俩比作“门锁”:
synchronized:出厂自带的智能门锁,简单耐用,基本够用ReentrantLock:可定制的专业门禁系统,功能多,但你得会配置、会关门
1)一句话结论
synchronized是 JVM 层面的内置锁,语法简单、自动释放、优化成熟;ReentrantLock是 JUC 提供的显式锁,支持可中断锁、超时获取、公平锁、多条件队列等高级能力,但需要手动unlock,更适合需要精细控制的并发场景。
2)最核心区别一览
| 对比点 | synchronized |
ReentrantLock |
|---|---|---|
| 获取/释放 | 进入/退出代码块自动完成 | lock()/unlock() 手动控制 |
| 异常安全 | 自动释放(不会忘) | 必须 finally 解锁(容易忘) |
| 可中断 | 不支持(等待锁不可被中断) | 支持 lockInterruptibly() |
| 超时 | 不支持 | tryLock(timeout) |
| 公平性 | 不直接提供公平锁语义 | 可选公平/非公平 |
| 条件队列 | 只有 wait/notify(monitor 级) |
Condition(可多个条件队列) |
| 性能优化 | JVM 优化成熟(锁升级等) | 基于 AQS,性能也很强 |
3)一个“不会忘记关门”的 ReentrantLock 正确写法
import java.util.concurrent.locks.ReentrantLock;
public class LockDemo {
private final ReentrantLock lock = new ReentrantLock();
private int count = 0;
public void incr() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
}
面试官常抓的坑:忘记 unlock() 会导致其他线程永久卡住。
4)可中断与超时:这就是它“值钱”的地方
4.1 可中断获取锁
lock.lockInterruptibly();
适用:线程等待锁时你希望能响应取消/中断(比如任务被撤销、应用关闭)。
4.2 超时获取锁
if (lock.tryLock(100, java.util.concurrent.TimeUnit.MILLISECONDS)) {
try {
// got lock
} finally {
lock.unlock();
}
} else {
// timeout fallback
}
适用:避免长时间卡死,做降级或快速失败。
5)Condition:一个锁上挂多个“等待室”
synchronized 的 wait/notify 只有一个隐式条件队列;ReentrantLock 可以创建多个 Condition,把不同条件的等待者分开管理。
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class ConditionDemo {
private final ReentrantLock lock = new ReentrantLock();
private final Condition notEmpty = lock.newCondition();
private final Condition notFull = lock.newCondition();
}
面试表达:
当你需要“多个条件队列”做精细唤醒时,Condition 比 wait/notify 更清晰。
6)什么时候选谁?(给出清晰选型)
优先 synchronized:
- 临界区简单
- 不需要超时/可中断/公平性/多条件队列
- 追求代码简单、异常安全
选择 ReentrantLock:
- 需要
tryLock超时、可中断获取锁 - 需要公平锁(避免长时间饥饿)
- 需要多个 Condition 做更精细的线程协作
- 需要更灵活的锁结构(比如分段锁思路)
7)一句话收尾(面试可直接用)
synchronized简单可靠且自动释放,适合大多数场景;ReentrantLock提供可中断、超时、公平锁与多 Condition 等高级能力,适合需要精细并发控制的场景,但必须 finally 解锁避免死锁。
Java面试题合集-23-volatile能解决什么问题.md
volatile 能解决什么问题?能保证原子性吗?
volatile 经常被误解成“轻量版 synchronized”。
实际上,它更像一个“公告栏”:
我写的东西必须立刻贴出来让大家看到;读的人也必须去公告栏看最新的。
但它不负责“多人同时改同一份账本时谁先谁后”的互斥问题。
1)一句话结论
volatile主要保证可见性与禁止特定重排序(提供 happens-before 关系),不保证复合操作的原子性;适合做状态标记、停止开关、单例双检锁的关键字段等,但计数自增这类读-改-写必须用锁或原子类。
2)volatile 解决的第一个问题:可见性
没有 volatile 时,一个线程可能把变量缓存在寄存器/工作内存里,另一个线程看不到最新值。
最经典例子:停止线程的开关
public class VolatileStopDemo {
static volatile boolean running = true;
public static void main(String[] args) throws Exception {
Thread t = new Thread(() -> {
while (running) {
// busy work
}
System.out.println("stopped");
});
t.start();
Thread.sleep(1000);
running = false;
}
}
如果 running 不是 volatile,循环可能“停不下来”(取决于 JVM 优化与运行时条件)。
3)volatile 解决的第二个问题:有序性(禁止重排序)
JIT/CPU 可能为了性能对指令重排。volatile 会在读写处插入内存屏障,限制某些重排,从而建立更稳定的先后关系(happens-before)。
面试不需要深入屏障细节,能说清:
volatile 写对之后的 volatile 读可见,并建立 happens-before。
就很加分。
4)volatile 不解决:原子性(最常见坑)
下面这个计数器即使 count 是 volatile,也不安全:
public class VolatileNotAtomic {
static volatile int count = 0;
static void incr() {
count++; // 读-改-写
}
}
因为 count++ 是三步:
1)读 count
2)+1
3)写回 count
两个线程可能同时读到同一个旧值,最后写回覆盖,导致丢增量。
正确做法 1:AtomicInteger
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicCounter {
static final AtomicInteger count = new AtomicInteger();
static void incr() { count.incrementAndGet(); }
}
正确做法 2:加锁
public class SyncCounter {
static int count = 0;
static synchronized void incr() { count++; }
}
5)volatile 的典型使用场景(说这几个很稳)
- 状态标记:停止开关、配置开关、是否初始化完成
- 单例 DCL 的关键字段:
volatile防止指令重排导致“半初始化对象被读到” - 读多写少的配置刷新:写线程更新,读线程实时可见
6)一句话收尾(面试可直接用)
volatile保证可见性与一定的有序性(happens-before),但不保证原子性;适合做状态标记与发布-订阅式的可见性需求,自增计数等复合操作要用锁或原子类。
Java面试题合集-24-CAS与ABA问题.md
CAS 是什么?ABA 问题怎么解决?
CAS 是并发世界的“抢座位”规则:
你想坐下(更新值),必须先确认座位还和你上次看的一样(期望值一致),否则你就得重新排队再看一眼。
它让很多并发操作避免了“先上锁再干活”的重量流程,但也带来一些经典问题,比如 ABA。
1)一句话结论
CAS(Compare-And-Swap)通过比较内存中的当前值与期望值,相等则原子更新为新值,否则失败重试;它是无锁并发的基础(Atomic 类、AQS 等大量使用)。ABA 问题指值从 A 变 B 又回到 A 导致 CAS 误判“没变”,常用版本号(
AtomicStampedReference)或标记位(AtomicMarkableReference)解决。
2)CAS 的核心伪代码
boolean cas(addr, expected, newValue) {
if (*addr == expected) {
*addr = newValue; // 原子
return true;
}
return false;
}
Java 里的 AtomicInteger.incrementAndGet() 本质就是 CAS 自旋重试:
while(true){
old = get()
new = old + 1
if (cas(old, new)) return new
}
3)CAS 的优点与代价(面试讲这两点很稳)
优点:
- 低竞争下性能好(避免线程阻塞与唤醒)
- 适合短临界区、高并发读写
代价/问题:
- 高竞争下自旋重试会浪费 CPU
- 只能保证“一个变量”的原子更新(多变量需要更复杂方案或加锁)
- ABA 问题
4)什么是 ABA?为什么是问题?
场景:
1)线程 T1 读到值 A,准备 CAS(A → C)
2)线程 T2 把值 A → B → A(中间发生过变化)
3)线程 T1 再 CAS,发现还是 A,于是成功
问题:T1 不知道中间发生过变化,这在某些业务里会产生严重逻辑错误(比如基于指针/节点的无锁结构)。
用图表示:
T1: 读到 A -------------------> CAS 看到还是 A(成功)
T2: A -> B -> A
5)ABA 怎么解决?——给“值”加一个版本号
5.1 AtomicStampedReference:值 + stamp(版本号)
思路:
- 每次更新不仅改值,还把 stamp +1
- CAS 时同时比较值和 stamp,避免“回到 A 但版本变了”被误判
你可以这样描述(面试够用):
用带版本号的 CAS:从 (A,1) 变成 (B,2) 再变成 (A,3),虽然值回到 A,但版本不同,CAS 会失败。
5.2 AtomicMarkableReference:值 + mark(标记位)
适合只需要一个布尔标记的场景(比如逻辑删除)。
6)一句话收尾(面试可直接用)
CAS 通过“比较期望值再原子更新”实现无锁并发,低竞争下高效但高竞争会自旋浪费 CPU;ABA 指值 A→B→A 导致 CAS 误判未变化,常用版本号(AtomicStampedReference)或标记位(AtomicMarkableReference)解决。
Java面试题合集-25-AQS原理.md
AQS 是什么?它在并发工具类中扮演什么角色?
如果把并发工具类比作“各种交通工具”(锁、信号量、倒计时器……),那 AQS 就是它们共用的“发动机底盘”:
你想造一把锁、一把门禁、一套排队系统?AQS 提供了排队、唤醒、状态管理这套通用骨架。
1)一句话结论
AQS(AbstractQueuedSynchronizer)是 JUC 中实现锁与同步器的基础框架,通过一个
state表示同步状态,并用 CLH 变体队列管理等待线程;ReentrantLock、Semaphore、CountDownLatch、ReentrantReadWriteLock等大量组件都基于 AQS 实现。
2)AQS 解决了什么“通用难题”?
实现一个同步器,你总会遇到这些问题:
- 怎么表示“资源是否可用”?(状态)
- 多线程抢资源失败后怎么办?(排队)
- 谁该被唤醒?什么时候唤醒?(阻塞/唤醒)
- 取消、超时、中断怎么处理?(边界条件)
AQS 把这些“通用部分”封装起来,你只要实现很少的核心方法即可。
3)AQS 的三个核心概念
3.1 state:同步状态(一个 int)
可以理解成“资源剩余量/锁占用次数”:
ReentrantLock:state 表示重入次数Semaphore:state 表示剩余许可数CountDownLatch:state 表示剩余计数
3.2 等待队列:抢不到就排队(CLH 变体)
抢失败的线程会被封装成节点进入队列:
Head -> Node(T1) -> Node(T2) -> Node(T3) -> Tail
唤醒一般从队头开始,遵循一定的公平/非公平策略。
3.3 acquire/release:获取与释放的统一流程
你可以把 AQS 的 acquire 理解成:
1)先 tryAcquire(自己尝试一次)
2)失败则入队
3)挂起等待(park)
4)被唤醒后再尝试 tryAcquire
release 则会在释放成功后唤醒队列中的下一个合适线程。
4)你需要实现什么?(面试讲到这里很加分)
AQS 的设计是模板方法:
- 框架负责排队/阻塞/唤醒
- 子类只负责“业务语义”的获取/释放条件
常见需要覆盖的方法:
tryAcquire(int)tryRelease(int)- 共享模式下:
tryAcquireShared(int)、tryReleaseShared(int)
5)独占 vs 共享:AQS 的两种模式
独占(Exclusive)
同一时刻只允许一个线程成功获取:
ReentrantLock
共享(Shared)
同一时刻允许多个线程成功获取:
Semaphore(许可>1)CountDownLatch(等待计数归零后所有线程通过)ReadWriteLock的读锁(多读共享)
面试一句话:
独占像“单人洗手间”,共享像“多座位地铁”。
6)面试追问:AQS 为什么用队列?不用忙等行不行?
忙等(自旋)在高竞争下会浪费 CPU。
AQS 的队列 + park/unpark 能让线程“睡着”等唤醒,更适合锁等待时间不确定的场景。
7)一句话收尾(面试可直接用)
AQS 是并发同步器的基础框架:用
state表示同步状态,用队列管理等待线程,封装了 acquire/release 的排队与唤醒流程;各类锁与同步器通过实现tryAcquire/tryRelease(或 shared 版本)复用这套通用骨架。
Java面试题合集-26-线程池参数与设置.md
线程池核心参数怎么设置?(core/max/queue/keepAlive)
线程池不是“越大越好”的仓库,它更像一家餐厅:
corePoolSize:常驻服务员数量maximumPoolSize:高峰期临时加人的上限workQueue:排队等位区keepAliveTime:临时员工闲着多久就下班
参数设置的本质是:用可控的方式把并发压住,避免线程无限增长把机器拖死。
1)一句话结论
线程池参数要围绕任务类型(CPU 密集/IO 密集)、延迟目标和资源上限来定:核心线程数通常接近 CPU 核心数(CPU 密集)或略大(IO 密集),队列容量用于吸收瞬时流量,最大线程数用于短时峰值兜底,keepAlive 控制临时线程回收;生产环境还必须配置合理的拒绝策略与命名线程工厂。
2)先把 ThreadPoolExecutor 的“决策流程”讲清楚
提交任务时大致规则(很适合面试描述):
1)线程数 < core:创建核心线程执行
2)否则:尝试入队
3)如果队列满 且 线程数 < max:创建非核心线程执行
4)如果队列满 且 线程数 == max:执行拒绝策略
所以参数的本质是:你在控制“先扩线程还是先排队”。
3)CPU 密集 vs IO 密集:核心线程数怎么估?
3.1 CPU 密集(计算为主)
特点:线程大多在跑 CPU,不怎么等待。
建议:
corePoolSize ≈ CPU 核心数 或 CPU 核心数 + 1
理由:过多线程会造成频繁上下文切换,反而变慢。
3.2 IO 密集(等待为主:网络/DB/磁盘)
特点:线程经常阻塞等待 IO。
建议:
corePoolSize 可以显著大于 CPU 核心数(比如 2~4 倍起步,再压测调整)
更“面试范”的表达:
IO 密集时,线程大部分时间在等待,适当增加线程数能提高 CPU 利用率,但上限要受连接数、内存与下游承载能力约束。
4)队列怎么选?(这比“线程数”更关键)
4.1 无界队列(LinkedBlockingQueue 默认无界)
风险:任务堆积会吃光内存,延迟越来越大,还不容易触发扩线程。
适用:非常稳定的负载、明确可控的生产者速度(但生产环境一般不建议“无界”)。
4.2 有界队列(ArrayBlockingQueue / 指定容量的 LinkedBlockingQueue)
优点:能对系统施加背压,防止无穷堆积。
缺点:容量太小会更早触发拒绝。
面试建议:
生产优先用有界队列:让“系统过载时失败得体面”,而不是把机器拖死。
4.3 SynchronousQueue(不存任务,直接移交)
特点:提交任务不排队,要么立刻被线程接手,要么扩线程/拒绝。
适用:希望尽量不排队、低延迟、允许扩线程承压的场景(例如某些短任务)。
5)最大线程数与 keepAlive:峰值兜底与回收策略
maximumPoolSize:用于短时峰值,别设成“无限”,要考虑:- 每个线程栈内存
- 上下文切换
- 下游(DB/Redis)连接数
keepAliveTime:控制非核心线程空闲回收速度allowCoreThreadTimeOut(true):让核心线程也可回收(用于波峰波谷明显的场景)
6)线程工厂与命名:排障��备(强烈建议)
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
public class NamedThreadFactoryDemo {
public static void main(String[] args) {
ThreadFactory factory = r -> {
Thread t = Executors.defaultThreadFactory().newThread(r);
t.setName("order-pool-" + t.getId());
return t;
};
}
}
有了名字,jstack/日志里一眼就能定位是哪一类任务把线程打满了。
7)一个相对“生产友好”的线程池示例
import java.util.concurrent.*;
public class PoolExample {
public static ExecutorService pool() {
int core = Runtime.getRuntime().availableProcessors();
return new ThreadPoolExecutor(
core,
core * 2,
60, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(500),
r -> {
Thread t = new Thread(r);
t.setName("biz-pool-" + t.getId());
return t;
},
new ThreadPoolExecutor.CallerRunsPolicy()
);
}
}
注意:这是示例模板,最终值必须压测并结合下游承载能力调整。
8)一句话收尾(面试可直接用)
线程池参数的核心是“线程扩容与排队策略”:core 控制常驻并发,queue 吸收突发,max 兜底峰值,keepAlive 回收临时线程;CPU 密集 core≈核数,IO 密集可更大但受下游与内存约束;生产建议有界队列 + 明确拒绝策略 + 命名线程工厂。
Java面试题合集-27-线程池拒绝策略.md
线程池拒绝策略有哪些?生产上如何兜底?
线程池的拒绝策略,本质是系统过载时的“态度”:
任务太多了,我是直接拒绝?丢掉一些?还是让提交者自己干?
选错策略的后果通常不是“报个错”,而是:
- 请求堆积把系统拖死
- 下游雪崩
- 或者悄悄丢任务导致业务账对不上
1)一句话结论
当线程数达到
maximumPoolSize且队列已满时会触发拒绝;JDK 提供 4 种内置拒绝策略:Abort(抛异常)、CallerRuns(调用者执行)、Discard(直接丢)、DiscardOldest(丢最旧);生产选型要结合“能否丢任务/是否允许降速/是否必须告警”,并配合监控与限流做兜底。
2)什么时候会拒绝?(先把触发条件讲清楚)
触发拒绝需要同时满足:
线程池线程数 == maximumPoolSize
且 workQueue 已满
所以拒绝不是“偶发”,是你参数策略的自然结果。
3)四种内置拒绝策略(面试必背但要会解释)
3.1 AbortPolicy(默认)
- 行为:抛
RejectedExecutionException - 适用:不能默默丢任务,必须让调用方感知失败(例如关键业务)
3.2 CallerRunsPolicy
- 行为:由提交任务的线程自己执行任务
- 适用:用“降速”做背压(提交者变慢,流量自然被压住)
- 注意:如果提交者是 IO 线程(比如 Netty 事件循环),会拖慢整个 IO 处理链
3.3 DiscardPolicy
- 行为:直接丢弃任务,不抛异常
- 适用:允许丢弃且对一致性要求低(比如某些非关键采样、低价值日志)
3.4 DiscardOldestPolicy
- 行为:丢弃队列中最旧的任务,然后尝试重新提交当前任务
- 适用:更关心“最新任务”(例如某些状态刷新),但要非常小心业务语义
4)生产兜底怎么做?(高质量回答)
4.1 先明确:任务能不能丢?
把任务分三类(面试表达很工程):
- 绝不能丢:交易、支付、扣库存(必须失败可见或落库重试)
- 可以降级:推荐、画像、异步刷新
- 可以丢:低价值统计、非关键日志(但最好可采样、可观测)
4.2 关键业务:拒绝要“可见 + 可重试”
常见做法:
- AbortPolicy:让调用方失败并告警
- 或自定义 RejectedExecutionHandler:落库/入 MQ/写本地队列做补偿
自定义策略示例(简化版):
import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.ThreadPoolExecutor;
public class LogAndAbortPolicy implements RejectedExecutionHandler {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
System.err.println("reject: queue=" + executor.getQueue().size());
throw new RuntimeException("系统繁忙,请稍后重试");
}
}
4.3 背压思路:CallerRuns 让上游慢下来
如果你的提交者本身就是业务线程(比如 Controller 线程),CallerRuns 往往能起到“自动限流”的效果。
5)配套三件套:没有它们拒绝策略等于裸奔
1)监控:活跃线程数、队列长度、拒绝次数、任务耗时分位数
2)限流:入口限流/令牌桶/漏桶,提前在入口削峰
3)降级:非核心功能在高峰自动关闭或走缓存兜底
6)一句话收尾(面试可直接用)
线程池拒绝发生在“线程达到 max 且队列满”时;Abort 抛异常、CallerRuns 让提交者执行形成背压、Discard 直接丢、DiscardOldest 丢旧保新。生产选型要围绕业务是否允许丢、是否需要降速和可观测补偿,并配合监控、限流与降级兜底。
Java面试题合集-28-Future与CompletableFuture.md
Future 与 CompletableFuture 的区别?如何组合异步任务?
把异步任务想象成“点外卖”:
Future:你下单后只能反复问“到了没?”(get 阻塞)CompletableFuture:你可以设置“到了给我打电话/顺便帮我拿餐具/两份到了再一起吃”(回调与组合)
1)一句话结论
Future主要用于获取异步结果,但组合能力弱,常需要阻塞get();CompletableFuture在Future基础上提供了丰富的回调与编排能力(thenApply/thenCompose/allOf/anyOf 等),支持非阻塞式组合、异常链路处理与自定义线程池,更适合复杂异步流程。
2)Future 的典型用法(简单但“等得难受”)
import java.util.concurrent.*;
public class FutureDemo {
public static void main(String[] args) throws Exception {
ExecutorService pool = Executors.newFixedThreadPool(2);
Future<Integer> f = pool.submit(() -> {
Thread.sleep(500);
return 42;
});
System.out.println("do other work");
System.out.println(f.get()); // 阻塞等待结果
pool.shutdown();
}
}
Future 的主要痛点:
- 结果只能
get(阻塞) - 组合多个 Future 往往要自己写等待逻辑
- 异常处理不够链式
3)CompletableFuture 的核心优势:可组合、可回调、可链式异常处理
3.1 thenApply:对结果做变换
import java.util.concurrent.CompletableFuture;
public class ThenApplyDemo {
public static void main(String[] args) {
CompletableFuture.supplyAsync(() -> 21)
.thenApply(x -> x * 2)
.thenAccept(System.out::println)
.join();
}
}
3.2 thenCompose:扁平化串联(避免嵌套 Future)
import java.util.concurrent.CompletableFuture;
public class ThenComposeDemo {
static CompletableFuture<String> queryUser() {
return CompletableFuture.supplyAsync(() -> "Tom");
}
static CompletableFuture<Integer> queryScore(String user) {
return CompletableFuture.supplyAsync(() -> user.length() * 10);
}
public static void main(String[] args) {
queryUser()
.thenCompose(ThenComposeDemo::queryScore)
.thenAccept(System.out::println)
.join();
}
}
串联时优先 thenCompose(“一个任务依赖上一个结果”)。
4)并行组合:allOf / anyOf(面试最常问)
4.1 allOf:都完成再继续
import java.util.concurrent.CompletableFuture;
public class AllOfDemo {
public static void main(String[] args) {
CompletableFuture<String> a = CompletableFuture.supplyAsync(() -> "A");
CompletableFuture<String> b = CompletableFuture.supplyAsync(() -> "B");
CompletableFuture<Void> all = CompletableFuture.allOf(a, b);
all.thenRun(() -> System.out.println(a.join() + b.join()))
.join();
}
}
4.2 anyOf:谁先完成用谁
import java.util.concurrent.CompletableFuture;
public class AnyOfDemo {
public static void main(String[] args) {
CompletableFuture<Object> any = CompletableFuture.anyOf(
CompletableFuture.supplyAsync(() -> "fast"),
CompletableFuture.supplyAsync(() -> "slow")
);
System.out.println(any.join());
}
}
5)异常处理:handle / exceptionally(写出来就很加分)
import java.util.concurrent.CompletableFuture;
public class ExceptionallyDemo {
public static void main(String[] args) {
int v = CompletableFuture.supplyAsync(() -> 1 / 0)
.exceptionally(e -> 0)
.join();
System.out.println(v);
}
}
6)线程池:不要默认“全用 commonPool”(生产建议)
supplyAsync 默认用 ForkJoinPool.commonPool()。在生产环境建议为业务异步指定线程池,避免相互影响:
import java.util.concurrent.*;
public class CustomPoolDemo {
public static void main(String[] args) {
ExecutorService pool = Executors.newFixedThreadPool(4);
CompletableFuture.supplyAsync(() -> "x", pool)
.thenAccept(System.out::println)
.join();
pool.shutdown();
}
}
7)一句话收尾(面试可直接用)
Future适合“提交任务-阻塞取结果”的简单场景;CompletableFuture支持链式回调与任务编排(串联 thenCompose、并行 allOf/anyOf、异常 exceptionally/handle),更适合复杂异步流程,且生产环境应尽量指定业务线程池避免共用 commonPool 带来的干扰。
Java面试题合集-29-ThreadLocal原理与内存泄漏.md
ThreadLocal 的原理是什么?为什么会引发内存泄漏?
ThreadLocal 很像“每个线程一个抽屉”:
- 你把东西放进抽屉(set)
- 只要还是同一个线程,随时能从抽屉拿出来(get)
- 别的线程看不到你的抽屉
它很方便(比如存用户信息、traceId),但也很容易踩坑:线程池 + 忘记清理 = 内存泄漏。
1)一句话���论
ThreadLocal并不是把变量存到 ThreadLocal 对象里,而是存到当前线程的ThreadLocalMap中;map 的 key 是ThreadLocal的弱引用,value 是强引用。当线程长期存活(线程池)且不remove,即使 key 被回收,value 仍可能被强引用挂住形成泄漏(更准确说是“value 泄漏/滞留”)。
2)ThreadLocal 的“存放位置”到底在哪?
常见误解:ThreadLocal 里存了值。
真实结构更像这样:
Thread
└─ ThreadLocalMap
├─ Entry(key=ThreadLocal弱引用, value=对象)
├─ Entry(...)
└─ ...
所以:
- ThreadLocal 只是“钥匙”
- 真正的“抽屉柜”在 Thread 里
3)为什么 key 用弱引用?为什么还会泄漏?
3.1 key 用弱引用的目的
避免你把 ThreadLocal 变量丢了(没有强引用了)后,key 还能被 GC 回收。
3.2 泄漏发生的关键:value 是强引用 + 线程长期存活
当 key 被 GC 回收后,会出现:
Entry(key=null, value=大对象还在)
如果线程一直不结束(线程池线程会长期复用),这些 value 就会一直挂在这个线程的 map 里,直到:
- 你显式 remove
- 或下一次 set/get 触发清理(但不可靠)
- 或线程结束(线程池里通常很久不会结束)
这就是“ThreadLocal 内存泄漏”的常见来源。
4)最典型的事故现场:线程池复用线程
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadLocalLeakDemo {
static final ThreadLocal<byte[]> tl = new ThreadLocal<>();
public static void main(String[] args) {
ExecutorService pool = Executors.newFixedThreadPool(1);
for (int i = 0; i < 1000; i++) {
pool.execute(() -> {
tl.set(new byte[1024 * 1024]); // 1MB
// 忘记 remove
});
}
pool.shutdown();
}
}
这段代码在真实环境里非常容易把内存顶满(或出现 value 长期滞留)。
5)正确姿势:一定要 finally remove(面试必答)
public class ThreadLocalBestPractice {
static final ThreadLocal<String> tl = new ThreadLocal<>();
static void handle() {
tl.set("traceId-xxx");
try {
// do work
} finally {
tl.remove();
}
}
}
面试表达:
ThreadLocal 适合传递线程上下文,但在线程池里必须在 finally 里 remove,避免线程复用导致上下文串线和内存滞留。
6)除了内存泄漏,还有一个更隐蔽的问题:上下文串线
如果你不清理,下一次任务复用同一线程时可能读到上一个请求的用户信息/traceId,产生严重安全问题。
所以 remove 不只是为了内存,更是为了数据隔离。
7)一句话收尾(面试可直接用)
ThreadLocal 的值存在线程的 ThreadLocalMap 中,key 是 ThreadLocal 的弱引用,value 是强引用;线程池线程长期存活时如果不 remove,可能出现 key 被回收但 value 仍滞留导致内存泄漏和上下文串线,所以必须在 finally 中
remove()。
Java面试题合集-30-死锁定位与避免.md
如何识别与排查死锁?如何避免死锁?
死锁就像两个人互相礼让:
- A 抱着“锁1”不放,等“锁2”
- B 抱着“锁2”不放,等“锁1”
结果就是:两个人都很有礼貌,但事情永远办不完。
1)一句话结论
死锁是多个线程形成环形等待:每个线程都持有部分资源并等待对方释放;排查通常用
jstack/线程 dump 直接定位“Found one Java-level deadlock”,结合业务代码找锁顺序;避免死锁的核心是统一加锁顺序、缩小锁范围、避免嵌套锁和在持锁时做阻塞操作,必要时使用tryLock超时失败策略。
2)死锁的四个必要条件(面试官爱问)
经典“��要素”:
- 互斥:资源一次只能被一个线程占用
- 占有且等待:持有资源还要再等其他资源
- 不可剥夺:资源不能被强行抢走
- 循环等待:形成环
面试表达技巧:你不必死背术语,能讲出“环形等待”就已经抓住核心。
3)最小复现:两把锁 + 反着拿
public class DeadlockDemo {
static final Object L1 = new Object();
static final Object L2 = new Object();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
synchronized (L1) {
sleep(100);
synchronized (L2) {
System.out.println("t1 done");
}
}
}, "t1");
Thread t2 = new Thread(() -> {
synchronized (L2) {
sleep(100);
synchronized (L1) {
System.out.println("t2 done");
}
}
}, "t2");
t1.start();
t2.start();
}
static void sleep(long ms) {
try { Thread.sleep(ms); } catch (InterruptedException ignored) {}
}
}
这段代码的“坏心思”在于:t1 先拿 L1 后拿 L2,t2 反过来拿。
4)怎么定位死锁?(生产最实用)
4.1 jstack 一把梭
步骤(说给面试官听就够):
1)jps 找到 Java 进程 pid
2)jstack pid > dump.txt
3)在 dump 里搜 deadlock 或 Found one Java-level deadlock
你会看到类似信息:
- 哪些线程死锁
- 分别在等待哪把锁、持有哪些锁
- 以及堆栈位置(精确到类与行号)
4.2 更通俗的排查思路
- 看到线程大量
BLOCKED:优先怀疑锁竞争或死锁 - 如果线程长期不变化、互相等待:高度怀疑死锁
5)怎么避免死锁?(给出可执行的工程策略)
5.1 统一加锁顺序(最有效)
只要所有地方都按同一个顺序拿锁,就不会形成环。
例如规定:
永远先拿 L1 再拿 L2
5.2 减少锁嵌套,缩小锁范围
把锁只包住“共享数据的最小操作”,不要把网络/IO/远程调用包在锁里。
5.3 使用 tryLock + 超时(宁可失败也不永久卡死)
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
public class TryLockAvoidDeadlock {
static final ReentrantLock A = new ReentrantLock();
static final ReentrantLock B = new ReentrantLock();
static void work() throws InterruptedException {
if (A.tryLock(100, TimeUnit.MILLISECONDS)) {
try {
if (B.tryLock(100, TimeUnit.MILLISECONDS)) {
try {
// do work
} finally {
B.unlock();
}
} else {
// fallback / retry
}
} finally {
A.unlock();
}
}
}
}
5.4 分层拆锁:用更粗的“单锁”替代多把锁(有时更安全)
当业务允许时,用一把锁保护一组资源,虽然并发度下降,但能显著降低死锁风险。
6)一句话收尾(面试可直接用)
死锁本质是环形等待;排查用
jstack线程 dump 可直接定位等待关系和代码行;避免死锁的关键是统一锁顺序、减少嵌套锁与持锁时间,避免持锁做阻塞操作,必要时用tryLock超时让系统能失败并恢复。
Java面试题合集-31-常用并发容器.md
常用并发容器有哪些?各自适合什么场景?
并发容器就像“专为多人同时操作设计的收纳盒”:
- 普通容器(ArrayList/HashMap)像“单人桌面”,你硬要多人一起用,就容易翻车
- 并发容器像“多人协作工位”,规则清晰、冲突可控
1)一句话结论
常用并发容器包括
ConcurrentHashMap、ConcurrentLinkedQueue、CopyOnWriteArrayList、BlockingQueue家族等;选型关键看读写比例、是否需要阻塞协作、是否需要有界背压与是否需要强一致遍历:Map 通常优先 CHM,读多写少 List 用 COW,线程间生产消费用阻塞队列,纯队列化传递可用无锁队列。
2)Map:ConcurrentHashMap(最常用)
适合:
- 高并发读写 map
- 需要原子复合操作:
computeIfAbsent、merge、compute
典型示例:并发计数(比 get+put 安全)
import java.util.concurrent.ConcurrentHashMap;
public class ChmMergeDemo {
static final ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
static void incr(String k) {
map.merge(k, 1, Integer::sum);
}
}
注意:CHM 不允许 key/value 为 null。
3)List:CopyOnWriteArrayList(读多写少)
适合:
- 读远多于写
- 需要遍历稳定、迭代期间不抛并发修改异常(快照语义)
不适合:
- 写很频繁(每次写都会复制底层数组)
4)Queue:无锁队列 vs 阻塞队列
4.1 ConcurrentLinkedQueue(非阻塞、无界)
适合:
- 高并发入队/出队
- 不需要阻塞等待(拿不到就返回空)
4.2 BlockingQueue(生产者-消费者必选)
常见:
ArrayBlockingQueue:有界、数组实现,内存更紧凑LinkedBlockingQueue:链表实现,可配置容量(注意默认可能很大)PriorityBlockingQueue:带优先级(无界),适合任务优先级调度DelayQueue:延迟任务(定时/超时处理)SynchronousQueue:不存储元素,直接移交(适合低延迟、扩线程/拒绝策略配合)
面试表达:
需要“阻塞协作”和“背压控制”就用 BlockingQueue;只需要高性能队列化传递就用 ConcurrentLinkedQueue。
5)Set:ConcurrentSkipListSet / CopyOnWriteArraySet
5.1 ConcurrentSkipListSet
特点:有序、并发安全(基于跳表),操作 O(log n)。
适合:需要有序集合且并发读写。
5.2 CopyOnWriteArraySet
特点:读多写少、快照遍历(内部基于 CopyOnWriteArrayList)。
适合:监听器列表、白名单等低频变更集合。
6)面试加分:并发容器也不是“万能”
常见误区:
- “用了并发容器就线程安全了”:复合操作仍要用原子 API(如 merge/compute),否则还是竞态
- “无界队列更稳”:无界队列更容易造成堆积与 OOM,生产一般更推荐有界背压
7)一句话收尾(面试可直接用)
并发容器要按场景选:Map 选
ConcurrentHashMap并使用compute/merge做原子复合操作;读多写少 List 选CopyOnWriteArrayList;线程间协作与背压用BlockingQueue(有界优先);高性能无锁队列用ConcurrentLinkedQueue;需要并发有序集合可用ConcurrentSkipListSet/Map。
更多推荐

所有评论(0)