一、线程面试需要什么条件?

在当前的互联网技术招聘中,线程面试往往是区分初级与高级开发工程师的分水岭。随着分布式系统和微服务架构的普及,高并发处理能力成为企业的核心需求。因此,线程面试不再仅仅考察简单的Thread类使用,而是深入到底层原理、JVM内存模型以及高并发场景下的问题解决能力。

1.1 核心考察维度

根据对多家头部互联网公司的招聘需求分析,线程面试主要涵盖以下四个维度:

基础概念

进程与线程的区别、线程的生命周期、状态转换、守护线程与用户线程。

同步机制

synchronized关键字原理、ReentrantLock、volatile、CAS、AQS。

线程池

ThreadPoolExecutor参数、拒绝策略、核心线程存活时间、队列选择。

并发工具

CountDownLatch、CyclicBarrier、Semaphore、ConcurrentHashMap。

1.2 为什么线程面试如此重要?

在实际业务中,线程面试所考察的知识直接对应着系统的吞吐量、响应时间和稳定性。例如,不当的线程池配置可能导致OOM(内存溢出),而错误的锁使用可能导致死锁或性能瓶颈。因此,企业通过线程面试来筛选出具备高并发思维的人才。

二、线程基础核心考点

在线程面试中,基础概念是必问环节。面试官通常会从最基础的线程状态开始,逐步深入到关键字原理。

2.1 线程的生命周期

Java线程在运行过程中有五种基本状态,理解这些状态及其转换条件是线程面试的基础:

  1. 新建 (NEW):线程对象已创建,但未调用start()方法。
  2. 可运行 (RUNNABLE):调用start()后,等待CPU调度,包括操作系统中的就绪和运行状态。
  3. 阻塞 (BLOCKED):等待获取监视器锁,进入synchronized同步块时。
  4. 等待 (WAITING):调用了wait()、join()或park(),需要其他线程唤醒。
  5. 超时等待 (TIMED_WAITING):调用了带参数的sleep()、wait()或join()。
  6. 终止 (TERMINATED):线程执行完毕或异常退出。

2.2 synchronized 与 ReentrantLock 的区别

这是线程面试中关于锁机制最常考的问题。两者都能实现同步,但存在显著差异:

特性 synchronized ReentrantLock
实现层次 JVM层面(关键字) JDK层面(API)
锁释放 自动释放(代码块结束或异常) 必须手动释放(finally中lock.unlock())
等待可中断 不可中断 支持(lockInterruptibly)
公平锁 非公平锁 可配置公平/非公平
条件变量 单一(wait/notify) 多个(Condition)
性能 JDK 6后优化,性能接近 高竞争下更灵活

2.3 volatile 关键字详解

在线程面试中,volatile常被问及。它保证了变量的可见性和有序性,但不保证原子性。其底层实现依赖于内存屏障(Memory Barrier)和CPU指令,防止指令重排序。

// 示例:volatile保证可见性
public class VolatileExample {
    volatile boolean flag = false;
    public void writer() {
        flag = true; // 写入volatile变量
    }
    public void reader() {
        if (flag) { // 读取volatile变量
            // 执行相关逻辑
        }
    }
}

三、进阶原理:AQS与CAS

深入理解线程面试中的高级考点,必须掌握JUC包的核心基石:CAS(Compare-And-Swap)和AQS(AbstractQueuedSynchronizer)。

3.1 CAS 原理与ABA问题

CAS是一种无锁算法,包含三个操作数:内存值V、旧的预期值E、要修改的新值N。当且仅当预期值E和内存值V相同时,将内存值V修改为N,否则什么都不做。这保证了操作的原子性。

ABA问题:如果一个值原来是A,变成了B,又变成了A,使用CAS检查时会认为它没有变化。解决方案是使用AtomicStampedReference,引入版本号。

3.2 AQS 核心机制

