一、为什么线程面试是后端开发的必考题?
在Java后端开发的面试体系中,线程面试往往是区分初级与高级开发者的关键分水岭。随着互联网业务的高并发特性日益显著,如何高效利用多核CPU、保证数据一致性、处理线程安全问题,成为了企业考察候选人技术深度的核心维度。
许多求职者往往只停留在“知道怎么创建线程”的层面,而在面对“线程池参数如何配置”、“死锁如何排查”、“CAS底层原理”等深度问题时束手无策。本页面旨在通过结构化的知识梳理,帮助开发者建立完整的线程面试知识图谱。
⚡ 高频考点分布
- 线程状态与生命周期 (100% 必问)
- ThreadLocal原理与内存泄漏 (85% 高频)
- 线程池核心参数与拒绝策略 (90% 必问)
- synchronized与Lock的区别 (80% 高频)
- volatile关键字与内存可见性 (75% 高频)
⚙️ 面试官视角
面试官考察线程面试不仅仅是为了确认你背过八股文,更是为了验证你在实际项目中是否具备处理并发问题的能力。例如,他们可能会问:“你在项目中遇到过什么并发问题?是如何解决的?” 这就要求我们不仅要懂理论,还要懂实战。
二、线程基础:生命周期与状态转换
理解线程面试中的基础概念是第一步。Java线程的生命周期通常包含六种状态:NEW(新建)、RUNNABLE(可运行)、BLOCKED(阻塞)、WAITING(等待)、TIMED_WAITING(超时等待)、TERMINATED(终止)。
2.1 状态详解与转换
| 状态名称 | 触发条件 | 对应方法/关键字 | 面试考点 |
|---|---|---|---|
| NEW | 线程被创建,但未启动 | new Thread() | 如何判断线程是否启动? |
| RUNNABLE | 调用start()后,等待CPU调度或正在运行 | start() | RUNNABLE包含操作系统层面的Running和Ready |
| BLOCKED | 等待获取synchronized监视器锁 | synchronized | 与WAITING的区别:是否有超时时间 |
| WAITING | 无限期等待其他线程唤醒 | wait(), join() | 不会释放锁?错,wait()会释放锁 |
| TERMINATED | 线程执行完毕或异常退出 | run()结束 | 终止的线程能再次start吗?不能 |
2.2 ThreadLocal:线程隔离的神器
在线程面试中,ThreadLocal是一个绕不开的话题。它主要用于实现线程封闭(Thread Confinement),即每个线程拥有变量的独立副本,避免线程同步问题。
public class ThreadLocalExample {
private static final ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
public static void main(String[] args) {
// 设置当前线程的值
threadLocal.set(100);
// 获取当前线程的值
System.out.println(threadLocal.get()); // 输出 100
// 移除当前线程的值,防止内存泄漏
threadLocal.remove();
}
}
面试陷阱:ThreadLocalMap的Key是弱引用,Value是强引用。如果Key被GC回收,而Value没有被清除,会导致内存泄漏。因此,务必在使用后调用remove()方法。
三、JUC并发工具包:高级线程面试核心
随着JDK版本的演进,java.util.concurrent(JUC)包成为了线程面试的重中之重。相比传统的synchronized,JUC提供了更灵活、高性能的并发控制手段。
ReentrantLock:可重入锁
ReentrantLock实现了Lock接口,提供了比synchronized更强大的功能。
- 可中断锁:lockInterruptibly(),允许在等待锁时响应中断。
- 尝试获取锁:tryLock(),避免死锁,可以设置超时时间。
- 公平锁与非公平锁:synchronized是非公平的,而ReentrantLock默认是非公平的,但支持构造公平锁。
- 多个条件变量:通过newCondition()可以创建多个Condition对象,实现更精细的线程通信。
ReentrantReadWriteLock:读写锁
在读多写少的场景下,读写锁能显著提升性能。它维护了一对锁,一个读锁(共享锁),一个写锁(独占锁)。
- 读锁之间不互斥,允许多个线程同时读取。
- 写锁与读锁、写锁与写锁互斥。
- 面试考点:读写锁的降级(Write -> Read)与升级(Read -> Write,不支持)。
Condition:线程通信
Condition替代了Object的wait()/notify()机制,提供了更灵活的线程等待/通知。
- await():释放锁并等待。
- signal():唤醒一个等待线程。
- signalAll():唤醒所有等待线程。
- 优势:可以绑定多个Condition,实现精确唤醒,避免“惊群效应”。
四、线程池:生产环境的线程面试必杀技
在实际开发中,我们绝不应该手动创建线程,而应该使用线程池。线程池不仅能提高性能(复用线程),还能控制并发数量,保护系统。在线程面试中,线程池的配置和原理是考察的重点。
4.1 ThreadPoolExecutor 核心参数
| 参数名 | 类型 | 说明 |
|---|---|---|
| corePoolSize | int | 核心线程数,线程池中长期存活的线程数 |
| maximumPoolSize | int | 最大线程数,允许创建的最大线程数 |
| keepAliveTime | long | 空闲线程存活时间,超过此时间的空闲线程会被回收 |
| workQueue | BlockingQueue | 任务队列,存放等待执行的任务 |
| threadFactory | ThreadFactory | 线程工厂,用于创建新线程,建议自定义以设置名称 |
| handler | RejectedExecutionHandler | 拒绝策略,当队列满且线程数达最大时如何处理新任务 |
4.2 拒绝策略
- AbortPolicy:直接抛出RejectedExecutionException异常(默认)。
- CallerRunsPolicy:由调用线程处理该任务,起到降级作用。
- DiscardPolicy:直接丢弃任务,不抛异常。
- DiscardOldestPolicy:丢弃队列中最老的任务,尝试重新提交。
4.3 线程池大小配置
线程池大小的配置取决于任务类型:
- CPU密集型:N+1,N为CPU核数。减少上下文切换。
- IO密集型:2N 或 N/(1-阻塞系数)。阻塞系数通常在0.8-0.9之间。
- 混合型:根据具体阻塞时间动态调整。
五、锁机制:从synchronized到JMM
锁是解决并发安全问题的核心。在线程面试中,面试官通常会深入考察锁的优化、底层原理以及内存模型。
5.1 synchronized 锁升级
在JDK 1.6之后,synchronized进行了大量优化,引入了锁升级概念。
对象头中的Mark Word记录对象哈希码、分代年龄等。此时没有锁。
当只有一个线程访问同步块时,进入同步块会给对象头和栈帧中的锁记录加上偏向锁。下次进入只需检查Mark Word是否指向当前线程。
当有第二个线程竞争偏向锁时,偏向锁升级为轻量级锁。通过CAS操作尝试获取锁,失败则自旋等待。
当自旋次数超过阈值或竞争加剧时,轻量级锁升级为重量级锁。此时线程阻塞,操作系统介入。
5.2 CAS 与 ABA 问题
CAS(Compare And Swap)是无锁编程的基础。它包含三个操作数:内存位置、预期原值和新值。如果内存位置的值与预期原值相匹配,则将该位置更新为新值。
ABA问题:一个值从A变为B,又变回A,CAS检查时会认为没有变化。解决方案是使用AtomicStampedReference,版本号机制。
六、实战案例:面试官最爱问的场景题
理论掌握后,面试官往往会给出一个具体的业务场景,考察你的实战能力。以下是几个经典的线程面试案例。
案例一:如何实现一个线程安全的单例模式?
常见的有双重检查锁(DCL)和静态内部类。DCL需要注意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;
}
}
案例二:如何保证三个线程按顺序执行?
可以使用CountDownLatch、join()或ReentrantLock+Condition。
// 使用CountDownLatch
CountDownLatch latch1 = new CountDownLatch(1);
CountDownLatch latch2 = new CountDownLatch(1);
Thread t1 = new Thread(() -> {
System.out.println("Thread 1");
latch1.countDown();
});
Thread t2 = new Thread(() -> {
try { latch1.await(); } catch (InterruptedException e) {}
System.out.println("Thread 2");
latch2.countDown();
});
Thread t3 = new Thread(() -> {
try { latch2.await(); } catch (InterruptedException e) {}
System.out.println("Thread 3");
});
t1.start(); t2.start(); t3.start();