搜「Java多线程面试题及答案」的人,多数不是没背过答案,而是不确定背下来的那句话到底成不成立,被追问「你依据什么」时也拿不出一段可运行的东西。比较稳妥的做法,是把要回答的结论对应到一段几十行、能当场运行的代码,再配合 JDK 自带工具的输出去核对,而不是只靠记忆复述。线程池的扩容顺序、volatile 的语义边界、wait 与 sleep 是否释放监视器,这几类都能这样核对。
多线程面试题的多数结论可以用可运行代码核对:线程池的扩容顺序看 getPoolSize() 和 getQueue().size() 的打印值,线程状态用 jcmd <pid> Thread.print 查看,wait 与 sleep 是否释放锁,用两个线程争同一把监视器验证。前提是先确认 JDK 版本(java -version),不同版本的线程模型和默认参数不一样。核对不出来的结论,答题时应说明适用条件,而不是当成无条件成立的定论。
多线程面试题及答案:先把能核对的事实摆清楚
这里讨论的对象是常见的几类面试题:ThreadPoolExecutor 的参数与扩容顺序、synchronized 与 Lock 的区别、volatile 能不能保证原子性、wait/notify 与 sleep 的差异、死锁怎么定位。前提条件先固定:JDK 版本、运行平台、是否用了自定义线程工厂。版本只能自己查,位置在 java -version 输出的第一行;单看运行中的进程用 jcmd <pid> VM.version。
能直接核对的现象有三类,都不依赖任何外部资料:
- ThreadPoolExecutor 的公开方法返回值:
getPoolSize()、getActiveCount()、getQueue().size()、getLargestPoolSize()、getCompletedTaskCount()。这些字段能反映当前线程数和排队数。 - 线程转储里的线程名前缀和
java.lang.Thread.State,字段位置在jcmd <pid> Thread.print -l的输出中。 - 源码里的判断分支。以 ThreadPoolExecutor.execute 为例,先看工作线程数是否小于 corePoolSize,再决定是新建线程还是入队,入队失败后才尝试扩到 maximumPoolSize。这一段读源码就能核对,不需要背。
先不下结论的原因是:同一道题的答案随队列类型、拒绝策略、JDK 版本变化。拿这些字段和分支当证据,比记一句话更耐用。
多线程面试题及答案最容易搞错的一点
最常见的一种误解,是把顺序说反了:以为「核心线程满了就扩容到 maximumPoolSize,再往队列里放」。按 execute 的判断顺序,正确理解通常是「核心线程满了先入队,队列也满了才扩容,扩容到上限仍失败才走拒绝策略」。这个分界不是靠记忆判断,而是有一个能把两种情况分开的检查动作:
- 构造
corePoolSize=1、maximumPoolSize=4、new ArrayBlockingQueue<Runnable>(2)的池。 - 连续提交 3 个各自阻塞 3 秒的任务,每提交一个就打印
getPoolSize()和getQueue().size()。 - 看第三次提交后的读数:如果 poolSize 仍是 1、queue 变成 2,说明是先入队;如果 poolSize 涨到 3,说明是先扩容。
这里有一个相关的坑:换成无界队列(比如默认容量的 LinkedBlockingQueue),队列几乎不会满,maximumPoolSize 基本用不上,线程数会停在 corePoolSize。面试时被追问「为什么你配了 max 却没生效」,答案往往就在队列的有界性上。
多线程面试题及答案:一个照着就能做完的做法
把下面这段存成 PoolProbe.java,放在任意空目录执行,它把扩容顺序这个结论变成一次可观察的输出。
import java.util.concurrent.*;
public class PoolProbe {
public static void main(String[] args) throws Exception {
ThreadPoolExecutor pool = new ThreadPoolExecutor(
1, 4, 60L, TimeUnit.SECONDS,
new ArrayBlockingQueue<Runnable>(2));
for (int i = 1; i <= 6; i++) {
final int id = i;
try {
pool.execute(new Runnable() {
public void run() {
System.out.println("task " + id + " start");
try { Thread.sleep(3000L); }
catch (InterruptedException e) { Thread.currentThread().interrupt(); }
}
});
} catch (RejectedExecutionException e) {
System.out.println("task " + id + " rejected");
}
System.out.println("after submit " + id
+ " poolSize=" + pool.getPoolSize()
+ " active=" + pool.getActiveCount()
+ " queue=" + pool.getQueue().size());
}
Thread.sleep(500L);
System.out.println("largestPoolSize=" + pool.getLargestPoolSize());
pool.shutdown();
}
}
执行两步:

javac PoolProbe.java
java -cp . PoolProbe
第 1 步正常结束不输出内容,说明编译通过;第 2 步在提交过程中就能看到前三个任务时 poolSize 与 queue 的变化顺序,第 6 个任务通常打印 rejected——core 1 + 队列 2 + 额外 3 个线程已占满 4 个上限,第 6 个进不来。想确认线上池子的真实状态,在程序还没退出时另开终端:
jcmd -l
jcmd <pid> Thread.print -l
第一条列出可附加的 Java 进程和 pid,第二条才是线程转储。该看到的是以 pool-1-thread- 开头的一组线程,空闲的那几个停在 java.lang.Thread.State: WAITING (parking),正在 sleep 的显示 TIMED_WAITING (sleeping)。
多线程面试题及答案:怎么验证结果对不对
验证分两层,一层看池子读数,一层看线程转储,两者对得上才算核对完成。
池子读数:程序退出前打印的 getLargestPoolSize() 如果停在 1,说明队列一直没满,扩容这一步根本没触发,此时不能说「扩容顺序是错的」,只能说明这个场景没覆盖到;它涨到 4,才说明走了扩容分支。正常运行的样子是 largestPoolSize 与场景预期一致,且 completedTaskCount 随时间递增。
线程转储:在进程存活期间执行 jcmd <pid> Thread.print,正常结果的形态是每个线程一段,头部形如 "pool-1-thread-1" #12 ...,下面跟着状态行和调用栈;空闲核心线程的栈顶通常落在 LinkedBlockingQueue.take 或 park 一类的等待点。排查死锁时看输出里有没有 Found one Java-level deadlock: 段落,有的话下面会列出互相持有的锁和线程名;没有这一段就说明当前时刻没检测到 Java 层死锁,但不代表不存在锁竞争。
需要说明边界:以上能核对的是线程池行为、线程状态和锁等待,像「加个 volatile 就能保证 i++ 正确」这类结论本来就不成立,volatile 只保证可见性和有序性,原子性要靠 AtomicInteger 或加锁;Thread.sleep 不释放已持有的监视器,Object.wait 才释放。这几条换个环境也成立,但线程池参数是否合适,必须结合队列类型、任务耗时和拒绝策略单独确认。