AQS是ReentrantLock、CountDownLatch等并发工具的基础。它通过一个int类型的state变量表示同步状态,通过FIFO队列管理等待线程。

步骤一:尝试获取锁

线程调用tryAcquire()尝试修改state。如果成功,设置当前线程为独占线程。

步骤二:加入队列

如果获取锁失败,线程被封装成Node节点,加入CLH队列尾部,并阻塞等待。

步骤三:释放锁

线程执行完毕后调用release(),修改state,并唤醒队列中的下一个节点。

四、线程池实战配置

在生产环境中,直接使用Thread创建线程是被禁止的。线程池(ThreadPoolExecutor)是线程面试中的高频考点,因为它直接关乎系统稳定性。

4.1 核心参数解析

ThreadPoolExecutor有7个核心参数,理解它们是线程面试通过的关键:

4.2 线程池工作流程

当新任务提交时,线程池的处理逻辑如下:

  1. 如果当前线程数 < corePoolSize,创建新线程执行任务。
  2. 如果当前线程数 ≥ corePoolSize,将任务加入workQueue。
  3. 如果队列已满,且当前线程数 < maximumPoolSize,创建非核心线程执行任务。
  4. 如果队列已满,且当前线程数 ≥ maximumPoolSize,执行拒绝策略。

4.3 如何合理配置线程池?

配置线程池需根据任务类型区分:

任务类型 特点 推荐配置
CPU密集型 CPU计算密集,IO少 corePoolSize = CPU核数 + 1
IO密集型 IO操作多,等待时间长 corePoolSize = CPU核数 2 或 更多
混合型 可拆分为CPU和IO任务 拆分后分别配置

五、线程面试高频问答(FAQ)

以下整理了线程面试中网友最关心的10个问题,并提供了深度解答。

Q1-Q3
Q4-Q6
Q7-Q10

Q1:如何停止一个线程?

推荐使用volatile标志位或interrupt()方法。interrupt()会设置中断标志,线程应定期检查该标志并优雅退出。避免使用已过时的stop()方法。

Q2:sleep()和wait()的区别?

sleep()是Thread类方法,不释放锁,时间到自动唤醒;wait()是Object类方法,释放锁,需notify()唤醒。sleep()可在任何地方调用,wait()必须在同步块中调用。

Q3:什么是死锁?如何避免?

死锁是指两个或多个线程互相持有对方需要的锁,导致无限等待。避免方法:固定锁获取顺序、使用tryLock()设置超时、使用死锁检测工具。

Q4:ConcurrentHashMap如何实现线程安全?

JDK 1.7使用分段锁(Segment),1.8使用CAS+synchronized。1.8中,只有桶头节点冲突时才加锁,粒度更细,并发度更高。

Q5:ThreadLocal原理及内存泄漏?

ThreadLocal为每个线程提供独立变量副本。Key是ThreadLocal实例,Value是线程对象中的ThreadLocalMap。内存泄漏是因为Key是弱引用,GC后Key为null,但Value是强引用。解决方法:使用完后调用remove()。

Q6:如何保证线程安全?

方法包括:使用不可变对象(final)、使用同步机制(synchronized/lock)、使用线程封闭(ThreadLocal)、使用并发容器(ConcurrentHashMap)。

Q7:CountDownLatch和CyclicBarrier区别?

CountDownLatch是一次性计数器,线程countDown()后减1,await()等待归零;CyclicBarrier是可重用屏障,线程await()等待到达指定数量后一起继续执行。

Q8:什么是原子类?原理是什么?

如AtomicInteger,底层使用CAS操作保证原子性。避免了对整个变量加锁的性能损耗。

Q9:线程池如何监控?

通过ThreadPoolExecutor提供的getActiveCount()、getQueueSize()等方法获取状态,结合监控系统(如Prometheus)进行报警。

Q10:Fork/Join框架适用场景?

适用于分治算法,如大规模数据排序、求和。通过工作窃取(Work-Stealing)算法平衡负载。