阶段:进阶篇 | 建议时长:7 天 | 前置:02-面向对象与核心类.md、03-集合与泛型.md
学习目标
- 能用三种方式创建线程,画出线程 6 种状态流转图。
- 能解释 JMM 三性与 volatile 的两个保证、一个不保证。
- 掌握 synchronized 三种用法与 ReentrantLock 差异,会写死锁并排查。
- 能默写线程池 7 参数、执行流程、4 种拒绝策略,并给出线程数经验公式。
- 会用 ThreadLocal、CAS 工具类、JUC 协调工具与 CompletableFuture 完成典型并发任务。
核心知识点
线程的三种创建方式
面试官想听:本质都是 Runnable——Thread 封装任务、FutureTask 接收返回值、线程池统一管理。
// 1. 继承 Thread
class MyThread extends Thread {
public void run() { System.out.println("thread"); }
}
// 2. 实现 Runnable(推荐,任务与执行解耦,还能交给线程池)
Runnable task = () -> System.out.println("runnable");
new Thread(task).start();
// 3. Callable + FutureTask(有返回值、可抛异常)
Callable<Integer> c = () -> 42;
FutureTask<Integer> ft = new FutureTask<>(c);
new Thread(ft).start();
Integer result = ft.get(); // 阻塞直到完成
- 必须调用
start()而不是run():run()只是普通方法调用(单线程执行),start()才创建新线程。 - 实战第 4 种:线程池
ExecutorService(见下文),避免手动 new Thread。 Thread.currentThread()获取当前线程;线程默认不继承父线程状态(InheritableThreadLocal 除外)。
线程的 6 种状态
面试官想听:NEW→RUNNABLE 的启动路径、RUNNABLE 与阻塞系状态的区分、WAITING 与 TIMED_WAITING 的差别。
NEW -> start() -> RUNNABLE <-> BLOCKED(等 synchronized 锁)
|-> WAITING(wait/join/LockSupport.park,无超时)
|-> TIMED_WAITING(sleep(n)/wait(n)/join(n))
|-> TERMINATED(run 结束)
sleep是 Thread 静态方法、不释放锁;wait是 Object 方法、释放锁,且必须在 synchronized 块内调用,靠notify/notifyAll唤醒。join():等待目标线程结束;yield():提示调度器让出 CPU(不可依赖);interrupt()只是设置中断标志,阻塞中线程会抛InterruptedException,需自行决定退出策略。- 守护线程:
thread.setDaemon(true)必须在 start 前调用;所有非守护线程结束 JVM 就退出,GC 线程就是守护线程。
JMM 三性
面试官想听:原子性、可见性、有序性,能各举一个反例并给出对应解决方案。
- 原子性:操作不可分割。
i++是「读-改-写」三步,多线程下会丢更新;用synchronized、AtomicInteger、Lock保证。经典对比:10 个线程各count++1000 次,用int count结果通常小于 10000 且每次运行都不同(读-改-写被交错打断,丢更新),换成AtomicInteger.incrementAndGet()则精确等于 10000(CAS 保证单次自增原子)。 - 可见性:一个线程的修改对其他线程立即可见。工作内存(CPU 缓存)与主存不同步导致;用
volatile、synchronized、final保证。 - 有序性:编译器/CPU 指令重排导致代码执行顺序与书写顺序不一致;
volatile禁止指令重排,锁保证串行语义。 - happens-before:JMM 定义的操作间偏序规则(程序顺序、锁的解锁先于加锁、volatile 写先于读、线程 start/join 等),满足则后操作可见前操作的结果,是理解并发的理论抓手。
- 传递性推演(面试爱考):线程 A 里先写
v = 1(volatile)再写y = 2(普通变量);线程 B 读到v == 1后再读y,则 B 一定读到y == 2。推导链:A 中”写 v 先于写 y”(程序顺序规则)+ “v 的写 happens-before B 对 v 的读”(volatile 规则)+ B 中”读 v 先于读 y”(程序顺序规则),传递可得 y 的写对 B 可见——普通变量的写跟着 volatile 的写”顺带”可见了。反过来(先写普通变量、后写 volatile)不保证。
- 传递性推演(面试爱考):线程 A 里先写
volatile
面试官想听:两个保证(可见性、禁止重排)+ 一个不保证(原子性),最好带双重检查锁单例的例子。
public class Singleton {
private static volatile Singleton instance; // 防止指令重排
public static Singleton getInstance() {
if (instance == null) { // 第一次检查:避免不必要加锁
synchronized (Singleton.class) {
if (instance == null) { // 第二次检查
instance = new Singleton(); // 分配内存->初始化->赋值,可能被重排
}
}
}
return instance;
}
}
new对象不是原子操作:若无 volatile,另一线程可能在「赋值完成但未初始化」时拿到半成品。- volatile 适合一写多读或状态标志位(
volatile boolean running = false;);复合运算仍需锁或原子类。
synchronized 与锁升级
面试官想听:三种用法对应不同锁对象;1.6 后锁升级路径(偏向→轻量级→重量级)说明 synchronized 并不重。
- 三种用法:① 修饰实例方法(锁当前对象 this);② 修饰静态方法(锁 Class 对象);③ 同步代码块
synchronized(obj)(锁指定对象)。 - 底层基于对象头的 Monitor(重量级锁依赖 OS mutex),Java 6 引入优化:无锁 → 偏向锁(单线程反复进入)→ 轻量级锁(CAS 自旋)→ 重量级锁(线程挂起),只升不降。
- 可重入:同一线程可重复获取自己持有的锁;异常会自动释放锁。
- 注意
String常量、Integer 缓存对象等被共享的锁对象,可能造成意外竞争。
公平锁与非公平锁(synchronized 只有非公平语义,公平性见 ReentrantLock):new ReentrantLock(true) 为公平锁,严格按队列顺序发放;默认非公平——新来的线程先 CAS 抢一次,抢到直接执行,省掉挂起/唤醒与排队的开销,上下文切换更少、吞吐更高。需要「排队保证」或防饥饿时才用公平模式或 tryLock(fair)/带超时的 tryLock。
读写锁 ReentrantReadWriteLock 与 StampedLock
- 锁分离:读读并行、读写互斥、写写互斥——state 高 16 位记读锁数、低 16 位记写锁重入数,适合「读多写少」(如本地配置缓存)。写线程可降级为读线程,读锁不能升级为写锁(会死锁)。
StampedLock(Java 8):增加乐观读tryOptimisticRead()——只拿一个戳不加锁,读完validate(stamp)校验期间没被写过,冲突才升级为读锁;性能更高但不可重入、不支持 Condition,出问题时排查更难。
ReentrantLock 与 synchronized vs Lock
面试官想听:默认用 synchronized,需要「可中断、超时、公平、多条件」四个能力之一才上 ReentrantLock。
private final ReentrantLock lock = new ReentrantLock(); // true 为公平锁
private final Condition notEmpty = lock.newCondition();
public void take() throws InterruptedException {
lock.lockInterruptibly(); // 可中断获取
try {
while (queue.isEmpty()) notEmpty.await(); // 条件等待(等价 wait)
// 取数据
} finally {
lock.unlock(); // 必须放 finally
}
}
| 维度 | synchronized | ReentrantLock |
|---|---|---|
| 层级 | JVM 关键字,自动释放 | JDK API,手动 lock/unlock |
| 可中断/超时 | 不支持 | lockInterruptibly()/tryLock(超时) |
| 公平锁 | 不支持 | 构造参数可选 |
| 条件变量 | 单一 wait/notify | 多个 Condition 精准唤醒 |
AQS(抽象队列同步器)
面试官想听:JUC 里锁和同步工具的公共地基——一个 volatile int + 一条排队队列。
- 核心:
volatile int state表示资源状态(ReentrantLock 中=重入次数,Semaphore 中=许可数,CountDownLatch 中=剩余计数);抢不到就封装成节点进入 CLH 变体的双向等待队列,靠LockSupport.park()挂起,前序释放时 unpark 唤醒。 - 模板方法模式:AQS 管排队与挂起/唤醒,子类只需实现
tryAcquire/tryRelease(独占)或tryAcquireShared/tryReleaseShared(共享)——ReentrantLock、Semaphore、CountDownLatch、ReentrantReadWriteLock 都基于它。 - 两种模式:独占模式(同一时刻只有一个线程持有,如 ReentrantLock)vs 共享模式(多个线程可同时获取,如 Semaphore、CountDownLatch)。
- 一句话记忆口径:state 表示资源,队列表示排队的人。
死锁:四条件与排查
面试官想听:背出四个必要条件并给出「按序加锁」这个最实用的预防手段。
- 四个必要条件:互斥、持有并等待、不可剥夺、循环等待,破坏任意一个即可预防(最常用:所有线程按相同顺序加锁;或
tryLock带超时)。 - 排查:
jstack pid末尾直接输出Found one Java-level deadlock;jconsole 检测死锁按钮;代码上lock.tryLock(timeout)日志告警。
线程池:7 参数、执行流程与拒绝策略
面试官想听:这是并发面试第一高频,参数、流程、拒绝策略必须一次说全,再补 Executors 的坑。
ThreadPoolExecutor pool = new ThreadPoolExecutor(
4, 8, 60, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(100),
Thread::new, // 线程工厂:命名便于排障
new ThreadPoolExecutor.CallerRunsPolicy());
- 7 参数:核心线程数 corePoolSize、最大线程数 maximumPoolSize、空闲存活时间 keepAliveTime、时间单位、任务队列 workQueue、线程工厂 threadFactory、拒绝策略 handler。
- 执行流程:核心线程未满则新建核心线程执行 → 满了进队列排队 → 队列满了再创建非核心线程至 maximum → 达到 maximum 则执行拒绝策略。
- 4 种拒绝策略:
AbortPolicy(默认,抛 RejectedExecutionException)、CallerRunsPolicy(调用者线程自己执行,天然削峰)、DiscardPolicy(静默丢弃)、DiscardOldestPolicy(丢队首最旧任务再重试)。 - Executors 的坑(阿里规约禁止):
newFixedThreadPool/newSingleThreadExecutor用无界队列 LinkedBlockingQueue,任务堆积可 OOM;newCachedThreadPool最大线程数 Integer.MAX_VALUE,可无限建线程。手动 new ThreadPoolExecutor。 - 线程数经验公式:CPU 密集型 ≈ N核 + 1;I/O 密集型 ≈ N核 × (1 + 等待时间/计算时间),再压测校准。
- 优雅关闭:
shutdown()(等任务跑完)→awaitTermination→ 超时后shutdownNow()。 submit吞异常:异常存在返回的 Future 里,不调用get()就无人知晓——记得处理或用 execute。
CAS 与 Atomic、ABA 问题
面试官想听:CAS = 比较并交换的 CPU 原子指令,知道它的三个问题:ABA、自旋开销、只能保一个变量。
- CAS:
compareAndSwap(内存值V, 预期值E, 新值N),硬件级原子指令,失败自旋重试;Unsafe→AtomicInteger等的实现基础。 - ABA 问题:值从 A→B→A,CAS 以为没变过;用
AtomicStampedReference(版本号戳)解决。 - 常用原子类:
AtomicInteger/AtomicLong(incrementAndGet)、AtomicReference、LongAdder(分段累加,高并发计数优于 AtomicLong)。
ThreadLocal 原理与内存泄漏
面试官想听:ThreadLocalMap 的 key 是弱引用这条链路,以及「为什么必须 finally 里 remove」。
static ThreadLocal<SimpleDateFormat> DF =
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));
try {
use(DF.get());
} finally {
DF.remove(); // 必须手动清理
}
- 原理:每个
Thread内有ThreadLocalMap,key 是 ThreadLocal 对象(弱引用),value 是存的值(强引用)。 - 泄漏链路:ThreadLocal 外部引用消失后 key 被 GC 变 null,但 value 仍被线程强引用,线程池线程长存 → value 永远无法回收。Entry 的 key 为 null 也不会被主动清。
- 结论:线程池场景用完必须
remove();用途:按线程隔离状态(如 SimpleDateFormat、用户上下文、链路 traceId)。
ConcurrentHashMap 1.7 vs 1.8
面试官想听:分段的粒度从「Segment 二级哈希」进化到「单桶 CAS+synchronized」。
- 1.7:
Segment分段锁(默认 16 段,继承 ReentrantLock),并发度=段数;两次 hash 定位。 - 1.8:抛弃分段,Node 数组 + 链表/红黑树;put 时桶为空用 CAS 插入,冲突时 synchronized 锁住桶头节点,锁粒度更细、并发度更高。
- size 统计用
baseCount + CounterCell[]分片计数(LongAdder 思想);扩容支持多线程协助迁移。 - key/value 都不允许 null(并发下无法区分「不存在」与「值为 null」)。
JUC 容器补充:CopyOnWriteArrayList
- 原理:写时复制——add/remove/set 时复制一份新数组改完再替换引用,读永远走旧数组,迭代器不加锁且不会抛 ConcurrentModificationException;适合读极多写极少(如监听器列表、黑白名单)。
- 代价:每次写都复制整个数组,写开销大、内存翻倍峰值;且读到的可能是旧快照(弱一致性)。与 ConcurrentHashMap 并列记忆:都是「并发场景替代非线程安全集合」的方案,一个治 ArrayList,一个治 HashMap。
CountDownLatch / CyclicBarrier / Semaphore
面试官想听:三个工具一句话定位——等 N 件事完成、凑 N 个人再一起走、只放行 M 个。
CountDownLatch:countDown()减计数、await()阻塞到 0;一次性,用于主线程等待多个子任务完成。CyclicBarrier:N 个线程互相等待到齐后一起继续,可复用(可传回调);用于分段计算后汇总。Semaphore:许可数=并发上限,acquire/release;用于限流(如数据库连接数、接口并发数)。- 阻塞队列:
ArrayBlockingQueue(有界数组)、LinkedBlockingQueue(默认无界链表)、SynchronousQueue(不存储、直接交接)、DelayQueue/PriorityBlockingQueue;put/take自动阻塞,是生产者消费者的现成方案。
CompletableFuture 与虚拟线程
CompletableFuture<String> price = CompletableFuture.supplyAsync(() -> queryPrice(id), pool);
CompletableFuture<Integer> stock = CompletableFuture.supplyAsync(() -> queryStock(id), pool);
price.thenCombine(stock, (p, s) -> p + " / " + s) // 两个结果合并
.orTimeout(2, TimeUnit.SECONDS) // 超时保护
.whenComplete((r, e) -> log.info("{}", r, e));
- 异步编排三件套:
supplyAsync提交、thenApply/thenAccept/thenCombine链式转换、thenCompose串行依赖、allOf/anyOf聚合多个任务。必须传入自定义线程池,默认池仍是 ForkJoinPool.commonPool。 - 虚拟线程(Java 21 正式):JVM 调度的轻量级线程(KB 级栈),
Thread.ofVirtual().start(...)或Executors.newVirtualThreadPerTaskExecutor();阻塞时自动挂起不占 OS 线程,适合大量阻塞 I/O 任务(一秒开万十个线程等下游)。 - 虚拟线程不适合什么:① CPU 密集任务无收益——它解决的是「等待时不占 OS 线程」,计算量不变,还是 N 核;② 观念要变:不池化(创建极廉价,用完即弃,别再建
newFixedThreadPool复用)、ThreadLocal慎用以避免长生命周期对象泄漏(可考虑替代方案),且 synchronized 块内长时间阻塞会把载体线程一起占住(建议改用 ReentrantLock)。
动手实践
任务 1:三线程交替打印 ABC
private static final Object lock = new Object();
private static int state = 0; // 0->A, 1->B, 2->C
// 每个线程:synchronized (lock) { while (state % 3 != self) lock.wait(); print; state++; lock.notifyAll(); }
- 验收标准:连续输出
ABCABCABC...至少 10 轮不乱序;用 wait/notifyAll(不能用 Thread.sleep 控制顺序);能解释为什么用 while 而不是 if 判断条件(防止虚假唤醒)。
任务 2:阻塞队列生产者消费者
- 要求:
ArrayBlockingQueue<Integer>(容量 5),3 个生产者各生产 100 个随机数,2 个消费者取出求和;用「毒丸对象」通知消费者退出。 - 验收标准:程序正常退出且总和打印完整;生产过快时观察
put阻塞(有界队列生效);改用手写wait/notifyAll版本对比一次。
任务 3:线程池批处理统计耗时
- 要求:用
new ThreadPoolExecutor(4, 8, ...)提交 1000 个模拟任务(Thread.sleep(1)),统计总耗时;分别设置 CPU 密集(sleep 0)与 I/O 密集(sleep 10)参数对比线程数公式。 - 验收标准:记录不同 corePoolSize 下的耗时对比数据;拒绝策略演示触发过一次(队列调小);所有线程有命名(自定义 ThreadFactory)。
高频面试题
Q:创建线程有几种方式?⭐
- 继承 Thread 重写 run;实现 Runnable 交给 Thread;实现 Callable 配 FutureTask 拿返回值;线程池提交任务。
- 本质上只有「new Thread().start()」一种启动方式,其他都是任务形态差异。
- 实践必须用线程池:复用线程、统一管控、避免频繁创建销毁。
Q:线程有哪些状态?⭐
- 6 种:NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED。
- 调 start 后进入 RUNNABLE;等 synchronized 进入 BLOCKED;wait/join/park 进入 WAITING;带超时版本进入 TIMED_WAITING。
- sleep 不释放锁、wait 释放锁且必须在同步块内。
Q:volatile 和 synchronized 的区别?⭐
- volatile 只保证可见性和有序性,不保证原子性;synchronized 三性都保证。
- volatile 是轻量级无锁方案,适合状态标志、一写多读;synchronized 互斥执行,适合复合操作。
- volatile 不阻塞线程;synchronized 可能阻塞。volatile 修饰变量,synchronized 修饰方法/代码块。
Q:说说线程池的核心参数与任务执行流程。⭐
- 7 参数:corePoolSize、maximumPoolSize、keepAliveTime、时间单位、workQueue、threadFactory、拒绝策略 handler。
- 流程:核心线程未满直接新建执行 → 满了进队列 → 队列满再建线程到最大值 → 达到最大值执行拒绝策略。
- 四种拒绝策略:Abort(抛异常)、CallerRuns(调用者执行)、Discard(丢弃)、DiscardOldest(丢最旧)。
Q:线程池大小怎么定?
- CPU 密集型:N+1(N 为核心数),避免过多上下文切换。
- I/O 密集型:N × (1 + 等待时间/计算时间),等待占比越大线程越多。
- 经验值需压测校准;区分业务用独立线程池,避免相互影响。
Q:ThreadLocal 为什么会内存泄漏?⭐
- 每个 Thread 持有 ThreadLocalMap,key 是弱引用的 ThreadLocal,value 是强引用。
- ThreadLocal 被回收后 key 变 null,value 无法访问但被长生命周期线程(线程池)强引用,无法回收。
- 规范:使用完(尤其线程池场景)必须在 finally 中 remove()。
Q:CAS 是什么?有什么问题?
- Compare And Swap:比较内存值与预期值,相等则替换为新值,由 CPU 原子指令保证,失败自旋重试。Atomic 类的底层。
- 问题:ABA(用 AtomicStampedReference 加版本号解决)、自旋开销(竞争激烈时 CPU 空转)、只能保证单个变量原子性。
- 典型应用:AtomicInteger 计数、ConcurrentHashMap 空桶插入。
Q:ConcurrentHashMap 如何保证并发安全?1.7 和 1.8 区别?⭐
- 1.7:Segment 分段锁(默认 16 段 ReentrantLock),锁一段。
- 1.8:CAS + synchronized 锁单个桶头节点,粒度更细并发更高;链表过长转红黑树。
- size 用 CounterCell 分片计数;不允许 null key/value。
Q:sleep 和 wait 的区别?
- 所属:Thread.sleep 是静态方法;wait 是 Object 方法。
- 锁:sleep 不释放锁;wait 释放对象锁。
- 场景:wait/notify 配合做线程间通信,必须在 synchronized 内;sleep 单纯休眠。
Q:死锁怎么产生、怎么排查和避免?
- 四个必要条件:互斥、持有并等待、不可剥夺、循环等待。
- 排查:jstack 输出 Found one Java-level deadlock;jconsole 检测死锁。
- 避免:全局固定加锁顺序、tryLock 带超时、减小锁粒度、用并发工具替代手写锁。
推荐资料
| 资料 | 说明 | 链接 |
|---|---|---|
| 廖雪峰 Java 多线程 | 线程基础到同步机制的主线教程 | https://liaoxuefeng.com/books/java/threading/index.html |
| pdai 并发知识体系 | JMM/锁/线程池/JUC 全景与源码索引 | https://pdai.tech/md/java/thread/java-thread-x-overview.html |
| JavaGuide 并发面试题 | 本章所有面试题的标准展开版本 | https://javaguide.cn/java/concurrent/java-concurrent-questions-01.html |