ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Java多线程面试题梳理:从并发原理到线程池实战的3天备考主线

Java多线程面试题梳理:从并发原理到线程池实战的3天备考主线 在准备 Java 面试的时候多线程往往是最容易让人“背了又忘、聊了就崩”的模块。很多候选人能说出synchronized是重量级锁、volatile能保证可见性但面试官一旦追问“锁升级的具体过程”“为什么 DCL 单例要加 volatile”“线程池核心线程数怎么定”瞬间就卡壳。真正的问题不是不努力而是没有把零散知识点串成一条主线。这篇文章不是给你一份新的八股文清单网上已经有很多了。我更想帮你做的是用 3 天时间把多线程面试题按“并发问题根源 → JVM 层解决方案 → JUC 工具 → 生产实践”这条主线重新梳理一遍把高频考点的出题逻辑和表达框架讲清楚让每一道题都能从底层原理说到代码落地。准备面试之前先把这条主线记住后面所有知识点都有了安放位置。建议先收藏这篇文章再按文末的 3 天计划逐步查漏补缺。下面我们进入正题。1. 这篇 Java 多线程面试题梳理到底要解决什么问题先看一个很常见的面试现场面试官问synchronized和ReentrantLock有什么区别候选人回答synchronized是关键字ReentrantLock是类ReentrantLock可以中断、可以公平锁、可以条件锁……这个回答不能算错但它只是“背了两列对比”没有说到本质。面试官真正想听的是你知不知道锁在并发场景里是在保护什么为什么有了synchronized还要设计ReentrantLock以及你在什么业务场景下会做这个选择。这篇文章想解决的问题就是帮你从“背结论”切换到“讲原理”。Java 多线程面试题虽然看起来很多但高频考点其实集中在几条主线上线程基础线程的状态流转、创建方式、start与run的区别。并发三大特性原子性、可见性、有序性以及它们分别由什么机制保证。锁机制synchronized的锁升级、volatile的可见性与禁止重排序、显式锁与 AQS。JUC 工具CountDownLatch、CyclicBarrier、Semaphore、ThreadLocal、CompletableFuture。线程池核心参数、拒绝策略、生产环境如何调优。综合能力如何定位死锁、如何排查线程池耗尽、如何设计一个限流组件。如果你发现自己对这些考点只能“记关键词”说不出“它解决什么问题、底层怎么实现、生产怎么用”那这篇文章就是为你准备的。3 天时间不指望你成为并发专家但足够你掌握一套“面试官怎么问都能接住”的回答框架。2. 线程基础与生命周期先搞清楚并发的最小单元2.1 进程与线程的区别面试官经常从一个看似简单的问题开始进程和线程的区别是什么这里不要只回答“线程是进程内部的一条执行路径”。更好的回答思路是进程是操作系统分配资源内存、文件句柄的基本单位线程是 CPU 调度的基本单位。同一个进程内的线程共享进程的堆和方法区但每个线程有独立的程序计数器、虚拟机栈和本地方法栈。进程之间的隔离性高、切换代价大线程之间的通信更方便通过共享内存但也更容易出现并发问题。这个回答的价值在于它引出了后面两个关键结论线程共享了数据所以才需要锁线程有自己的栈所以才会有线程私有数据的概念比如ThreadLocal的用武之地。2.2 线程的六种状态Java 线程在java.lang.Thread.State枚举中定义了六种状态状态说明进入方式NEW线程创建后尚未启动new Thread(...)RUNNABLE可运行状态可能正在执行也可能在等待 CPU 时间片start()之后BLOCKED阻塞等待监视器锁进入synchronized代码块或方法时锁被占用WAITING无限期等待需要其他线程显式唤醒wait()、join()、LockSupport.park()TIMED_WAITING限时等待到时间自动唤醒sleep(time)、wait(time)、join(time)TERMINATED线程执行完毕run()方法正常结束或抛出未捕获异常面试时建议把“状态之间的流转路径”画在脑子里尤其是RUNNABLE - BLOCKED - RUNNABLE和RUNNABLE - WAITING - RUNNABLE这两种。很多候选人容易混淆的是sleep()不会释放锁所以线程只是进入TIMED_WAITING不会触发锁竞争而wait()会释放锁所以其他线程有机会获得监视器锁。2.3 start() 与 run() 的区别这道题几乎是“送分题”但每年都有候选人答偏。start()的作用是启动一个新线程由 JVM 调用该线程的run()方法。真正执行run()的是新创建的线程。run()是一个普通方法直接调用它不会创建新线程而是在当前线程里同步执行方法体。从源码角度start()会调用本地方法start0()这涉及到 JVM 底层创建操作系统线程直接调用run()根本没有走到start0()自然也就没有多线程效果。这道题的正确答法是先回答作用区别再补充一句“所以尽量不要直接调用run()那等于把异步操作做成了同步操作”。2.4 创建线程的方式比较常见的回答是“三种继承Thread、实现Runnable、实现Callable”。但更好的回答是继承Thread简单但 Java 单继承扩展性差。实现Runnable避免单继承限制但没有返回值也不能抛出受检异常。实现CallableFutureTask有返回值可以拿到执行结果或抛出异常。实际工作中我们通常不直接创建线程而是通过线程池提交任务因为线程的创建和销毁成本很高。我这里给一个简单的代码示例把三种方式放在一起对比// 1. 继承 Thread class MyThread extends Thread { Override public void run() { System.out.println(Thread: Thread.currentThread().getName()); } } // 2. 实现 Runnable class MyRunnable implements Runnable { Override public void run() { System.out.println(Runnable: Thread.currentThread().getName()); } } // 3. 实现 Callable class MyCallable implements CallableString { Override public String call() throws Exception { return Callable result from Thread.currentThread().getName(); } } public class ThreadCreateDemo { public static void main(String[] args) throws Exception { new MyThread().start(); Thread t2 new Thread(new MyRunnable()); t2.start(); FutureTaskString futureTask new FutureTask(new MyCallable()); Thread t3 new Thread(futureTask); t3.start(); System.out.println(futureTask.get()); // 实际开发中推荐线程池 Runnable/Callable ExecutorService pool Executors.newFixedThreadPool(2); pool.execute(() - System.out.println(pool task)); pool.shutdown(); } }这段代码里FutureTask.get()会阻塞等待异步结果面试时可以顺带说明“Future就是用来拿异步执行结果的但它本身是阻塞获取想更灵活可以了解CompletableFuture”。3. 并发三大特性多线程为什么容易出问题如果你只能记住一个“核心原理”那一定是并发三大特性原子性、可见性、有序性。几乎所有并发问题都是这三个特性被破坏造成的。3.1 原子性原子性是指一个操作是不可分割的要么全部执行成功要么全部不执行。比如i看起来是一行代码但在字节码层面它分成“读取 i → i 加 1 → 写回 i”三步不是原子操作。多个线程同时执行i就可能出现值覆盖丢失的情况。解决原子性的常见手段是锁比如synchronized、ReentrantLock以及基于 CAS 的原子类。public class AtomicityDemo { private int count 0; private final AtomicInteger atomicCount new AtomicInteger(0); // 非原子操作多线程下结果可能小于预期 public void unsafeIncrement() { count; } // 通过 synchronized 保证原子性 public synchronized void safeIncrement() { count; } // 通过 CAS 原子类保证原子性 public void atomicIncrement() { atomicCount.incrementAndGet(); } }面试时可以强调AtomicInteger底层是 CAS volatileCAS 失败会自旋重试适合并发冲突不激烈的场景如果冲突非常激烈CAS 自旋会消耗大量 CPU这时反而不如锁。3.2 可见性可见性是指一个线程修改了共享变量其他线程能不能立即看到这个修改。现代 CPU 有多级缓存线程操作变量时可能先读的是缓存里的值缓存没有及时刷新到主内存或者主内存的值没有及时失效都会导致“读到的不是最新值”。volatile关键字就是解决可见性的它保证了对变量的写操作会立即刷新到主内存读操作会从主内存读取。注意volatile不保证原子性所以volatile int i的i依然不是安全的。3.3 有序性有序性是指程序执行顺序符合代码逻辑顺序。但编译器和 CPU 为了优化性能可能会对指令进行重排序。在单线程下重排序不会影响最终结果但在多线程环境下重排序可能导致其他线程读到“看起来不符合逻辑”的状态。volatile除了保证可见性还能通过内存屏障禁止指令重排序。这就是著名的 DCLDouble Check Lock单例模式为什么要加volatile的原因。3.4 三大特性与 Java 内存模型的关系上面三个特性都跟 Java 内存模型密切相关。JMMJava Memory Model定义了线程与主内存之间的抽象关系每条线程有自己的工作内存工作内存中保存了主内存变量的副本线程对变量的所有操作都必须在工作内存中进行不能直接操作主内存。这个定义引出的结论是synchronized和volatile本质上都是在约束线程工作内存与主内存之间的同步行为。synchronized在进入同步块时清空工作内存在退出时把修改刷新到主内存volatile在每次读写时都强制同步到主内存。面试时能把这个“线程工作内存 → 主内存”的模型讲清楚说明你是真理解而不是背概念。4. synchronized 与锁机制面试必问的锁升级过程4.1 synchronized 的三种用法synchronized是 Java 内置的互斥锁用法有三种修饰实例方法锁住的是当前实例对象。修饰静态方法锁住的是当前类的 Class 对象。修饰代码块可以指定任意对象作为锁。注意“类锁”和“对象锁”是两个不同维度的锁。静态方法锁的是类对象实例方法锁的是具体实例对象它们是两把不同的锁。这经常是面试官追问的点问一个类有 synchronized 静态方法和 synchronized 实例方法两个线程分别调用它们会互斥吗答不会。静态方法持有的是类级别的锁Class 对象实例方法持有的是对象级别的锁二者没有竞争关系。4.2 锁升级的过程偏向锁 → 轻量级锁 → 重量级锁锁升级是高频中的高频但这个点很容易被讲乱。我用一个简单版本说明适合面试表达早期synchronized是重量级锁直接依赖操作系统 mutex线程阻塞唤醒要切到内核态开销很大。HotSpot 后来做了优化引入了锁升级机制。偏向锁只有一个线程反复进入同步块时JVM 在对象头 Mark Word 中记录线程 ID之后该线程再次进入时不需要 CAS 操作。如果出现其他线程竞争偏向锁才会撤销。轻量级锁竞争出现后偏向锁升级为轻量级锁。线程在栈帧中创建锁记录通过 CAS 尝试将对象头 Mark Word 替换为指向锁记录的指针。如果 CAS 成功表示拿到锁失败则说明有竞争开始自旋。重量级锁自旋超过一定次数或竞争线程数量太多轻量级锁膨胀为重量级锁后续线程直接阻塞等待操作系统唤醒。面试表达建议这样说锁升级不是为了锁效率的“锦上添花”而是为了在不同竞争强度下都能尽量用最低成本实现线程安全。单线程场景用偏向锁低竞争场景用 CAS 自旋的轻量级锁高竞争场景才把线程挂起。还要提醒一点较新的 JDK 版本对偏向锁的默认策略已经做了调整部分版本默认关闭偏向锁更依赖轻量级锁与自适应自旋。面试时主动提到“JDK 版本不同锁优化策略会有差异”是加分项说明你关注版本演进而不是死背书。4.3 死锁的产生与排查死锁是锁机制里最经典的场景题两个线程各自持有一把锁同时等待对方释放锁就会互相“卡死”。代码示例public class DeadLockDemo { private static final Object LOCK_A new Object(); private static final Object LOCK_B new Object(); public static void main(String[] args) { new Thread(() - { synchronized (LOCK_A) { System.out.println(线程1持有A等待B); try { Thread.sleep(100); } catch (InterruptedException e) { } synchronized (LOCK_B) { System.out.println(线程1拿到B); } } }, 线程1).start(); new Thread(() - { synchronized (LOCK_B) { System.out.println(线程2持有B等待A); try { Thread.sleep(100); } catch (InterruptedException e) { } synchronized (LOCK_A) { System.out.println(线程2拿到A); } } }, 线程2).start(); } }真遇到死锁时第一步是用jps找到进程 ID再用jstack pid导出线程快照看到Found one Java-level deadlock的输出就能定位到具体线程和锁。预防死锁的常规手段是统一加锁顺序、避免嵌套锁、使用tryLock加超时。5. volatile 解决可见性问题DCL 单例为什么必须加它volatile在面试题里的地位不亚于synchronized。很多候选人能说出“volatile 保证可见性、禁止指令重排序但不保证原子性”可一到代码层面还是说不清为什么 DCL 单例要加 volatile。先看经典写法public class Singleton { // 为什么必须加 volatile 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; } }问题出在instance new Singleton()这一步。它并不是一个原子操作在字节码层面大致分为三步为Singleton分配内存空间。调用构造方法初始化对象字段。将instance引用指向刚刚分配的内存地址。如果 CPU 和编译器发生了指令重排序步骤 2 和 3 可能交换顺序。也就是说instance先指向了一块尚未初始化完成的内存。此时另一个线程进来在第一层检查时发现instance ! null直接返回了半初始化的对象调用其方法就可能出问题。加了volatile之后instance的写入操作会插入内存屏障禁止步骤 2 和 3 重排序。这就是 DCL 必须依靠volatile的原因。面试官如果继续追问“那不用 volatile 行不行”可以回答在 JDK 5 之前volatile的语义不够强DCL 仍可能有问题JDK 5 之后 JMM 修订volatile可以保证这种场景的安全。另外也有替代方案比如用静态内部类实现懒加载单例或者直接用枚举单例。6. 线程池生产环境最常问的 Java 多线程考点线程池几乎是每场 Java 面试必考的内容因为它最能区分“只会写 demo”和“真正在项目里处理过并发”的候选人。6.1 为什么不建议直接 new Thread很多人知道答案线程的创建和销毁开销大而且大量线程会占用内存、增加上下文切换、带来不可控的资源消耗。但更好的表达是new Thread每次都会创建一个操作系统线程线程执行结束后就被回收没有复用机制高并发下会频繁创建销毁性能很差。线程数量没有上限容易把系统资源耗尽甚至触发OutOfMemoryError。缺乏统一的管理机制比如定时执行、定期执行、并发数控制、拒绝策略等。线程池的核心价值是“复用线程 控制流量”。它预先创建一批线程让任务在已有线程上执行避免频繁创建销毁同时通过队列和核心线程数、最大线程数来限制并发规模。6.2 ThreadPoolExecutor 核心参数面试常问的是ThreadPoolExecutor的构造参数可以直接列成一个表格参数作用说明corePoolSize核心线程数即使空闲也保留的线程数量maximumPoolSize最大线程数线程池允许创建的最大线程数量keepAliveTime线程空闲存活时间超过核心线程数的线程空闲多久后被回收workQueue任务队列提交给线程池但尚未执行的任务存放队列threadFactory线程工厂创建新线程的工厂可以自定义线程名handler拒绝策略队列满且线程数达到 maximumPoolSize 时的处理策略核心参数的工作流程也要会答提交任务时如果当前线程数小于corePoolSize创建核心线程执行任务。如果当前线程数大于等于corePoolSize任务进入workQueue。如果队列已满且线程数小于maximumPoolSize创建非核心线程执行任务。如果线程数已经达到maximumPoolSize执行拒绝策略。这个流程非常容易被颠倒建议死记住先核心线程再入队列再开非核心线程最后拒绝。6.3 常见拒绝策略AbortPolicy默认策略直接抛出RejectedExecutionException。CallerRunsPolicy由调用者所在线程执行任务相当于变相降速。DiscardPolicy丢弃任务不抛出异常。DiscardOldestPolicy丢弃队列中最早的任务然后尝试重新提交当前任务。实际项目中我会更关注CallerRunsPolicy因为它能在系统过载时实现“背压”让提交任务的线程自己承担一部分任务而不是默默丢掉业务数据。6.4 完整代码示例不推荐使用Executors里的newFixedThreadPool或newCachedThreadPool因为它们用的是无界队列或没有限制的最大线程数容易导致OutOfMemoryError。更推荐手动创建ThreadPoolExecutor明确参数含义import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.ThreadFactory; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; public class ThreadPoolDemo { public static void main(String[] args) { ThreadFactory threadFactory new ThreadFactory() { private final AtomicInteger seq new AtomicInteger(0); Override public Thread newThread(Runnable r) { Thread t new Thread(r); t.setName(biz-thread- seq.incrementAndGet()); t.setDaemon(false); return t; } }; ThreadPoolExecutor pool new ThreadPoolExecutor( 4, // 核心线程数 8, // 最大线程数 60L, TimeUnit.SECONDS, // 空闲线程存活时间 new ArrayBlockingQueue(1000), // 有界队列 threadFactory, // 自定义线程工厂 new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略 ); for (int i 0; i 20; i) { final int taskId i; pool.execute(() - { System.out.println(Thread.currentThread().getName() 执行任务: taskId); }); } pool.shutdown(); } }这段代码里有两个值得在面试时展开的点为什么用ArrayBlockingQueue而不是无界队列无界队列会让任务无限堆积最终导致内存耗尽有界队列能在系统过载时触发拒绝策略。为什么自定义ThreadFactory生产环境一定要给线程起有意义的名字否则出问题时jstack里全是pool-1-thread-1完全无法定位是哪个业务模块的线程。6.5 核心线程数怎么设置这道题没有标准答案但面试时可以按场景给出判断CPU 密集型任务线程数可以接近 CPU 核心数避免太多线程造成频繁上下文切换。比如N 1。IO 密集型任务线程大部分时间在等待 IO可以适当多配置比如N * 2或更高具体要看 IO 等待占比。混合型任务可以考虑拆分成 CPU 密集和 IO 密集两个线程池分别设置参数。更好的说法是线程数没有万能公式需要结合压测结果调整。先通过估算拿到初始值再在测试环境下压测观察 CPU 使用率、队列积压情况和响应时间逐步调整。7. JUC 高频工具与对比题从源码到场景一次讲清7.1 ReentrantLock 与 synchronized 的区别synchronized是关键字由 JVM 内置实现ReentrantLock是 JUC 提供的类基于 AQS 实现。synchronized不需要手动释放锁ReentrantLock必须手动加锁和释放锁一般配合try/finally。ReentrantLock支持可中断获取锁、支持超时获取锁、支持公平锁和非公平锁synchronized只能是非公平锁。ReentrantLock可以通过newCondition()创建多个条件队列实现更精细的等待/唤醒synchronized配合wait/notify只能有一个条件队列。这里的关键不是背差异而是能说出应用场景如果需要可中断、可限时的锁竞争或者需要多个条件队列实现复杂协调选ReentrantLock日常简单的互斥场景直接synchronized就够了代码更简洁还会自动释放锁。7.2 CountDownLatch、CyclicBarrier、Semaphore这三个工具的用途经常被搞混。CountDownLatch一个或多个线程等待其他线程完成一组操作。计数器只能减少不能重置。典型场景是主线程等待多个子任务都执行完再继续。CyclicBarrier多个线程互相等待直到所有线程都到达屏障点然后一起继续执行。计数器可以循环使用适合“分批齐头并进”的场景。Semaphore信号量控制同时访问资源的线程数量。典型场景是限流比如数据库连接池、接口并发限制。7.3 ThreadLocal 与内存泄漏问题ThreadLocal常被问到的不是“能干什么”而是“会导致什么问题”。每个线程持有自己的ThreadLocalMapkey是弱引用但value是强引用。如果线程长期存活而ThreadLocal对象已经不可达value就始终无法被回收造成内存泄漏。标准做法是使用完ThreadLocal后在finally块中调用remove()清掉当前线程的副本。尤其在线程池场景下线程会被复用如果不清理下一次任务可能读到上一次任务残留的值造成比较隐蔽的业务 bug。7.4 CompletableFuture如果面试聊到异步编程CompletableFuture是绕不开的 JDK 8 新工具。它解决了FutureTask.get()阻塞等待的问题支持回调编排、异步组合、异常处理。import java.util.concurrent.CompletableFuture; public class CompletableFutureDemo { public static void main(String[] args) throws Exception { CompletableFutureString future CompletableFuture.supplyAsync(() - { try { Thread.sleep(1000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } return hello; }).thenApply(s - s world) .exceptionally(ex - error: ex.getMessage()); System.out.println(future.get()); } }面试时可以补充CompletableFuture默认使用ForkJoinPool.commonPool()如果任务是 IO 密集型的建议传入自定义线程池避免和系统中的其他并行任务争抢公共线程池。8. 高频道题反复考查场景以及如何作答除了原理细节这轮准备有一个更重要的目标让自己在真实面试里把零散知识点组织成可演讲的答案。这里我把常见问题归成三种“表达模板”。8.1 模板一概念对比型比如“sleep和wait有什么区别”。回答框架三步走先说定义sleep是Thread的静态方法wait是Object的实例方法。说关键区别sleep不释放锁wait会释放锁sleep到时间自动唤醒wait需要被notify/notifyAll唤醒或设置超时。说场景如果只想暂停线程且不释放锁用sleep如果需要多线程协调、等待某个条件达成用wait/notify。8.2 模板二为什么型比如“为什么线程池不能使用Executors的快捷方法”。回答框架先说它的问题newFixedThreadPool用了无界LinkedBlockingQueuenewCachedThreadPool最大线程数是Integer.MAX_VALUE。说后果任务堆积或线程无限制创建可能导致 CPU 飙升、内存耗尽甚至 OOM。说正确做法手动创建ThreadPoolExecutor根据业务配置队列大小和拒绝策略。8.3 模板三场景设计型比如“有一个接口上游瞬间会打过来大量请求你怎么设计”回答框架明确目标保护下游系统不被冲垮同时尽量保证请求成功率。分层原则接入层做限流比如 Sentinel、网关限流应用层有兜底调用下游处要设置超时时间和线程池隔离。具体落地考虑信号量控制并发、线程池做排队、拒绝策略选择CallerRunsPolicy或抛异常让上层降级。验证方式压测看耗时、失败率、系统线程数量再调参。这种表达方式能让面试官觉得你不是在背八股文而是真的有工程设计思路。9. 常见问题与排查思路很多候选人背题很顺但面试题稍作变形就慌了。这里汇总几个常见“翻车场景”和对应的修正方向。问题现象可能原因排查/修正方式能把每个概念说一遍但串不起来缺少主线知识点之间没有关联按“Java 内存模型 → 三大特性 → synchronized/volatile → JUC → 线程池”顺序重新梳理i加 volatile 后仍然线程不安全混淆了可见性和原子性记住 volatile 只能保证可见性/有序性不能替代锁或原子类DCL 单例不知道为什么要加 volatile不知道 new 对象不是原子操作自己手写 DCL画出字节码三步骤说明重排序风险线程池参数记混流程答错没有任务提交流程的“肌肉记忆”按四步走核心线程 → 队列 → 非核心线程 → 拒绝策略重复表达直到流畅死锁题目只知道原理不会排查缺少 JVM 工具使用经验本地写死锁 demo用jpsjstack实践一遍手动创建线程池后不知道如何关闭没有掌握优雅停机用shutdown()停止接收新任务用awaitTermination等待已有任务完成如果项目里已经能用上这些排查方法面试时讲出来会天然比“纯背诵”更有说服力。建议在准备期间刻意写几个会“出事”的小 demo亲自触发一次死锁或者线程池拒绝这个过程比背十道题更有价值。10. 3 天备考计划从主线到实战的落地安排考虑到大多数读者是边工作边准备面试这里给出一套 3 天闭环计划。核心思路是第一天搭框架第二天攻重点第三天做表达和实战训练。10.1 第一天建立并发主线上午复习 Java 内存模型、三大特性、线程状态。下午把synchronized的锁升级、volatile的 DCL、wait/notify从概念到代码过一遍。晚上把sleepvswait、startvsrun、对象锁 vs 类锁这三组对比题用“先定义、再核心区别、最后场景”的模板各写一遍口述稿。10.2 第二天掌握线程池与 JUC上午手写ThreadPoolExecutor参数配置跑一个带打印日志的任务池观察核心线程、队列、最大线程数的配合。下午过一遍ReentrantLock、CountDownLatch、CyclicBarrier、Semaphore、ThreadLocal、CompletableFuture的代码示例。晚上重点做线程池参数、拒绝策略、核心线程数设置的“口述练习”要能对着白板画出任务提交流程图。10.3 第三天模拟面试与排错实战上午找一套 Java 多线程面试题不看答案用口述方式回答每个问题控制在 2 到 3 分钟。下午写一个死锁 demo用jpsjstack排查再写一个模拟线程池队列满、触发拒绝策略的场景观察异常日志。晚上把回答不流畅的问题重新整理成自己的话而不是直接背诵别人的解析。多线程面试题准备到此可以形成一个比较完整的闭环。如果你只有零散时间也可以把上面的计划压缩成“每天 2 小时 周末 4 小时”关键不是时间长短而是每天都要有“输入 → 输出”的循环。只看不写、只看不说很难在面试高压下自然表达出来。11. 几个容易踩坑的细节与加分表达最后这部分我想提醒几个在面试表达中容易被忽略但非常加分的细节。第一提到synchronized锁升级时可以补充一句“锁升级是一个动态过程不是所有场景都会走到重量级锁”。这能让面试官觉得你知道锁优化的适用条件。第二讲线程池时一定要体现“先压测再定参”的工程观。与其背一堆参数公式不如说清楚自己会在测试环境做线程数梯度压测观察 CPU、内存、RT 和队列堆积再来确定 corePoolSize 和 maximumPoolSize。第三讨论死锁时不要只停留在理论。主动说“排查时我会先看 jstack 输出重点关注 BLOCKED 状态的线程和锁持有关”会明显比背诵死锁四个必要条件更贴近真实工作。第四如果你准备的是高级岗位还可以主动提一句“高并发场景下可能需要结合消息队列削峰、缓存击穿保护、多级限流等手段单靠线程池并不能解决所有问题”。这会让你的回答有架构视角。希望这篇 Java 多线程面试题梳理能帮你在短时间内抓住主线。去准备吧按 3 天计划走一遍比在收藏夹里吃灰要有效得多。
返回列表