Java并发编程面试指南:线程状态、synchronized、volatile与线程池 简介面向Java后端开发与面试复习的多线程八股文PDF内容以问答形式覆盖线程定义、线程安全与不安全、自旋锁、CAS、乐观锁与悲观锁、AQS框架并延伸到原子操作、Executors线程池、阻塞队列与生产者-消费者模型等高频考点适用于面试突击和并发体系查漏补缺。整个资源为单个PDF文件共1个文件约2.58MB支持电脑或手机离线阅读。目前已有116人学习下载文档在结构上按问题分类每个问题先给结论再解释细节例如synchronized与CAS的区别、自旋锁在SMP架构下的适用场景、JDK1.6后synchronized的优化方向、阻塞队列的类型与典型用途等易读且能快速定位重点。读者可以借此在短时间内回顾多线程核心机制既服务面试准备也有助于在实际项目中选择合适的并发工具。1. Java 多线程八股文面试官真正在验什么候选人不缺题缺的是把一道八股展开成一组可对答的底层逻辑。Java 多线程面试题的资料包每年招聘季都会被反复下载但一个现实是能把 synchronized、volatile、线程池七参数背下来的人很多能在边界条件下讲清「为什么」的人很少。面试官顺着多线程面试题往下问不是在验证记忆力而是在验证候选人处理过什么样的并发问题——有没有排查过死锁有没有被无界队列拖垮内存有没有被虚假唤醒咬过。这篇按状态、内存模型、锁、并发工具、线程池五层展开每一层都能停在一个可动手验证的位置。2. 线程六态与状态切换最容易被问漏的细节2.1 RUNNABLE 是跑步机上的状态不是 CPU 占用中Java 把操作系统层面的就绪态和运行态合并成了 RUNNABLE 一个状态。线程只要具备被调度资格就处于 RUNNABLE哪怕它一个 CPU 时间片都没拿到只要没阻塞状态就是 RUNNABLE。JVM 不做 CPU 调度分不清线程是刚被唤醒排队还是此刻正在内核里抢占执行所以规范把两种情形合并。答到这一层和只会背六个枚举名的候选人立刻拉开距离当你说「RUNNABLE 就是正在跑」时面试官通常会追问「那怎么解释线程状态是 RUNNABLE 但 CPU 占用为 0」。线程六态完整枚举是 NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED。new 一个 Thread 还没 start 是 NEWstart 之后进入 RUNNABLE被 synchronized 挡住是 BLOCKED被 LockSupport.park、join、wait 挂起是 WAITING带超时参数的 sleep、await、wait 是 TIMED_WAITINGrun 方法返回或抛异常是 TERMINATED。这里最容易混的是 BLOCKED 与 WAITING单独展开讲。2.2 BLOCKED 与 WAITING 不是同一个等待池BLOCKED 只出现在等待 synchronized 监视器锁的场景里。而 ReentrantLock 这类 AQS 锁拿不到锁时线程停在 WAITINGparking不是 BLOCKED。很多背过八股的人在这里翻车面试官问「ReentrantLock 抢锁失败线程是什么状态」回答 BLOCKED 就错了。jstack 输出里区分得非常清楚java.lang.Thread.State: BLOCKED (on object monitor)是 synchronized 阻塞WAITING (parking)是 LockSupport.park 挂起AQS 体系都走这条路。2.3 用一份小代码观察六态切换public class ThreadStateDemo { private static final Object LOCK new Object(); public static void main(String[] args) throws InterruptedException { Thread t1 new Thread(() - { synchronized (LOCK) { try { Thread.sleep(3000); // 持有锁睡 3 秒 } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } }, t1); Thread t2 new Thread(() - { synchronized (LOCK) { // 抢不到锁就停在 BLOCKED } }, t2); t1.start(); Thread.sleep(50); // 先让 t1 进入锁 t2.start(); Thread.sleep(100); // 等 t2 完成锁竞争 System.out.println(t1.state t1.getState()); // 大概率 TIMED_WAITING System.out.println(t2.state t2.getState()); // 大概率 BLOCKED } }这段代码用一个共享锁对象制造竞争t1 拿着锁进入 sleep属于 TIMED_WAITINGt2 在同步块入口排队属于 BLOCKED。两个 50ms、100ms 的延迟不是随便写的它们确保 t1 先进入临界区、t2 随后才出发不然两个线程的先后顺序可能反掉。getState() 只能看瞬间快照所以要在状态最典型的时间点采样。真实排查中不要用这种轮询方式看状态直接jstack pid看线程 dump 更可靠。2.4 sleep、wait、join谁让出 CPU谁释放锁方法让出 CPU释放当前持有的锁必须持有锁唤醒方式Thread.sleep是否不需要时间到Object.wait是是需要notify/notifyAll/超时Thread.join是是底层是 wait不需要显式持有目标线程终止LockSupport.park是否按调用方逻辑不需要unpark/中断sleep 不碰锁这是它和 wait 最大的分界。wait 释放锁是刻意设计的它要让出监视器给其他线程进入临界区的机会。join 的实现值得补一刀——它底层是 while (isAlive()) wait(0)所以在 synchronized 方法里调 join 也会释放当前对象锁这个细节能拦住不少自以为熟的人。答队列题时把「谁让 CPU、谁放锁」讲清楚比背定义有用得多。3. Volatile 与 JMM把「三性」讲出信息差3.1 从 JVM 内存模型讲起而不是从缓存讲起Java 内存模型JMM定义了一组抽象规则每个线程有工作内存寄存器或缓存的抽象共享变量存在于主内存。线程读写共享变量时先在自身工作内存里做操作再同步回主内存同步时机由 JMM 规则决定不由程序员直接控制。面试时如果只搬「CPU 三级缓存」那套只讲对了一半因为 JMM 是语言层面的规范缓存一致性是硬件层面的实现JMM 的可见性规则在无缓存环境下依然成立。更准确的表述是JMM 规定了一个线程对共享变量的写什么时候对另一个线程的读可见。3.2 Volatile 只保证可见性与有序性不保证原子性volatile 的语义有两条对 volatile 变量的写会立即对其他线程的后续读可见读写前后插入内存屏障禁止相关指令重排序。它不保证复合操作的原子性。下面这段是高频演示代码。public class VolatileNotAtomicDemo { private static volatile int count 0; public static void main(String[] args) throws InterruptedException { Thread[] threads new Thread[100]; for (int i 0; i threads.length; i) { threads[i] new Thread(() - { for (int j 0; j 100; j) { count; // 读-改-写三步不是原子操作 } }); threads[i].start(); } for (Thread t : threads) { t.join(); // 等待所有线程执行完成 } System.out.println(count count); } }100 个线程各累加 100 次期望是 10000实际几乎每次都会得到接近但小于 10000 的数。count 看似一行底层是读取当前值、加 1、写回三步线程可能在上一步读到旧值后还没写回就被切换。volatile 只能保证写之前的修改对后续读可见挡不住多个线程在同一旧值上各自加 1。这里 join 还顺带演示了上一章的用法主线程等 100 个子线程全部完成后才打印结果。对比可见性可以用另一个经典场景一个线程修改布尔标志位另一个线程死循环读它。不加 volatile 时JIT 或工作内存可能把 flag 的读取优化成寄存器值子线程永远看不到主线程的修改加了 volatile 后循环能正常退出。这类问题受 JIT 影响不是必现简历上写「排查过可见性问题」的人至少要说得出复现不稳定这个前提。3.3 双重检查锁单例为什么两个 if 还要加 Volatileclass 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; } }new Singleton() 在 JVM 里拆成三步分配内存、调用构造方法初始化、把引用赋给 instance。不加 volatile 时后两步可能被重排另一个线程在第一个 if 处看到 instance 非 null直接拿去用读到的是一个还没走完构造方法的半初始化对象。volatile 禁止了「写引用」之前的重排序保证 final 字段和构造逻辑在线程看到引用前完整发布。去掉两个 if 中的任何一个这个单例都会出问题去掉 volatile问题变成概率性出现——只在特定指令重排和线程交错下触发这也是并发 Bug 难以稳定复现的典型特征。3.4 加分点Volatile 到底怎么实现可见性JMM 不规定 volatile 的底层实现HotSpot 在 x86 上的典型做法是volatile 写生成带 lock 前缀的写指令强制写穿到内存并让其他核心对应的缓存行失效volatile 读本身通常不需要额外屏障。面试时补上「规范规定语义实现依赖平台」这个区分能显示出你看过 JLS 之外的东西。但如果面试官明显是初级岗位这层点到即可不要展开成硬件课。4. Synchronized 与锁升级从偏向锁讲到 JDK15 的转折4.1 Synchronized 锁的是对象头里的东西synchronized 的锁信息存在对象头的 Mark Word 里锁膨胀到重量级后Mark Word 存放 monitor 对象的指针。每个对象都可以关联一个 monitor所以任意对象都能当锁。这也是「锁 this」「锁 Class 对象」「锁指定对象」三种写法的基础。可重入性来自 monitor 的计数器同一个线程多次进入同一把锁计数器累加完全退出才归零。JVM 在锁的获取和释放上做了大量优化这才是面试真正的重心。4.2 锁升级路径无锁到偏向锁到轻量级锁到重量级锁JDK 6 起 synchronized 不再是「上来就重量级」而是按竞争程度逐步升级。无锁状态由单个线程反复进入时JVM 记录线程 ID 到 Mark Word这就是偏向锁免去每次加锁的 CAS 开销一旦有其他线程来争抢偏向锁撤销升级为轻量级锁轻量级锁通过 CAS 抢 Mark Word抢不到就自旋等待适合临界区短、竞争不激烈的场景自旋超过阈值或竞争加剧膨胀为重量级锁进入操作系统 monitor 机制未抢到锁的线程挂起涉及内核态切换开销最大。这里要提一个常被忽略的转折JDK 15 通过 JEP 374 默认禁用了偏向锁HotSpot 团队认为偏向锁在当代应用环境下的维护成本已经不值得保留。所以现在还按旧文章把四个状态讲得头头是道反而暴露知识没更新。更合理的答法是说明偏向锁的历史作用和废弃原因再把重点放在轻量级锁与重量级锁的分界上并补一句「没有走到重量级锁之前锁对象和线程栈帧里维护的锁记录在做 CAS 交换」。这一下就和其他背图的人分开。4.3 三种加锁方式锁的是不同对象class LockScope { private int shared 0; // 实例方法锁的是当前对象 this public synchronized void incr() { shared; } // 静态方法锁的是 LockScope.class 类对象 public static synchronized void staticIncr() { // 操作全局状态 } // 代码块可自行指定锁对象粒度最灵活 public void lockRange(Object guard) { synchronized (guard) { // 只在这一段内持有锁 } } }三个方法看起来都是 synchronized锁对象完全不同。实例方法锁 this静态方法锁 Class 对象这个 Class 对象全局唯一才能保护静态变量代码块可以锁任何对象粒度由自己决策。常见误用是两个线程一个调 incr() 锁 this另一个调 staticIncr() 锁 Class实际各自为政互不相干。线程安全不是加了 synchronized 就叫安全而是所有访问同一份数据的线程都必须在同一把锁上汇聚。写法锁对象典型场景同步实例方法this实例内共享可变状态同步静态方法Class 对象静态变量、全局计数器同步代码块显式指定的对象缩小锁范围减少持有时间4.4 用 jstack 看锁竞争的现场jstack pidjstack 是 JDK 自带工具pid 用jps -l查。线程 dump 里能看到三类关键信息BLOCKED (on object monitor)表示线程在等 synchronized 锁- locked表示持有某把 monitor 锁Found one Java-level deadlock说明 JVM 检测到了死锁并列出循环等待链。处理线上锁问题时连续执行两三次 jstack 对比线程状态变化能看出锁持有了多久、竞争线程堆积在哪。面试问「怎么排查死锁」标准答法里一定要有 jstack 这一步。5. ReentrantLock 与 AQS从四个 API 拿到加分项5.1 四个核心 API 各管一个场景synchronized 解决 90% 的互斥需求但有三件事它做不了抢锁超时、响应中断、公平排队。ReentrantLock 把这几件事补齐了。ReentrantLock lock new ReentrantLock(); // 默认非公平锁 if (lock.tryLock(2, TimeUnit.SECONDS)) { // 最多等 2 秒 try { // 临界区 } finally { lock.unlock(); // 必须手动释放 } } else { System.out.println(2 秒内没拿到锁先走降级逻辑); }lock() 拿不到锁就一直阻塞tryLock(timeout) 等待有限时间后放弃适合业务侧做降级lockInterruptibly() 让等待中的线程可以被中断唤醒适合处理需要取消的操作unlock() 必须放在 finally 里否则临界区抛异常锁就永远不释放。使用 ReentrantLock 的每一处代码都必须保证 unlock 与 lock 成对出现这是它与 synchronized 最大的使用差异——synchronized 的解锁是 JVM 自动完成的ReentrantLock 的解锁全靠程序员自觉。能力synchronizedReentrantLock自动释放锁是否尝试超时获取否tryLock(timeout)响应中断否lockInterruptibly公平排队否构造参数设置条件变量单一 wait set多个 Condition5.2 公平锁与非公平锁一个换吞吐一个换公平ReentrantLock 默认非公平新来的线程可以直接 CAS 抢锁抢不到才排队。非公平锁吞吐更高因为减少了线程唤醒切换的开销代价是队列尾部线程可能长期吃不到锁。公平锁严格按照 FIFO 顺序避免饥饿但线程在锁释放瞬间被唤醒前新的请求即使到了也得排队整体吞吐下降。面试里讲清楚「非公平是插队公平是排队」不难难的是补一句为什么默认选非公平——大部分业务场景中短临界区下让刚来的线程直接抢比重唤醒一个挂起的线程更划算。5.3 AQS 是 Java 并发工具的公共底座AbstractQueuedSynchronizer 是 ReentrantLock、Semaphore、CountDownLatch、ReentrantReadWriteLock 的共同骨架。结构上看三板斧一个 volatile int state 记录同步状态一个 CLH 变体队列存放等待线程一组模板方法由子类实现。以 ReentrantLock 为例state 记录重入次数每次 lock 加一unlock 减一以 Semaphore 为例state 表示剩余许可acquire 减少release 增加。AQS 管排队、唤醒、入队出队这些通用逻辑子类只负责决定 state 怎么变。面试把这个结构说清任何基于 AQS 的工具题都接得住。5.4 手写题两个线程交替打印 A1B2C3public class AlternatePrint { private static final ReentrantLock lock new ReentrantLock(); private static final Condition letterTurn lock.newCondition(); private static final Condition digitTurn lock.newCondition(); private static int state 0; // 0 表示轮到字母1 表示轮到数字 public static void main(String[] args) { Thread letters new Thread(() - { for (char c A; c Z; c) { lock.lock(); try { while (state ! 0) { letterTurn.await(); // 不是自己的轮次就等待 } System.out.print(c); state 1; digitTurn.signal(); // 精确唤醒数字线程 } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { lock.unlock(); } } }); Thread digits new Thread(() - { for (int i 1; i 26; i) { lock.lock(); try { while (state ! 1) { digitTurn.await(); } System.out.print(i); state 0; letterTurn.signal(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { lock.unlock(); } } }); letters.start(); digits.start(); } }输出为 A1B2C3 一直到 Z26。两个 Condition 各自对应一个线程的等待队列signal 只叫醒对端避免 synchronized 版本里 notifyAll 唤醒所有线程再重新竞争的无效操作。await 在 Condition 中会释放锁signal 只是把等待线程移回锁等待队列实际拿锁还要等当前线程 unlock。while 而不是 if 判断 state是为了防范虚假唤醒——线程可能没收到 signal 就被唤醒需要重新检查条件。这段代码把 ReentrantLock、Condition、模板化手写题的常见坑全部覆盖是 Java 并发面试题里性价比极高的一道准备题。6. 线程池七参数与拒绝策略把八股配置成可落地的方案6.1 Execute 与 Submit一类管执行一类管结果ExecutorService pool Executors.newFixedThreadPool(2); FutureInteger future pool.submit(() - { throw new IllegalStateException(boom); }); Integer result future.get(); // 异常在这里抛出execute 接收 Runnable只管执行任务抛出的异常进入线程的 UncaughtExceptionHandlersubmit 接收 Callable 或 Runnable返回 Future异常被封装在 Future 内部调用 get() 时才抛出。这个区别易被忽视但线上排查任务静默失败时十有八九是 submit 之后从不调 get。顺带说明Executors 的工具方法方便但不建议用于生产newFixedThreadPool 用无界队列任务堆积可能耗尽内存。业界常见做法是直接 new ThreadPoolExecutor 并显式指定有界队列。6.2 七参数速查与配置依据参数作用配置参考corePoolSize核心线程数常驻不回收CPU 密集型接近核数IO 密集型 2 倍以上maximumPoolSize最大线程数峰值吞吐与资源上限的折中keepAliveTime非核心线程空闲存活时间任务到达间隔的观察值workQueue任务缓冲队列有界优先无界有 OOM 风险threadFactory线程工厂必须命名便于 jstack 排查handler拒绝策略依据业务容忍度选择执行流程是固定的核心线程数没满创建线程执行满了进队列队列满了创建到 maximumPoolSize还满走拒绝策略。keepAliveTime 只作用于非核心线程除非开启 allowCoreThreadTimeOut。边界条件很重要corePoolSize 设得再大队列不满时也未必创建到 maximumqueue 满了会先去拉新线程而不是让任务初始进入就直接拒绝。6.3 一个有限流需求的导入任务配置ThreadPoolExecutor importPool new ThreadPoolExecutor( 4, // coreCPU 核数一半平时吞吐够用 8, // max峰值时扩充一倍 60, TimeUnit.SECONDS, // 非核心线程 60 秒空闲回收 new ArrayBlockingQueue(2000), // 有界队列防止任务无限堆积 new ThreadFactory() { private final AtomicInteger seq new AtomicInteger(1); Override public Thread newThread(Runnable r) { Thread t new Thread(r, import-worker- seq.getAndIncrement()); t.setDaemon(false); return t; } }, new ThreadPoolExecutor.CallerRunsPolicy() );导入场景的特点是瞬时任务量大但单个任务短。queue 容量的选择要看最慢消费者的耗时如果单任务平均 100ms2000 队列在 4 个核心线程下大约能缓冲 50 秒的积压足够扛住大多数秒级峰值。CallerRunsPolicy 在队列满时不丢弃任务而是让提交线程自己执行相当于天然背压——提交方变慢业务层就能感知到的拥堵。线程工厂命名是排查利器jstack 看到 import-worker-3 在跑什么比看 pool-1-thread-3 直观得多。这个配置里三个关键点有界队列、命名线程、CallerRunsPolicy分别解决内存溢出、排障效率和服务过载三个问题。如果队列持续打满且提交方经常被拖住执行说明需要扩容消费者而不是继续调大 maximumPoolSize。本文还有配套的精品资源点击获取