8月求职季,Java 后端岗位面试里最容易被连环追问、也最容易暴露基本功的,就是多线程和并发编程。很多人在简历上写“熟悉多线程”,结果被问线程池参数、锁升级、JMM 可见性、ThreadLocal 内存泄漏,直接卡壳。
这篇文章不搞概念堆砌,直接按“面试官会怎么问、你怎么答、背后考什么”来拆。重点覆盖线程基础、JUC 工具、线程池、锁机制、内存模型、并发实战、答题话术和避坑清单。全文不跑偏、不灌水,3 天内能把这里面的题过完,多线程这一块就能撑住面试主战场。
1. 核心能力速览
| 能力项 | 说明 |
|---|---|
| 适用岗位 | Java 后端开发、校招/社招、初中级进阶 |
| 核心考点 | 线程基础、JUC 工具、线程池、锁、JMM、并发容器 |
| 实战能力 | 手写生产者消费者、阻塞队列、线程池提交任务 |
| 知识门槛 | 需掌握 Java 基础语法,理解类和对象概念 |
| 面试难度 | 从概念题到原理题再到手写代码,逐级加深 |
| 准备周期 | 高密度集中学习 3 天,长期需配套刷题和项目落地 |
| 高频追问 | volatile 可见性、synchronized 锁升级、线程池拒绝策略 |
| 扩展方向 | JUC 源码阅读、并发编程实战、性能调优、分布式锁设计 |
需要说明:多线程面试不依赖显卡、GPU 这类硬件,真正影响答题质量的是“代码示例+原理推导+场景落地”的组合。所以这篇文章会用大量可运行的 Java 代码片段和面试问答对照,帮你在短时间内建立面试表达体系。
2. 适用场景与学习边界
多线程知识点在 Java 面试里不是单独存在的,它和 JVM、MySQL 锁、Redis 分布式锁、消息队列消费者设计都会交叉。学这部分内容前,先明确哪些场景迫切需要:
- 简历里写了“熟悉 Java 并发编程”“项目中使用多线程处理批量任务”;
- 面试岗位明确要求并发编程经验,比如交易系统、订单系统、消息系统;
- 项目里有异步处理、定时任务、线程池使用,但解释不清楚原理;
- 准备大厂面试,阶段刷题需要整合一份多线程题库。
不适合什么场景?如果 Java 基础还比较薄弱,连集合框架、异常处理、IO 都不熟,这时候直接背多线程八股文效果很差,容易只记术语、答不出为什么。建议先补基础语法,再进入并发体系。
同时要明确边界:面试题整理替代不了真实开发能力。多线程不是靠背出来的,是要在线程安全、性能、容错之间做取舍。实际项目里高并发批量任务必须考虑线程池参数、任务队列容量、拒绝策略、监控和优雅关闭,这部分我在第 8 章和第 9 章会展开讲。
3. 环境准备与前置条件
多线程代码验证不需要额外硬件,只需要一个能随时运行 Java 代码的环境。
3.1 环境清单
| 项目 | 要求 |
|---|---|
| JDK | 推荐 JDK 8 或 17,验证锁升级用 8+,记录分类用 17+ |
| 开发工具 | IDEA 社区版 / Eclipse / VS Code + Java 插件 |
| 构建工具 | Maven 或 Gradle,非必需,临时测试可用命令行 javac/java |
| 内存 | 至少 4GB 可用内存,线程池测试不要开过大线程数 |
| 操作系统 | Windows / macOS / Linux 均可 |
检查 JDK 版本:
java -version javac -version如果还没配置环境变量,建议先安装 JDK 并设置JAVA_HOME,然后确认 IDEA 里 Project SDK 已指向正确 JDK。
3.2 验证环境能不能跑线程
先写一个最小线程程序验证环境:
public class ThreadTest { public static void main(String[] args) { Runnable task = () -> { System.out.println(Thread.currentThread().getName() + " running"); }; new Thread(task, "worker-1").start(); System.out.println("main thread done"); } }如果环境正常,输出里会看到main thread done和worker-1 running,顺序不固定,这正好说明线程调度不可预测。
3.3 学习素材准备
- 官方文档:Java Concurrency 相关 Javadoc
- 源码:
java.util.concurrent包下的ThreadPoolExecutor、AQS、ConcurrentHashMap - 项目案例:本机起一个 Spring Boot 工程,用线程池跑批量任务观察效果
- 刷题网站:牛客、力扣的多线程题目,比如交替打印、哲学家就餐
不建议一开始只看 PDF 或纯文字笔记,多线程面试题必须配合代码跑一遍,否则回答“volatile 到底能不能保证原子性”这种问题时,很容易答混。
4. 线程基础高频题拆解
面试官开场的多线程题目通常不难,但每一道都在筛选你到底是“用过”还是“理解”。
4.1 进程和线程的区别
答法:进程是操作系统进行资源分配的基本单位,有独立的内存空间;线程是 CPU 调度的基本单位,属于进程,共享进程的内存空间和资源文件。
面试官追问:Java 多线程是并发还是并行?
答:两者都有。单核 CPU 上多个线程是并发执行,宏观上是同时运行,微观上是时间片切换;多核 CPU 上多个线程可以真正并行。Java 线程的调度由操作系统负责,start()只是把线程交给调度器,不保证立即执行。
4.2 Java 创建线程有几种方式
标准答法有 3 种,加分答法有 4 种:
- 继承
Thread类,重写run() - 实现
Runnable接口 - 实现
Callable接口,配合FutureTask - 通过线程池创建,本质还是前几种的实现封装
示例:
import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.FutureTask; public class ThreadCreateDemo { public static void main(String[] args) throws ExecutionException, InterruptedException { // 1. 继承 Thread Thread t1 = new Thread() { @Override public void run() { System.out.println("Thread extend"); } }; t1.start(); // 2. Runnable Thread t2 = new Thread(() -> System.out.println("Runnable run"), "runnable-demo"); t2.start(); // 3. Callable + FutureTask Callable<String> callable = () -> "callable result"; FutureTask<String> futureTask = new FutureTask<>(callable); Thread t3 = new Thread(futureTask); t3.start(); System.out.println(futureTask.get()); } }加分点:面试官问“run() 和 start() 区别”,直接说start()会启动新线程并回调run();直接调用run()只是在当前线程执行普通方法,没有创建新线程。
4.3 线程生命周期
线程状态有 6 种:NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED。
容易出错的地方是Runnable和Blocked的关系。Java 里运行中线程处于RUNNABLE,可能包含操作系统层面的运行和就绪;BLOCKED专门指等待监视器锁;WAITING是 wait/join/park;TIMED_WAITING是带超时的等待。
一个可以直接贴到面试题答案里的例子:
public class ThreadStateDemo { public static void main(String[] args) throws Exception { Object lock = new Object(); Thread waitingThread = new Thread(() -> { synchronized (lock) { try { lock.wait(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } }); waitingThread.start(); Thread.sleep(200); // 此时 waitingThread 处于 WAITING 状态 System.out.println(waitingThread.getState()); Thread blockedThread = new Thread(() -> { synchronized (lock) { // block here } }); blockedThread.start(); Thread.sleep(200); // 此时 blockedThread 大概率是 BLOCKED 状态 System.out.println(blockedThread.getState()); } }如果你能画出状态流转图,再说出BLOCKED和WAITING的触发场景,面试官基本认可状态机掌握扎实。
4.4 sleep、join、yield 的区别
sleep:让线程睡眠指定时间,期间不释放锁,到点进入可运行状态。join:当前线程等待调用 join 的线程执行完,再继续执行。yield:当前线程让出 CPU,但调度器可以忽略,不释放锁,常用于调试和并发测试。
高频追问:sleep和wait的区别。
标准答法:sleep是 Thread 静态方法,不释放锁;wait是 Object 方法,必须在持有对象的监视器锁时调用,释放锁并进入等待队列。wait/notify主要用于线程间协作,sleep主要用于模拟耗时操作和控制节奏。
加分答法:从“线程安全和公平性”角度补充,wait必须在 synchronized 块中使用,否则会抛IllegalMonitorStateException,这是很多新手容易踩的坑。
4.5 wait/notify 生产者消费者
手写生产者消费者是面试常见题,重点是 use while 不是 if,防止虚假唤醒。
import java.util.LinkedList; import java.util.Queue; public class ProducerConsumerDemo { private static final int CAPACITY = 5; private final Queue<Integer> queue = new LinkedList<>(); public synchronized void produce(int value) throws InterruptedException { while (queue.size() == CAPACITY) { wait(); } queue.offer(value); System.out.println("Produced: " + value + ", queue size: " + queue.size()); notifyAll(); } public synchronized int consume() throws InterruptedException { while (queue.isEmpty()) { wait(); } int value = queue.poll(); System.out.println("Consumed: " + value + ", queue size: " + queue.size()); notifyAll(); return value; } public static void main(String[] args) { ProducerConsumerDemo demo = new ProducerConsumerDemo(); Thread p1 = new Thread(() -> { for (int i = 0; i < 10; i++) { try { demo.produce(i); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } }, "producer"); Thread c1 = new Thread(() -> { for (int i = 0; i < 10; i++) { try { demo.consume(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } }, "consumer"); p1.start(); c1.start(); } }这段代码覆盖了线程协作、锁、while 判断三大考点。如果面试官让你再优化,就从“改用 Lock + Condition”或“改用 BlockingQueue”来答。
5. 核心并发机制与锁
锁是 Java 多线程面试的重灾区,面试官默认你会用 synchronized,真正区分水平的是“锁升级”“Lock 接口”“volatile 语义”。
5.1 synchronized 的四种使用方式
- 修饰实例方法:锁当前对象实例。
- 修饰静态方法:锁当前类的 Class 对象。
- 修饰同步代码块,锁是括号内配置的对象。
- 修饰类对象:锁范围等价于静态方法。
示例:
public class SynchronizedDemo { public synchronized void instanceMethod() { System.out.println("lock this"); } public static synchronized void staticMethod() { System.out.println("lock Class object"); } public void blockMethod(Object lock) { synchronized (lock) { System.out.println("lock custom object"); } } }高频追问:synchronized 是公平锁还是非公平锁?
答:默认非公平锁。JDK 1.6 之后有偏向锁、轻量级锁、重量级锁的升级过程。面试官问到锁升级时,一定要按这个顺序答:偏向锁 → 轻量级锁(CAS 自旋)→ 重量级锁(Monitor 阻塞),并说明无竞争时偏向锁、轻度竞争时自旋、竞争激烈时挂起阻塞。
5.2 Lock 接口和 synchronized 的选择
面试题几乎必问:Lock 和 synchronized 区别。
| 对比项 | synchronized | Lock |
|---|---|---|
| 用法 | 关键字,隐式获取释放 | 接口,需要 lock/unlock |
| 锁获取超时 | 不支持 | tryLock(timeout) 支持 |
| 可中断 | 不响应中断 | lockInterruptibly 响应中断 |
| 公平性 | 默认非公平 | 可以构造公平锁 |
| 条件变量 | wait/notify | Condition await/signal |
加分答法:会补一句“synchronized 在 JDK 1.6 之后性能已经大幅优化,日常优先用 synchronized;需要超时、可中断、多个条件队列时选 Lock”。
ReentrantLock 示例:
import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class LockDemo { private final Lock lock = new ReentrantLock(); public void doWork() { lock.lock(); try { System.out.println(Thread.currentThread().getName() + " working"); } finally { lock.unlock(); } } }一定记得 finally 里 unlock,否则抛异常锁不会释放。
5.3 volatile 和可见性
面试官问 volatile 时,不能只说“保证可见性”。要分三层:
- 可见性:volatile 变量的修改对所有线程可见,底层通过内存屏障实现。
- 有序性:禁止指令重排序。
- 不保证原子性:i++ 这种复合操作依旧线程不安全。
面试常拿双重检查锁单例来考:
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()不是原子操作,可能发生指令重排,其他线程可能拿到半初始化对象。volatile 禁止重排序,保证安全发布。
这里还可以补充 JMM 的先行发生原则,比如 volatile 变量写之后读,一定能看到最新值。
5.4 CAS 和 ABA 问题
CAS(Compare And Swap)是 JUC 的核心底层操作。回答时先讲原理:比较内存值是否等于预期值,相等才更新,否则重试。
面试官追问“CAS 有什么缺点?”答三点:
- ABA 问题:值从 A 变成 B 再变回 A,CAS 无法感知,用 AtomicStampedReference(带版本号)解决。
- 自旋消耗 CPU:高竞争时循环尝试,太多次浪费 CPU,JDK 会自适应自旋。
- 只能保证单个变量原子操作:多个变量同步时不能用 CAS,需要加锁或封装成对象。
示例:
import java.util.concurrent.atomic.AtomicInteger; public class AtomicCounter { private final AtomicInteger count = new AtomicInteger(0); public void increment() { count.incrementAndGet(); } public int get() { return count.get(); } }5.5 ThreadLocal 和内存泄漏
ThreadLocal 也是高频题,常见问法:说下 ThreadLocal 原理,为什么内存泄漏?
回答结构:
- 每个线程有 ThreadLocalMap,以 ThreadLocal 为 key,value 为线程私有数据。
- 键是弱引用,值是强引用,如果 ThreadLocal 外部无引用且线程存活,value 无法回收,造成内存泄漏。
- 解决方式是使用完主动执行
remove()。 - 使用场景:线程上下文、日期格式化、数据库连接、事务信息传递。
示例:
public class ThreadLocalDemo { private static final ThreadLocal<String> USER_CONTEXT = new ThreadLocal<>(); public static void setUser(String user) { USER_CONTEXT.set(user); } public static String getUser() { return USER_CONTEXT.get(); } public static void clear() { USER_CONTEXT.remove(); } }如果面试官往 Spring 方向问,可以追答 TransmittableThreadLocal 解决线程池中上下文传递问题。
6. 线程池与 JUC 工具
线程池是 Java 多线程面试里必考且不会放过的部分。不会回答线程池,等于多线程白学。
6.1 线程池核心参数
ThreadPoolExecutor七个参数必须背熟:
- corePoolSize:核心线程数
- maximumPoolSize:最大线程数
- keepAliveTime:非核心线程存活时间
- unit:时间单位
- workQueue:任务队列
- threadFactory:线程工厂
- handler:拒绝策略
创建示例:
import java.util.concurrent.*; public class ThreadPoolDemo { public static void main(String[] args) { ThreadPoolExecutor executor = new ThreadPoolExecutor( 2, 4, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<>(10), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy() ); for (int i = 0; i < 15; i++) { int taskId = i; executor.execute(() -> { System.out.println(Thread.currentThread().getName() + " execute task " + taskId); }); } executor.shutdown(); } }面试官必问:提交一个任务,线程池怎么处理?
标准流程:
- 核心线程数未满,创建核心线程执行。
- 核心线程数满,任务入队列。
- 队列满,创建非核心线程。
- 线程数达到 maximumPoolSize,执行拒绝策略。
6.2 四种拒绝策略
- AbortPolicy:直接抛 RejectedExecutionException。
- CallerRunsPolicy:调用者线程直接执行任务。
- DiscardPolicy:静默丢弃。
- DiscardOldestPolicy:丢弃最老任务再提交。
实际开发推荐CallerRunsPolicy,因为不会丢任务,还能起到天然限流效果,但要慎重处理性能问题。
6.3 为什么不推荐 Executors
Executors 提供的便捷方法有隐患:
newFixedThreadPool用无界队列,任务堆积可能 OOM。newCachedThreadPool最大线程数是 Integer.MAX_VALUE,可能创建过多线程。newScheduledThreadPool无界队列同样有堆积风险。
所以面试要答:手动 new ThreadPoolExecutor,明确队列容量和拒绝策略。
6.4 AQS 原理
AQS(AbstractQueuedSynchronizer)是 JUC 基石。回答套路:
AQS 是一个同步器框架,内部用 volatile int state 表示同步状态,用 CLH 双向队列管理等待线程。获取资源失败的线程进入队列等待,释放资源时唤醒后继节点。
ReentrantLock、Semaphore、CountDownLatch、ReentrantReadWriteLock 都基于 AQS。
进阶答法:说 AQS 提供独占和共享两种模式,tryAcquire/tryRelease对应独占,tryAcquireShared/tryReleaseShared对应共享,子类实现钩子方法控制状态变化。
6.5 CountDownLatch、CyclicBarrier、Semaphore
这三兄弟经常被放在一起问,很多人在这一步翻车。
CountDownLatch:一个或多个线程等待其他线程完成操作。计数不可重置。
示例:
import java.util.concurrent.CountDownLatch; public class CountDownLatchDemo { public static void main(String[] args) throws InterruptedException { CountDownLatch latch = new CountDownLatch(3); for (int i = 0; i < 3; i++) { new Thread(() -> { System.out.println(Thread.currentThread().getName() + " task done"); latch.countDown(); }, "worker-" + i).start(); } latch.await(); System.out.println("all tasks completed"); } }CyclicBarrier:多个线程互相等待,都到达屏障点后继续执行,可重复使用。适合“多线程分阶段计算”的场景。
Semaphore:控制同时访问资源的线程数量,适合限流场景。
| 工具 | 核心用途 | 是否可重复使用 |
|---|---|---|
| CountDownLatch | 等待多个任务完成 | 否 |
| CyclicBarrier | 多线程互相等待到齐 | 是 |
| Semaphore | 限流控制并发数 | 是 |
7. 内存模型与并发容器
多线程没有 JMM,前面所有概念都会飘。这里必须把 JMM 和并发容器串起来。
7.1 JMM 是什么
Java 内存模型规定:线程对共享变量的操作基于主内存和线程工作内存。线程修改变量,先把值从主内存读到工作内存,改完再写回主内存。
问题就出在写回时机不可控,导致脏读、不可见。
7.2 happens-before 规则
回答 JMM 时提 happens-before,是面试加分项。核心规则:
- 程序次序规则:单线程内代码按顺序执行。
- 锁规则:解锁 happens-before 后续加锁。
- volatile 规则:volatile 写 happens-before 后续读。
- 传递性:A happens-before B,B happens-before C,则 A happens-before C。
- 线程启动/中断/终止规则:join 线程内操作 happens-before 线程终止后主线程继续执行。
7.3 并发容器速查
| 容器 | 线程安全策略 | 适用场景 |
|---|---|---|
| ConcurrentHashMap | 分段锁/CAS + 锁粒度细化 | 高并发读写 Map |
| CopyOnWriteArrayList | 写时复制 | 读多写少 |
| BlockingQueue | 阻塞队列 | 生产者消费者 |
| LinkedBlockingQueue | 链表阻塞队列 | 任务缓冲 |
| ArrayBlockingQueue | 有界数组阻塞队列 | 有界任务队列 |
面试官问到 ConcurrentHashMap 在 JDK 7 和 JDK 8 的区别:
JDK 7 是分段锁 Segment + HashEntry,锁粒度是 Segment;JDK 8 改为 Node + CAS + synchronized,锁粒度是单个桶头节点,并发写作性能更好,扩容时也优化了。
8. 经典并发实战与手写题
前面背完,面试官大概率会追加一题手写代码。高频题目有:手写阻塞队列、交替打印、多线程累加、并发请求合并。
这里给出一个手写阻塞队列案例,直接可用:
import java.util.LinkedList; import java.util.Queue; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class BlockingQueueDemo<E> { private final Queue<E> queue = new LinkedList<>(); private final int capacity; private final Lock lock = new ReentrantLock(); private final Condition notEmpty = lock.newCondition(); private final Condition notFull = lock.newCondition(); public BlockingQueueDemo(int capacity) { this.capacity = capacity; } public void put(E item) throws InterruptedException { lock.lock(); try { while (queue.size() == capacity) { notFull.await(); } queue.offer(item); notEmpty.signalAll(); } finally { lock.unlock(); } } public E take() throws InterruptedException { lock.lock(); try { while (queue.isEmpty()) { notEmpty.await(); } E item = queue.poll(); notFull.signalAll(); return item; } finally { lock.unlock(); } } }这个手写题考察四个点:Lock 获取释放是否正确、while 防虚假唤醒、Condition 使用是否合理、finally 是否必修。能写出来且讲清楚,面试基本稳。
交替打印可以考察 synchronized + wait/notify,或者 ReentrantLock + Condition。这里给精简版:
public class AlternatingPrintDemo { private final Object lock = new Object(); private int count = 1; private final int limit = 10; public void printOdd() throws InterruptedException { synchronized (lock) { while (count <= limit) { if (count % 2 == 1) { System.out.println("Odd: " + count); count++; lock.notifyAll(); } else { lock.wait(); } } } } public void printEven() throws InterruptedException { synchronized (lock) { while (count <= limit) { if (count % 2 == 0) { System.out.println("Even: " + count); count++; lock.notifyAll(); } else { lock.wait(); } } } } }实际跑的时候要注意:两个线程竞争同一个锁,拿到锁后发现不是自己输出就 wait,交出锁;这里是面试演示,不是性能最优写法,但逻辑完整。
再补一个多线程累加的批量任务场景:用线程池提交一批任务,用 CountDownLatch 等待全部完成。
import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicInteger; public class BatchTaskDemo { public static void main(String[] args) throws InterruptedException { int taskCount = 10; ExecutorService executor = Executors.newFixedThreadPool(4); CountDownLatch latch = new CountDownLatch(taskCount); AtomicInteger sum = new AtomicInteger(0); for (int i = 1; i <= taskCount; i++) { final int value = i; executor.execute(() -> { sum.addAndGet(value); latch.countDown(); }); } latch.await(); System.out.println("sum = " + sum.get()); executor.shutdown(); } }这个例子能在日常开发中直接改编成“批量处理订单”“批量同步数据”的任务模板,也是面试里“线程池+任务拆分+汇总”的标准答案。
9. 面试答题技巧与常见追问
多线程面试题是“八股+原理+实战”的组合,光会背不够,还要有自己的答题节奏。
9.1 先说结论再展开
面试官问“线程池怎么选?”,不要上来罗列参数。先说结论:“优先手动创建 ThreadPoolExecutor,根据任务类型决定参数”。再说核心线程数可以按照 CPU 密集型和 IO 密集型来估算。最后补充队列大小、拒绝策略、监控和 shutdown。这就是结论-原理-落地。
9.2 经典追问链
问了线程池,后面大概率会这样追:
核心线程数设多大合适?答:CPU 密集型设为 N+1,IO 密集型设为 2N 或结合阻塞系数估算,最终压测调优。keepAliveTime 设多少?答:根据任务尖峰持续时间和线程闲置成本选,避免频繁销毁重建。队列满了怎么办?答:触发扩容到 maximumPoolSize,再满走拒绝策略。线程池要监控什么?答:活跃线程数、队列大小、任务完成数、拒绝次数,用 ThreadPoolExecutor 的 getPoolSize、getActiveCount、getQueue 方法定期采集。线程池如何优雅关闭?答:先 shutdown,等待任务执行完,再 shutdownNow,处理中断异常。
9.3 怎么避开八股味道
回答“volatile 能保证原子性吗?”时,不要只说“不能”,立刻补充一段代码或场景说明。例如:
volatile 只保证可见性和有序性,比如 boolean 开关控制线程停止就是典型用法。但如果多个线程同时执行 counter++,即使 counter 是 volatile,结果也不正确,因为这不是原子操作,需要 AtomicInteger 或 synchronized。
这样回答比单纯背概念好很多,因为面试官能看到你真正理解语义边界。
9.4 每个高权重题目准备一个例子
建议考前记住几个可手写的代码模板:
- 线程池初创建任务提交,带拒绝策略。
- synchronized 生产者消费者,用 while + notifyAll。
- ReentrantLock + Condition 阻塞队列。
- CountDownLatch 等待多线程任务完成。
- ThreadLocal 上下文传值并 remove。
每个模板不需要太长,但要能够随时写出来。面试官让“手写一个场景”时,直接复用模板,再按照题目需求改参数,比自己临时从零写要稳得多。
10. 资源占用与性能观察
多线程程序面试时可能被追问性能问题,这里重点观察两个方向:
10.1 线程数与内存
线程本身会占用栈内存,默认线程栈约 1MB(随 JVM 参数可调)。创建 1 万个线程可能占用 10GB 虚拟内存,虽然不会立即物理占用,但会迅速耗尽地址空间或触发 OOM。所以大量任务一定要用线程池。
你可以在本机用命令观察线程数:
jps jstack <pid> | grep "java.lang.Thread.State" | sort | uniq -c如果面试聊到你做过线程排查,说出jstack看线程状态、jmap看堆内存、jstat看 GC,会明显加分。
10.2 性能对比
| 指标 | 单线程 | 线程池 | 无限制创建线程 |
|---|---|---|---|
| 任务量大时 CPU 利用率 | 低 | 可控 | 高,但切换开销大 |
| 内存风险 | 低 | 中 | 高 |
| 代码维护 | 简单 | 中 | 低 |
| 推荐场景 | 简单任务 | 大多数生产任务 | 不推荐 |
10.3 调优建议
- 先压测,再定线程池参数,不要拍脑袋。
- 任务处理耗时短而数量大:减少最大线程数,放大队列容量。
- 任务耗时参差大:增加 keepAliveTime,减少频繁创建销毁。
- 不要在线程里直接操作大 List 而不考虑线程安全,优先使用 ConcurrentHashMap 或 CopyOnWriteArrayList。
11. 常见问题与排查方法
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 程序卡死,任务不执行 | 线程池队列满或拒绝策略抛异常 | 查看日志有无 RejectedExecutionException | 调大队列,调整拒绝策略 |
| 数据不一致,多个线程写共享变量 | 未同步或用了 volatile 当原子变量 | 代码审查,压测复现 | 加锁或用 Atomic 类 |
| 死锁,两个线程互相等待锁 | 锁顺序不一致 | jstack 查看 Deadlock 信息 | 统一锁获取顺序,减少嵌套锁 |
| ThreadLocal 值跑到别的线程 | 线程池复用线程 | 确认是否在任务内 remove | 任务结束后 ThreadLocal.remove() |
| 线程数暴涨,CPU 飙升 | 无界线程池或任务耗时过长 | 用 jstack 看线程栈 | 改用有界线程池,细化任务超时 |
| 内存溢出 | 无界队列堆积任务 | 查看堆使用和队列大小 | 使用有界 ArrayBlockingQueue |
多线程问题定位最核心的套路是:先看线程状态和锁情况,再看堆内存和 GC,最后结合业务日志定位。
12. 最佳实践与学习建议
面试准备不是刷完题就结束,下面这些实践建议能帮你把知识真正落到项目里。
12.1 准备一套最小可运行 Demo
在你本机建一个并发 demo 工程,代码目录建议这样划分:
src/main/java/ threadbasic/ 线程创建、状态、join/sleep/yield synchronization/ synchronized、Lock、Condition threadpool/ ThreadPoolExecutor、拒绝策略 concurrentutil/ CountDownLatch、CyclicBarrier、Semaphore把例子都跑通,面试前 2 小时快速过一遍,比背文字效率高得多。
12.2 与业务场景结合
给面试官讲多线程时,不要只说理论。比如:
- “我用线程池批量处理导入文件的每一行,拆分成多个任务,用 CountDownLatch 等待全部处理完,再返回导入结果。”
- “用 ThreadLocal 保存登录用户信息,在拦截器里 set,请求结束后 remove,避免内存泄漏。”
- “用 ConcurrentHashMap 做本地缓存,避免了 ConcurrentModificationException。”
这些场景不需要多复杂,关键要让对方相信你在真实项目里处理过并发问题。
12.3 合规与安全提示
多线程程序不是越快越好,不是所有场景都需要并发。涉及支付、订单、资金类业务,多线程方案必须做幂等、限流、兜底和监控。本地测试和项目上线要区分环境,不要直接用高线程数暴力压测生产库。
另外,面试题整理和八股文背诵只是求职的一部分。准备过程中要多写多跑多调试,用 Debugger 观察线程状态,用 jstack 定位死锁,这些能力才是面试官真正想看到的。
13. 总结与下一步
多线程面试准备最怕两件事:一是背了一堆术语但不会手写代码,二是刷了大量题但没法把概念串起来。
这篇内容覆盖了线程基础、锁机制、JMM、线程池、并发容器、实战手写、性能观察和排查方法。建议按下面顺序安排 3 天:
- 第 1 天:线程基础 + synchronized + Lock,跑通线程创建、生产者消费者、锁示例;
- 第 2 天:JMM + volatile + CAS + ThreadLocal + JUC 工具,重点理解可见性和原子性区别;
- 第 3 天:线程池 + 并发容器 + 手写阻塞队列 + 接口问答,输出一套自己的答题模板。
如果时间不够,优先把线程池和 synchronized 吃透,这两个项目在 Java 多线程面试中出现频率最高。如果时间还有富余,再往下看 ConcurrentHashMap 源码、AQS 同步队列、 CompletableFuture 异步编排和虚拟线程,这些放在进阶轮掌握更合适。
多线程的学习路径很长,但面试需要的核心节点就这么多。按这套思路走下去,至少能在面试里把“用过”变成“能讲清”,也能在项目里真正拿多线程技术解决问题。