Java多线程面试题及答案

文章导读
准备「Java多线程面试题」时,能背出 volatile 和 synchronized 的区别通常不够用。面试官往下追问线程池的 corePoolSize 到底怎么生效、日志里怎么看出来、为什么任务一直卡在队列里不执行,只背定义的人往往在这里答不上来。判断自己有没有真掌握,最省事的办法是在本机把一段线程池代码跑起来:先用 java -version 确认 JDK 版本,再打印 poolSize 与
📋 目录
  1. Ⅰ 多线程面试题:先把能核对的事实摆清楚
  2. Ⅱ 多线程面试题最容易搞错的一点
  3. Ⅲ 多线程面试题:一个照着就能做完的做法
  4. Ⅳ 多线程面试题:怎么验证结果对不对
A A

准备「Java多线程面试题」时,能背出 volatile 和 synchronized 的区别通常不够用。面试官往下追问线程池的 corePoolSize 到底怎么生效、日志里怎么看出来、为什么任务一直卡在队列里不执行,只背定义的人往往在这里答不上来。判断自己有没有真掌握,最省事的办法是在本机把一段线程池代码跑起来:先用 java -version 确认 JDK 版本,再打印 poolSize 与 queue.size(),最后用 jstack 核对线程状态;这三步能把“我以为”和“实际如此”分开。

线程池这类题先落到可核对的事实上:执行顺序通常是核心线程 → 任务队列 → 扩容到 maximumPoolSize → 拒绝策略,队列无界时永远走不到扩容那一步。适用场景是 ThreadPoolExecutor 及其常见封装;操作上先跑一段打印 poolSize 和 queue.size() 的代码;验证方式是 jstack 对比线程名与数量;边界在于不同 JDK 版本实现细节可能有差异,以本机 $JAVA_HOME 下的源码为准。

多线程面试题:先把能核对的事实摆清楚

这页讨论的对象是 java.util.concurrent.ThreadPoolExecutor,前提是一台能装 JDK 的机器,并且你能自己编译运行 Java 文件。先确认版本和源码位置,这两项是最容易被含糊过去的事实:

java -version
echo $JAVA_HOME
ls $JAVA_HOME/lib/src.zip

可核对的现象集中在几个 API 字段上:getPoolSize() 返回当前线程数,getQueue().size() 返回排队任务数,getActiveCount() 返回正在执行的任务数,getTaskCount() 返回累计提交数。这些是运行时返回值,不是推断。另一个可核对的位置是线程栈:jstack 或 jcmd <pid> Thread.print 输出里能看到线程名和 java.lang.Thread.State,输出可以重定向到文件,比如 /tmp/probe.jstack.txt,留作对比。规则层面的依据在 $JAVA_HOME/lib/src.zip 里的 ThreadPoolExecutor.java,重点看 execute() 方法;JDK 各版本的具体行为建议以本机这份源码为准,先不下结论。

多线程面试题最容易搞错的一点

最常见的误解是:任务一多,线程池会先扩容到 maximumPoolSize,再开始排队。实际顺序是核心线程满了之后先进队列,队列满了才扩容到 maximumPoolSize,两者都满才触发拒绝策略。判断分界就在队列有没有容量上限:用 Executors.newFixedThreadPool(n) 拿到的是无界 LinkedBlockingQueue,maximumPoolSize 形同虚设,线程数基本停在 core 上,任务全堆在队列里;换成有界队列,扩容才会发生。

Java多线程面试题及答案

能把这两种情况区分开的检查动作只有一个:同一个线程池配置下,提交任务时同步打印 pool.getPoolSize() 和 pool.getQueue().size()。如果第 3、4 个任务进来时 queue 在涨而 poolSize 不动,说明先入队;如果 queue 满了之后 poolSize 才变大,说明扩容发生。反过来,把队列换成无界再跑一次,poolSize 长时间停在 core 数量、queue 一直增长,就是面试里常被追问的那种写法。

多线程面试题:一个照着就能做完的做法

下面这段代码能同时验证入队顺序和扩容时机。把它存成 /tmp/mt/PoolProbe.java,队列容量故意设成 2,core 设 2、max 设 4:

Java多线程面试题及答案
import java.util.concurrent.*;

public class PoolProbe {
    public static void main(String[] args) throws Exception {
        ThreadPoolExecutor pool = new ThreadPoolExecutor(
            2, 4, 60L, TimeUnit.SECONDS,
            new LinkedBlockingQueue<>(2),
            new ThreadFactory() {
                private int i = 0;
                public Thread newThread(Runnable r) {
                    return new Thread(r, "probe-" + (++i));
                }
            });
        for (int i = 1; i <= 6; i++) {
            final int id = i;
            pool.execute(() -> {
                try { Thread.sleep(20000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
            });
            System.out.println("提交第" + id + "个任务 queue=" + pool.getQueue().size()
                + " poolSize=" + pool.getPoolSize());
        }
        Thread.sleep(30000);
        pool.shutdown();
    }
}
  1. 编译:cd /tmp/mt && javac -encoding UTF-8 -d out PoolProbe.java,这一步应当无输出,出现编译错误先修代码。
  2. 运行:java -cp out PoolProbe,看到 6 行“提交第 N 个任务”,先记下每行 queue 和 poolSize 的数值。
  3. 抓线程栈:另开一个终端执行 jps -l 找到 PoolProbe 的 pid,再执行 jstack -l <pid> > /tmp/probe.jstack.txt,这一步应当落下一个非空文件。
  4. 对照:grep -c '"probe-' /tmp/probe.jstack.txt 统计线程数,和前面打印的 poolSize 对得上,说明线程确实创建了。

任务体里的 Thread.sleep(20000) 是留给抓栈的窗口,别删掉,也不要只看程序退出后的状态。jstack 需要 JDK 而不是 JRE,容器里一般还要用同一个用户执行,否则可能报找不到 pid 或无权限,这是实际操作里最容易卡住的一步。

多线程面试题:怎么验证结果对不对

判断标准就写在输出里。正常的一份打印大致是这样(具体数值顺序可能因线程创建时机略有差别):

提交第1个任务 queue=0 poolSize=1
提交第2个任务 queue=0 poolSize=2
提交第3个任务 queue=1 poolSize=2
提交第4个任务 queue=2 poolSize=2
提交第5个任务 queue=2 poolSize=3
提交第6个任务 queue=2 poolSize=4

关键是前四个任务提交后 poolSize 不超过 core 数量、queue 在增长;第五个任务之后 poolSize 才超过 core。符合这个形状,说明你对入队与扩容顺序的理解是对的。再打开 /tmp/probe.jstack.txt,正常的栈里应当出现名为 probe-1 到 probe-4 的线程,状态多为 TIMED_WAITING (sleeping);如果线程数少于打印的 poolSize,或者线程名对不上,先怀疑抓栈时机太早或太晚。反例也很好认:queue 一直涨而 poolSize 停在 2,基本可以判定用的是无界队列,maximumPoolSize 没起作用;直接抛 RejectedExecutionException,说明队列和 maximumPoolSize 都已满,拒绝策略生效了。把这两组现象对应到面试回答里,比复述定义更经得起追问;版本相关的细节,仍建议回到本机 $JAVA_HOME/lib/src.zip 核对 execute() 的实现。