这次我们来看 Java 多线程面试题。内容不绕弯子,直接按考点拆。无论是校招还是社招,多线程基本都在二面或三面出现,而且经常和项目细节绑在一起问。很多候选人能背出synchronized和Lock的区别,但一问到线程池拒绝策略、ThreadLocal的 key 为什么用弱引用、volatile到底保证什么,就明显卡壳。这篇内容把 Java 多线程面试题按知识体系整理成一份可直接复习的资料,每个章节给出核心结论、可运行的代码示例和面试回答思路,适合准备 Java 面试、想快速过一遍并发知识点的读者。
如果你只剩 3 天复习多线程,直接按这份清单走:第一天吃透线程基础、锁机制和 JMM;第二天搞定线程池、并发工具类和阻塞队列;第三天刷高频问答、过一遍 Spring Boot 中的多线程实战场景,再排查一遍常见问题。下面进入正题。
1. Java 多线程面试核心考点速览
先看整体知识分布,方便对照自查。
| 考点模块 | 核心内容 | 面试出现频率 | 推荐掌握程度 |
|---|---|---|---|
| 线程基础 | 创建方式、生命周期、start与run | 高 | 必会 |
| JMM 与线程安全 | 可见性、原子性、有序性、volatile | 高 | 必会 |
| 锁机制 | synchronized、Lock、CAS、AQS | 高 | 必会 |
| 线程池 | 七大参数、执行流程、拒绝策略 | 很高 | 必会 |
ThreadLocal | 原理、内存泄漏、使用场景 | 中高 | 必会 |
| 并发工具类 | CountDownLatch、CyclicBarrier、Semaphore | 中 | 掌握 |
| 阻塞队列 | ArrayBlockingQueue、LinkedBlockingQueue | 中高 | 掌握 |
| 并发容器 | ConcurrentHashMap、CopyOnWriteArrayList | 中高 | 掌握 |
| 异步编程 | CompletableFuture、FutureTask | 中高 | 掌握 |
| 项目实战 | Spring Boot 异步、多线程调用外部接口 | 高 | 会叙述场景 |
从这个表能看到,线程池、锁和 JMM 是面试官最常下手的位置。项目经历里如果能写清“我用线程池处理了什么任务、怎么设置的参数、有没有遇到拒绝或超时”,就是加分项。
2. 线程基础:生命周期与创建方式
2.1 线程创建的四种方式
Java 中创建线程主要有四种方式:
- 继承
Thread类,重写run()。 - 实现
Runnable接口,传给Thread。 - 实现
Callable接口,配合FutureTask获取返回值。 - 通过线程池提交任务,底层也是基于
Runnable或Callable。
实际开发中,继承Thread的方式很少用,因为 Java 单继承限制会让类设计变差。多数场景优先Runnable,需要返回值时用Callable。
// Callable 示例 ExecutorService executor = Executors.newSingleThreadExecutor(); Future<Integer> future = executor.submit(() -> { return 1 + 1; }); Integer result = future.get(); // 阻塞获取结果 executor.shutdown();2.2 线程六种状态与生命周期
线程状态在Thread.State枚举中定义:
public enum State { NEW, // 新建,还没调用 start() RUNNABLE, // 可运行,可能在运行,也可能在等待 CPU BLOCKED, // 阻塞,等待监视器锁 WAITING, // 等待,需要其他线程唤醒 TIMED_WAITING, // 限时等待,时间到自动唤醒 TERMINATED; // 终止 }面试容易问这几个点:
start()和run()的区别:start()会创建新线程并执行run(),run()只是普通方法调用,不会启动新线程。sleep()和yield()的区别:sleep()让线程进入TIMED_WAITING,不释放锁;yield()只是让出 CPU,状态还是RUNNABLE。wait()和sleep()的区别:wait()必须在同步代码块中调用,会释放锁;sleep()不释放锁。
一个易错点是:BLOCKED状态只在等待 synchronized 监视器锁时出现。如果是Lock接口的阻塞等待,线程处于WAITING或TIMED_WAITING,不进入BLOCKED。
2.3 为什么说run()不能直接调用
直接调用run()是在当前线程执行任务,没有创建新线程。面试可以这样回答:start()的本质是告诉 JVM 创建一个新的操作系统线程,由该线程回调run();直接调run()只是普通方法调用,无法实现异步执行。
经常有候选人被问到“线程池提交任务时,如果直接调run()会怎样”,其实线程池内部也是这样,只有真正把任务交给 worker 线程执行才叫异步,否则就在提交线程里串行跑。
3. JMM 与线程安全三要素
3.1 Java 内存模型
Java 内存模型(JMM)规定了共享变量在不同线程之间的可见性规则。它定义了一套抽象内存结构,不是实际的物理内存布局:
- 所有共享变量存在主内存。
- 每个线程有自己的工作内存,保存共享变量的副本。
- 线程操作变量时,先把主内存变量读到工作内存,修改后再写回主内存。
这就带来三个问题:
- 可见性:线程 A 修改了变量,线程 B 不一定立刻看到。
- 原子性:复合操作如
i++,不是一步完成。 - 有序性:编译器和 CPU 可能重排序。
volatile解决可见性和有序性,不解决原子性。synchronized和Lock同时解决三个问题,因为锁保证互斥,也就保证了原子性。
3.2volatile到底保证什么
volatile有两个核心语义:
- 写一个
volatile变量时,JMM 会把该线程工作内存中的变量强制刷新到主内存。 - 读一个
volatile变量时,JMM 会使线程工作内存中该变量的副本失效,必须从主内存重新读取。
面试经典题:用volatile实现线程安全的单例。
public class Singleton { private static volatile Singleton instance; private Singleton() {} public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; } }这里为什么用volatile:new Singleton()不是原子操作,在 JVM 层面分三步:
- 分配内存。
- 初始化对象。
- 引用指向内存地址。
如果不用volatile,2 和 3 可能重排序,另一个线程会拿到一个“未完全初始化”的对象。
volatile不能解决原子性的典型例子是count++。所以日常开发中,不要指望volatile替代锁或原子类。
3.3 happens-before 规则
JMM 定义了一组 happens-before 规则,用来判断某个写操作是否对另一个读操作可见:
- 程序顺序规则:同一线程中,前面的操作 happens-before 后面的操作。
- 锁规则:解锁 happens-before 后续的加锁。
volatile规则:volatile写 happens-before 后续的volatile读。- 传递性:A happens-before B,B happens-before C,则 A happens-before C。
- 线程启动规则:
start()之前的操作 happens-before 新线程中的操作。 - 线程终止规则:线程中的所有操作 happens-before 其他线程检测到该线程终止。
回答可见性问题时,抬出 happens-before 规则会很加分。面试官会接着问“volatile为什么能保证可见性”,其实就是volatile写读规则。
4. 锁机制:synchronized、Lock、CAS、AQS
4.1synchronized的锁升级过程
synchronized在 JDK 1.6 之后做了大量优化,锁存在四种状态,按竞争程度升级:
无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁。
- 偏向锁:只有一个线程访问同步块时,偏向该线程,减少 CAS 开销。
- 轻量级锁:有竞争时,通过 CAS 尝试获取锁,适应线程交替执行场景。
- 重量级锁:竞争激烈时,升级为依赖操作系统互斥量的锁,涉及用户态和内核态切换,开销大。
面试被问到“synchronized为什么重”,要能解释锁升级和重量级锁的内核态切换开销。
4.2synchronized和Lock怎么选
高频对比题,建议直接记住下表:
| 对比点 | synchronized | Lock |
|---|---|---|
| 锁获取方式 | 自动获取/释放 | 手动lock()/unlock() |
| 可中断性 | 不可中断 | lockInterruptibly()可中断 |
| 公平性 | 非公平 | ReentrantLock支持公平/非公平 |
| 多个条件 | 一个监视器 | newCondition()支持多个条件 |
| 性能 | 优化后差距不大 | 灵活性高 |
用synchronized还是ReentrantLock,结论是:能不加锁就不加锁;必须加锁时优先synchronized,因为写法简单、不容易忘释放锁。只有需要可中断、公平锁、多条件队列时,才用ReentrantLock。
ReentrantLock lock = new ReentrantLock(true); // 公平锁 Condition notFull = lock.newCondition(); Condition notEmpty = lock.newCondition(); lock.lock(); try { // 业务逻辑 } finally { lock.unlock(); }4.3 CAS 与 ABA 问题
CAS(Compare And Swap)是原子操作,底层调用 CPU 的cmpxchg指令。它包含三个操作数:内存位置 V、旧值 A、新值 B。只有 V 等于 A 时,才把 V 更新为 B。
Java 中AtomicInteger、LongAdder、ConcurrentHashMap都依赖 CAS。
CAS 的经典问题是 ABA:
- 线程 1 读取值为 A。
- 线程 2 把值从 A 改成 B,再改回 A。
- 线程 1 执行 CAS,发现值还是 A,于是 CAS 成功。
实际上变量已经被改过两次。解决 ABA 可以使用版本号,Java 中AtomicStampedReference就是这个思路。
4.4 AQS 是什么
AQS(AbstractQueuedSynchronizer)是ReentrantLock、Semaphore、CountDownLatch等并发工具类的底层框架。核心是一个volatile int state和一个 CLH 变体的等待队列。
state == 0表示锁未被占用。- 线程获取锁失败,会封装成 Node 节点进入同步队列,阻塞挂起。
- 持有锁的线程释放后,唤醒队列头部节点。
回答 AQS 时,不需要背内部实现细节,但要能说清“状态变量 + FIFO 等待队列 + 模板方法模式”这三层。
5. 线程池核心考点
5.1 七大参数
线程池在 Java 面试里出现频率极高,核心是ThreadPoolExecutor的构造参数。直接背记忆点:
public ThreadPoolExecutor( int corePoolSize, // 核心线程数 int maximumPoolSize, // 最大线程数 long keepAliveTime, // 非核心线程空闲存活时间 TimeUnit unit, // 存活时间单位 BlockingQueue<Runnable> workQueue, // 任务队列 ThreadFactory threadFactory, // 线程工厂 RejectedExecutionHandler handler // 拒绝策略 )面试必问:线程池执行流程。
- 提交任务后,当前线程数小于
corePoolSize,创建核心线程执行。 - 当前线程数等于
corePoolSize,任务进入workQueue排队。 - 队列满,当前线程数小于
maximumPoolSize,创建非核心线程执行。 - 队列满且线程数达到
maximumPoolSize,执行拒绝策略。
这个流程一定要按顺序说,面试官会追问“队列满了为什么是先加线程而不是先拒绝”,答案是:线程池设计思想是先用队列缓冲,再扩线程,最后才拒绝,尽可能保证任务被执行。
5.2 四种拒绝策略
ThreadPoolExecutor内置四种策略:
| 策略 | 行为 |
|---|---|
AbortPolicy | 直接抛RejectedExecutionException |
CallerRunsPolicy | 调用者线程自己执行任务 |
DiscardPolicy | 静默丢弃任务 |
DiscardOldestPolicy | 丢弃队列最老任务,然后重试提交 |
平时默认是AbortPolicy。实际项目里CallerRunsPolicy比较实用,因为它能保留任务,但会对调用方造成背压,需要评估调用方能否承受。
5.3 线程池创建方式
禁用Executors中的快捷方法创建线程池,原因是:
FixedThreadPool和SingleThreadPool任务队列使用了无界LinkedBlockingQueue,可能堆积大量请求导致 OOM。CachedThreadPool线程数无上限,可能创建大量线程导致 OOM。ScheduledThreadPool同样存在线程数上限问题。
面试时直接回答:阿里巴巴开发规范明确建议手动创建ThreadPoolExecutor,明确参数,规避资源耗尽风险。
ThreadPoolExecutor executor = new ThreadPoolExecutor( 4, 8, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<>(1000), new ThreadFactoryBuilder().setNameFormat("biz-thread-%d").build(), new ThreadPoolExecutor.CallerRunsPolicy() );5.4 如何设置线程池参数
这是个高频场景题,没有唯一答案,但可以给出合理思路。任务分两类:
- CPU 密集型:线程数设置为 CPU 核数 + 1。
- IO 密集型:线程数设置为 CPU 核数 * 2,或者按“CPU 核数 / (1 - 阻塞系数)”估算。
阻塞系数高时,线程数可以更多。实际项目中还要考虑下游服务吞吐量和调用超时时间,不能只套公式。
线上排查线程池问题,先看监控指标:活跃线程数、队列积压量、拒绝任务数。如果队列长期积压,优先调大队列容量或增加核心线程数,而不是无脑调大maximumPoolSize。
6. ThreadLocal 与内存泄漏
6.1 ThreadLocal 原理
ThreadLocal为每个线程提供变量副本。每个Thread对象内部有一个ThreadLocalMap,key 是ThreadLocal对象,value 是线程本地变量。
关键设计点:
ThreadLocalMap的 key 是ThreadLocal的弱引用。- value 是强引用。
- 弱引用 key 在 GC 时可能被回收,变成
key == null的 Entry。 - 如果不清理
ThreadLocalMap中的无效 Entry,value 会一直存在,直到线程结束。
6.2 内存泄漏与规避
线程池中的线程是复用的,不会轻易销毁,如果ThreadLocal的 value 不被及时删除,就会发生内存泄漏。
规避手段是:每次用完ThreadLocal都调用remove()。
private static final ThreadLocal<UserContext> USER_CONTEXT = new ThreadLocal<>(); public void process() { try { USER_CONTEXT.set(user); // 业务代码 } finally { USER_CONTEXT.remove(); } }面试回答模板:ThreadLocal的设计是 key 弱引用、value 强引用。弱引用 key 被回收后,value 无法被访问但依然被ThreadLocalMap引用。线程池复用时线程不销毁,value 就形成泄漏。解决方法是显式remove()。
顺带记住ThreadLocal的常见应用:SimpleDateFormat线程安全改造、Spring 事务上下文、TraceId 传递、用户登录信息保存。
7. 并发工具类与阻塞队列
7.1 CountDownLatch 与 CyclicBarrier
两者都是线程协作工具,容易混淆。
CountDownLatch:一个或多个线程等待其他线程完成后再继续,计数递减到 0 时放行。
int taskCount = 5; CountDownLatch latch = new CountDownLatch(taskCount); for (int i = 0; i < taskCount; i++) { executor.submit(() -> { try { // 执行任务 } finally { latch.countDown(); } }); } latch.await(10, TimeUnit.SECONDS);CyclicBarrier:一组线程互相等待,全部到达屏障点后一起继续,可以循环复用。
对比结论:CountDownLatch是一次性的,减计数,适合任务汇总;CyclicBarrier可循环,加计数,适合多线程任务分批执行,比如多阶段并行计算。
7.2 Semaphore
Semaphore控制同时访问共享资源的线程数,适合做限流。
Semaphore semaphore = new Semaphore(3); for (int i = 0; i < 10; i++) { executor.submit(() -> { try { semaphore.acquire(); // 受限资源访问 } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { semaphore.release(); } }); }Semaphore区别于线程池的地方是:线程池限制线程数量,Semaphore限制资源访问并发数,两者可以配合使用。
7.3 阻塞队列
阻塞队列是线程池的核心组成部分。常见实现:
ArrayBlockingQueue:有界数组队列,FIFO。LinkedBlockingQueue:链表队列,可设置容量,默认容量Integer.MAX_VALUE,所以无界时要小心 OOM。SynchronousQueue:不存储元素,每个插入操作必须等待一个移除操作。PriorityBlockingQueue:支持优先级排序。DelayQueue:延迟队列,任务到期后出队。
put()和take()是阻塞方法,一个在队列满时阻塞,一个在队列空时阻塞。生产消费者模型就可以基于这两个方法实现。
7.4 并发容器
ConcurrentHashMap是面试重点。回答这个问题要注意版本差异。
JDK 1.7 中,ConcurrentHashMap用分段锁实现。默认 16 个 Segment,每个 Segment 相当于一个小的HashMap,锁粒度是 Segment。
JDK 1.8 中,组织结构改成数组 + 链表 + 红黑树,锁粒度细化到桶,使用synchronized+ CAS。插入时如果桶为空,直接用 CAS;桶不为空时,锁住桶头节点。因为细化了锁粒度,并发能力比 1.7 更强。
CopyOnWriteArrayList是读写分离思想的实现。写操作时复制底层数组,写完后替换引用;读操作不加锁。适合读多写少的场景,比如白名单、路由配置等。缺点是内存占用高,每次写都会复制数组。
8. Spring Boot 中的多线程场景
8.1 Spring Boot 请求本身是多线程吗
很多候选人被问“Spring Boot 的请求是多线程吗”。准确回答是:
- 每个 HTTP 请求默认由 Tomcat 的线程池中的线程处理。
- Spring Boot 默认使用 Tomcat,这里有最大工作线程数配置,比如
server.tomcat.max-threads。 - 同一个请求内部默认是单线程执行的,除非显式开启异步处理。
- 多个请求之间是并发的,但每个请求自己走一条独立的线程路径。
这里经常会问“Controller 是线程安全的吗”。答案是:默认单例,且不是线程安全的。如果 Controller 里定义了一个可变的实例变量,多个请求同时写入就会产生线程安全问题。正确做法是不在 Controller 中保存可变状态。
8.2 多线程调用外部接口
项目里常见的场景是:主线程需要并行调用多个外部接口,汇总结果后返回。可以这样实现:
public OrderDetail queryOrderDetail(Long orderId) { CompletableFuture<OrderInfo> orderFuture = CompletableFuture .supplyAsync(() -> orderClient.getOrder(orderId), bizExecutor); CompletableFuture<UserInfo> userFuture = CompletableFuture .supplyAsync(() -> userClient.getUser(orderId), bizExecutor); return CompletableFuture.allOf(orderFuture, userFuture) .thenApply(v -> { OrderDetail detail = new OrderDetail(); detail.setOrder(orderFuture.join()); detail.setUser(userFuture.join()); return detail; }) .join(); }这样能把顺序调用的 N 次耗时压到一次并发耗时,但要注意三个问题:
- 外部接口超时设置,避免多个线程同时卡住。
- 线程池参数和下游吞吐量匹配。
- 异常链路要捕获并把错误信息透出到上层。
8.3 CompletableFuture 核心用法
CompletableFuture是Future的增强版,支持编排多个异步任务。
常用方法:
supplyAsync():异步执行有返回值的任务。thenApply():任务完成后转换结果。thenAccept():任务完成后消费结果。exceptionally():异常恢复。allOf():等待所有任务完成。anyOf():任意一个任务完成即可。
需要强调:CompletableFuture默认使用 ForkJoinPool.commonPool,如果任务是阻塞 IO,要传入自定义线程池,避免公共线程池被打满。
9. Java 多线程高频面试题清单
下面整理一份高频题目的“答题骨架”,适合最后一天速记。
9.1 什么是线程安全?如何实现线程安全?
线程安全是指多个线程访问同一个共享变量时,程序行为依然正确。实现方式有:使用局部变量、加锁、使用原子类、使用不可变对象、使用并发容器。不要只说“加锁”,要能列举多种手段。
9.2 为什么不建议用Executors创建线程池
因为没有显式指定线程池参数,容易造成资源耗尽。FixedThreadPool使用无界队列,任务堆积可能 OOM;CachedThreadPool线程数无上限,线程过多也可能 OOM。回答时把ThreadPoolExecutor的七大参数带上,说明手动创建更可控。
9.3synchronized锁的是什么
锁对象取决于用法:
- 修饰实例方法:锁当前实例
this。 - 修饰静态方法:锁当前类的
Class对象。 - 修饰代码块:锁括号里指定的对象。
面试加分点:静态方法和实例方法不能互相影响,因为它们锁对象不同。
9.4 什么是死锁?如何避免?
死锁是多个线程互相持有对方需要的锁,导致互相等待且永不释放。经典条件是互斥、持有并等待、不可剥夺、循环等待。
避免方式:
- 尽量避免嵌套加锁。
- 按固定的锁顺序加锁。
- 使用
Lock的tryLock(timeout),获取不到就放弃。 - 使用并发工具类如
Semaphore、ReentrantLock替代手动多锁。
写一个死锁示例,面试官通常会问“怎么定位”。可以回答:先jps找到进程号,再用jstack <pid>查看线程 dump,能看到Found one Java-level deadlock提示。
9.5i++为什么不是线程安全的
因为i++在字节码层面至少有读取、加一、写回三步,不是原子操作。多个线程同时执行时,会互相覆盖写回结果。解决方式:使用AtomicInteger,或者加synchronized,或者用LongAdder提升高并发计数性能。
9.6wait()和notify()为什么必须在同步代码块中
这是为了确保等待和通知操作与共享变量的修改互斥,避免“丢失通知”问题。如果不在同步块内,线程可能在“检查条件”和“进入等待”之间被打断,另一个线程此时已经执行完notify(),等待线程永远不会被唤醒。
9.7 线程池队列满了会怎样
先判断线程数是否达到maximumPoolSize。没达到就创建非核心线程;达到了就执行拒绝策略。拒绝策略默认抛异常。面试时可以补一句:线程池的设计是先入队再扩线程,这个顺序很关键。
9.8 sleep 和 wait 的区别
sleep是Thread的静态方法,wait是Object的方法;sleep不释放锁,wait释放锁;sleep自动唤醒,wait需要notify或notifyAll唤醒。
9.9 如何实现生产者消费者模型
可以用BlockingQueue实现,这是最简单的方式;也可以用wait/notify原生实现,但要注意使用while循环检查条件,防止虚假唤醒。
9.10 什么是虚假唤醒
线程可能在没收到notify的情况下被唤醒。因此wait()一定要放在while循环中,而不是if中。
10. 多线程场景常见报错与排查
结合热搜词里的java: outofmemoryerror: insufficient memory,多线程场景最容易出现的报错集中在资源耗尽上。
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
OutOfMemoryError: unable to create new native thread | 线程数过多或系统线程限制 | ulimit -u、jstack统计线程数 | 优化线程池参数,减少线程创建 |
OutOfMemoryError: insufficient memory | 堆内存不足,可能是任务队列堆积 | jmap -heap、GC 日志 | 限制任务队列容量,增大堆内存或排查对象泄漏 |
| 线程池任务堆积 | 队列设置不合理或下游响应慢 | 监控队列大小、活跃线程数 | 调整corePoolSize与队列容量,增加超时控制 |
| 接口响应线程卡死 | 线程池拒绝策略触发且未熔断 | 查看拒绝日志、jstack抓线程状态 | 使用CallerRunsPolicy并加熔断降级 |
ThreadLocal内存泄漏导致 Full GC | 未调用remove() | 用 MAT 或jmap分析堆 | 在finally中remove() |
排查多线程问题,优先记住三个工具命令:
jps -l jstack <pid> jstat -gcutil <pid> 1000jstack能看到线程栈、锁等待、死锁和阻塞状态,是排查并发问题最直接的命令。
11. 3 天复习计划与最佳实践
最后给一份可执行的 3 天计划。
11.1 第一天:基础与核心概念
当天目标是把线程生命周期、JMM、volatile、synchronized和Lock过一遍。每学一个概念,手写一个小例子,比如写一个多线程累加的例子,观察volatile为什么不能让count++线程安全。理论必须配合示例代码,否则面试时很难转述成自己的语言。
11.2 第二天:线程池与并发工具
当天重点做三件事:
- 手写一个
ThreadPoolExecutor,把七大参数全部设置成明确值。 - 跑一个
CountDownLatch等待多个任务完成的例子。 - 用
CompletableFuture模拟并行调用两个外部接口。
线程池参数不要死记,要理解变化过程:任务提交时先判断核心线程,再排队,再扩线程,最后拒绝。
11.3 第三天:高频问答、项目场景与排查
第三天不再看新知识点,把手上的高频清单过一遍,挑出 8 个回答最不流畅的题目,写一遍自己的回答稿。同时准备一个多线程项目场景:比如订单详情并行查询、批量任务处理、日志异步上报,把技术点串进去。
11.4 多线程实战最佳实践
- 线程池统一管理,禁止在方法内部随手
new Thread。 - 所有线程池使用有界队列,并明确拒绝策略。
- 原子类不滥用,长耗时场景不要长时间持有锁。
- 批量并发任务要设置总超时时间。
- 关键业务链路加监控,线程池队列深度、活跃线程数、拒绝次数都要有指标。
- 加锁范围尽量小,只锁共享资源操作,不要锁整个方法。
总结与下一步
Java 多线程在面试中出现频率非常稳定,而且容易和项目经验联动。最容易丢分的不是概念本身,而是“概念都知道,但说不清楚应用场景”。复习时每学一个机制,都要问自己:这东西在项目里哪里用到了?如果没用过,写代码的时候实际跑一遍,面试就能给出真实的细节。
建议把这份清单转成自己的速查表格:线程池七大参数、拒绝策略选择、volatile和synchronized的语义边界、ThreadLocal的泄漏原因、并发工具类的适用场景。先把这五块吃透,再扩展AQS、CompletableFuture和 Spring Boot 实战,多线程面试题的核心覆盖就基本到位了。真正面试时,要避免只背结论,尽量用自己的项目案例来佐证,这样比单纯背诵八股文更有说服力。