Java 并发编程面试全攻略

线程池 · 锁与AQS · volatile/CAS · ThreadLocal · 同步工具

7大核心参数 4种拒绝策略 锁升级全链路 并发三大件
📚 想要深入理解原理?推荐搭配专题图解学习

本页侧重面试考点速答。若需理解底层原理和运行机制,请看以下专题图解:

📘 Java并发编程图解 📘 Java线程间通信图解

先理解:为什么需要线程池?

🔴 真实事故:一个 new Thread() 搞垮了整个系统
某电商大促,一个新人写的代码是 new Thread(task).start()——来一个请求就创建一个线程。 结果 QPS 到 5000 时,JVM 里创建了 3 万多个线程, 每个线程默认占 1MB 栈空间,直接吃掉 30GB 内存(服务器才 16GB)。 更致命的是:3 万个线程在 CPU 只有 8 核的机器上来回切换, 光切换开销就吃掉了 90% 的 CPU,业务代码几乎没机会执行。

教训:线程不是越多越好。没有管理的 `new Thread()` 就像不设限地招临时工—— 人越多越混乱,最后谁也干不了活。线程池就是那个"招聘经理+调度员"。

线程池 = 餐厅后厨。想象一家餐厅的后厨:
核心线程 = 正式厨师(常驻员工,没活也待命)
临时线程 = 旺季临时工(忙不过来才招,闲了就辞退)
工作队列 = 订单排队区(厨师都在忙,新订单先排队)
拒绝策略 = 餐厅满座时的处理方式(直接拒绝?让客人自己来帮忙?丢掉最早的订单?)

没有"后厨管理"的餐厅,每来一单就临时招一个厨师,做完就辞退——招人成本极高,厨房爆满混乱。 有了线程池,厨师固定、排队有序、超载有预案,整个系统才能稳定运转。

不用线程池 vs 用线程池:代码对比

// ❌ 不用线程池:每个请求 new 一个线程 @GetMapping("/order") public Result createOrder() { new Thread(() -> { processOrder(); // 高并发下线程无限创建 → OOM }).start(); return Result.ok(); } // ✅ 使用线程池:受控的线程数量 + 队列缓冲 + 超载保护 @Autowired @Qualifier("orderPool") private ThreadPoolExecutor pool; @GetMapping("/order") public Result createOrder() { pool.execute(() -> { processOrder(); // 线程数受控,队列满时触发拒绝策略 }); return Result.ok(); }

线程池的三个核心价值:①控制线程数量(防止 OOM)②队列缓冲(削峰填谷)③拒绝策略(超载保护)。

ThreadPoolExecutor 7 大核心参数

对照"餐厅后厨"理解这 7 个参数:
corePoolSize = 正式厨师数量   ② maximumPoolSize = 正式+临时厨师上限   ③ keepAliveTime = 临时工空闲多久辞退   ④ unit = 时间单位
workQueue = 订单排队区大小   ⑥ threadFactory = 给厨师发工牌(命名线程)   ⑦ handler = 满座时的处理策略

构造方法签名:

ThreadPoolExecutor(
  int corePoolSize,       // 核心线程数
  int maximumPoolSize,    // 最大线程数
  long keepAliveTime,     // 空闲存活时间
  TimeUnit unit,         // 时间单位
  BlockingQueue<Runnable> workQueue,  // 工作队列
  ThreadFactory threadFactory,  // 线程工厂
  RejectedExecutionHandler handler  // 拒绝策略
)
参数技术含义生产配置要点
corePoolSize 核心线程数,即使空闲也不回收(除非设 allowCoreThreadTimeOut) 常驻线程,CPU密集型=核数+1,IO密集型=核数×2
maximumPoolSize 线程池最大线程数 = 核心 + 临时 峰值期才创建临时线程,不可超过此上限
keepAliveTime 临时线程空闲超过此时间就被回收 通常设 60 秒,避免临时线程长期占用资源
unit keepAliveTime 的时间单位 秒 / 分钟 / 毫秒
workQueue 等待执行的任务阻塞队列 必须有界!无界队列任务堆积会 OOM
threadFactory 创建线程的工厂,可自定义线程名 自定义命名(如 order-1),方便 jstack 排查
handler 队列满且线程全忙时的拒绝策略 生产环境不要用默认 AbortPolicy

任务执行流程(面试必背)

① 提交任务
② 核心线程未满?
直接执行
③ 核心满了
入队列等待
④ 队列也满了
创建临时线程
⑤ 全满
触发拒绝策略

任务提交后依次判断:核心线程是否还有空闲 → 没有则放入工作队列 → 队列满了才创建临时线程(到 maximumPoolSize)→ 临时线程也满了就触发拒绝策略。注意顺序是先入队列再扩线程,不是先创建到最大线程数再入队列。

面试官常问:"线程池是先扩线程还是先入队列?" 答案是 先入队列,再扩线程。这和很多人直觉相反!核心线程满了之后,任务先进队列,队列满了才会创建临时线程到 maximumPoolSize。这个设计的原因是:创建线程的开销远大于入队,优先用队列缓冲。

4 种内置拒绝策略

当核心线程 + 队列 + 最大线程全部满载时,新来的任务怎么处理?

最常用

AbortPolicy(默认)

做法:直接抛出 RejectedExecutionException

效果:调用方收到异常,需要自己 try-catch 处理。如果上层没有处理,可能导致整个请求失败。

// 默认策略,不设置handler就是这个
throw new RejectedExecutionException()
常用

CallerRunsPolicy

做法:由提交任务的线程自己执行该任务

效果:提交线程被阻塞去执行任务,相当于自动限流——上游看到提交变慢,自然就放慢了提交速率。

// 调用者线程自己跑
r.run(); // 在调用者线程中同步执行
少用

DiscardPolicy

做法:默默丢弃任务,不抛异常

效果:任务静默消失,没有任何日志。生产环境几乎不用——出了问题完全无法排查。

// 什么都不做,任务静默丢失
// do nothing
少用

DiscardOldestPolicy

做法:丢弃队列中最老的任务,然后重新提交当前任务

效果:牺牲最早的任务来容纳最新任务。适合过时数据无意义的场景(如实时行情推送)。

queue.poll(); // 丢掉队头(最老)
executor.execute(r); // 重试提交

生产环境中不要用默认的 AbortPolicy,因为直接抛异常可能导致整个服务不可用。推荐使用 CallerRunsPolicy 或自定义策略(如记录日志+入库重试)。

自定义拒绝策略示例

new RejectedExecutionHandler() {
  @Override
  public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
    // 1. 记录日志
    log.error("任务被拒绝: pool={}, queue={}, task={}",
      e.getPoolSize(), e.getQueue().size(), r);
    // 2. 持久化到DB/消息队列,后续重试
    retryService.save(r);
  }
}

生产环境线程池怎么配?

标准写法(推荐)

@Configuration
public class ThreadPoolConfig {

  @Bean("orderPool")
  public ThreadPoolExecutor orderPool() {
    return new ThreadPoolExecutor(
      4,                   // corePoolSize: 核心线程
      8,                   // maximumPoolSize: 最大线程
      60, TimeUnit.SECONDS,  // keepAliveTime: 临时线程空闲时间
      new LinkedBlockingQueue<>(200), // 有界队列,容量200
      new CustomThreadFactory("order"), // 自定义线程名
      new CallerRunsPolicy()  // 拒绝策略:调用者线程执行
    );
  }
}

自定义 ThreadFactory(规范线程名)

public class CustomThreadFactory implements ThreadFactory {
  private final AtomicInteger counter = new AtomicInteger(1);
  private final String prefix;

  public CustomThreadFactory(String prefix) {
    this.prefix = prefix;
  }

  @Override
  public Thread newThread(Runnable r) {
    Thread t = new Thread(r, prefix + "-" + counter.getAndIncrement());
    t.setDaemon(false);
    return t;
  }
}

// 线程名效果: order-1, order-2, order-3 ...
// jstack 一眼就能看出是哪个线程池的

线程数怎么定?(经验公式)

任务类型公式原理
CPU 密集型
(计算、加密、压缩)
CPU核数 + 1 线程多了反而增加上下文切换开销。+1 是为了在某个线程偶尔因缺页中断暂停时,额外线程能补上
IO 密集型
(网络请求、DB查询、文件读写)
CPU核数 × 2
或 CPU核数 / (1-阻塞比)
线程大部分时间在等IO,CPU空闲,可以多配线程充分利用CPU。阻塞比=0.9 → 线程数=核数×10
混合型 先按IO密集配,再压测调优 实际业务都是混合型,先用大一点的值,看 CPU 利用率和队列长度再调

CPU密集型为什么线程数不能多?因为线程都在做计算,CPU已经满载了,多加线程只会增加上下文切换的开销(保存/恢复寄存器、TLB刷新等),反而降低总吞吐量。IO密集型为什么可以多?因为线程大部分时间在阻塞等待IO响应(网络、磁盘),CPU是空闲的,多配线程可以让CPU在等待期间处理其他任务。

面试中说出"线程数不是越多越好"和"需要根据 CPU 核数和任务类型来定"就能拿到基本分。能说出具体公式 + 动态调整(如美团动态线程池)是加分项。

为什么阿里规范禁用 Executors 创建线程池?

newFixedThreadPool

Executors.newFixedThreadPool(5)

核心=最大=5,队列=LinkedBlockingQueue(无界!)

风险:任务堆积 → 队列无限增长 → OOM

newSingleThreadExecutor

Executors.newSingleThreadExecutor()

核心=最大=1,队列=LinkedBlockingQueue(无界!)

风险:同上,无界队列 OOM

newCachedThreadPool

Executors.newCachedThreadPool()

核心=0,最大=Integer.MAX_VALUE,队列=SynchronousQueue

风险:来多少任务创建多少线程 → 线程爆炸 → OOM

newScheduledThreadPool

Executors.newScheduledThreadPool(5)

核心=5,最大=Integer.MAX_VALUE,延迟/定时任务

风险:同样可能线程爆炸

阿里巴巴 Java 开发手册明确规定:线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式。原因就是上面的 OOM 风险。面试说出这个规范非常加分。

synchronized vs ReentrantLock & AQS 原理

面试官问完线程池,90% 会追问锁和 AQS——这是并发编程的核心骨架。

synchronized vs ReentrantLock 全面对比

对比维度synchronizedReentrantLock
实现层面 JVM 层面(monitorenter/monitorexit 字节码) API 层面(java.util.concurrent.locks)
锁的获取 自动获取,退出同步块自动释放 手动 lock() / unlock(),必须在 finally 中释放
可中断 不可中断,等锁时无法响应中断 lockInterruptibly() 可响应中断
公平性 非公平锁 可选公平/非公平(构造参数)
条件变量 只能 wait/notify(一个条件) 多个 Condition,精准唤醒
尝试获取 不能尝试,阻塞等待 tryLock(),获取不到立即返回
锁升级 偏向锁 → 轻量级锁 → 重量级锁(JVM 自动优化) 无锁升级机制,始终依赖 AQS
使用场景 简单同步,锁竞争不激烈 需要公平锁、可中断、多条件、超时等高级功能

synchronized = 公共厕所的门锁:进了就锁,出来就开,没得商量——不需要手动释放,不能超时,不能中断。ReentrantLock = 银行取号排队系统:可以设 VIP 优先(公平锁)、可以超时放弃(tryLock)、可以只叫某一类号(多 Condition 精准唤醒)——功能强大,但要自己记得 unlock。

AQS(AbstractQueuedSynchronizer)核心原理

AQS 是 java.util.concurrent 的基石,ReentrantLock、Semaphore、CountDownLatch 都基于它。

state 状态变量
volatile int state = 0
CAS 抢锁
compareAndSetState(0, 1)
抢不到入队列
CLH 双向队列
LockSupport.park
线程挂起等待
锁释放
state → 0
唤醒后继节点
LockSupport.unpark
后继节点抢锁
CAS setHead

AQS = 银行取号排队系统
state = 柜台状态(0=空闲,1=有人正在办理)
CAS 抢锁 = 进门先看柜台空不空,空就冲过去(CAS),被别人抢先了就放弃
CLH 队列 = 取号排队区,按 FIFO 先来先服务,每个人坐在椅子上等
park/unpark = 坐着睡觉(park 挂起,不消耗 CPU)/ 叫号时被拍醒(unpark 唤醒)
模板方法 = 银行规定了排队叫号的流程(AQS),但每个窗口"怎么办理业务"由各个银行自己定(ReentrantLock、Semaphore 等子类)

关键理解:

state:锁的状态。独占锁 0/1,共享锁可 >1(如 Semaphore 允许多个许可)

CLH 队列:双向链表,FIFO 排队。每个节点记录等待状态(SIGNAL/CANCELLED 等)

模板方法模式:AQS 定义排队/阻塞逻辑,子类只实现 tryAcquire/tryRelease(怎么抢锁/怎么释放)

synchronized 锁升级过程(JDK 1.6+)

无锁
对象刚创建
偏向锁
单线程反复进入
轻量级锁
CAS 自旋
重量级锁
操作系统互斥量

锁只能升级,不能降级(GC 时可以)。面试中说出这四个阶段 + 只升不降就够了。

为什么要锁升级?因为不同竞争程度需要不同的"锁力度":
偏向锁:只有一个人上厕所,门上挂个写着你名字的牌子就行,不用真锁(记录线程ID,零开销)
轻量级锁:两个人偶尔交替用,不用真锁门,谁进门前 CAS 看一眼牌子是否被改(自旋等待,省了操作系统调度)
重量级锁:一堆人抢一个厕所,CAS 自旋太浪费 CPU,直接上真锁(OS 互斥量),没抢到的人被操作系统挂起睡眠
JVM 自动根据竞争激烈程度"逐步升级",尽量用最轻量的方式——这就是 JDK 1.6+ 的核心优化。

面试中 synchronized 和 ReentrantLock 的对比是必考题。加分回答:能说出 synchronized 的锁升级(偏向→轻量→重量),以及 AQS 的 state + CLH 队列模型。

volatile、CAS 与原子类

volatile 三大特性

特性是否保证说明
可见性 ✅ 保证 写操作强制刷回主内存,读操作从主内存加载。基于内存屏障(Memory Barrier)实现
有序性 ✅ 保证 禁止指令重排序(happen-before 规则)。典型场景:DCL 单例中防止对象半初始化
原子性 ❌ 不保证 i++ 不是原子操作(读→改→写三步),volatile 无法保证。需要 AtomicInteger 或 synchronized

volatile = 公告栏:所有人看同一块公告栏(主内存),有人改了立刻刷新上去所有人都能看到(可见性);公告的顺序不能乱贴(有序性);但两个人同时改同一个公告可能冲突覆盖(不保证原子性)。

volatile 典型面试题:DCL 单例为什么需要 volatile?

public class Singleton {
  private static volatile Singleton instance; // ← 为什么加 volatile?

  public static Singleton getInstance() {
    if (instance == null) {         // 1. 第一次检查
      synchronized (Singleton.class) {
        if (instance == null) {  // 2. 第二次检查
          instance = new Singleton(); // 3. 创建对象
        }
      }
    }
    return instance;
  }
}

// new Singleton() 实际分3步:
// ① 分配内存空间
// ② 初始化对象(调用构造方法)
// ③ 将引用指向内存地址
// JVM可能重排为 ①→③→②,此时其他线程拿到未初始化的对象!
// volatile 禁止 ②③ 重排序,确保对象完全初始化后才可见

CAS(Compare And Swap)原理

CAS 是乐观锁的核心,也是所有原子类的底层实现。CAS = 抢红包:看到红包先记住当前金额(期望值),点的时候如果金额没变就抢到(更新成功),如果已经被别人抢了(值变了)就重试。

// CAS 伪代码
do {
  oldValue = get(addr);     // 读取当前值
  newValue = oldValue + 1;    // 计算新值
} while (!compareAndSwap(addr, oldValue, newValue));
// 如果 addr 处的值仍是 oldValue,就更新为 newValue
// 否则说明被其他线程改了,重试(自旋)

CAS 三大问题:

ABA 问题:值从 A→B→A,CAS 认为没变。解决:AtomicStampedReference(加版本号)

自旋开销:竞争激烈时 CAS 一直失败重试,CPU 空转。解决:适当控制并发度

只能保证单变量原子:多个变量需要用锁或 AtomicReference 包装

原子类一览

类别常用类场景
基本类型 AtomicInteger / AtomicLong / AtomicBoolean 计数器、序列号生成
引用类型 AtomicReference / AtomicStampedReference 无锁数据结构、解决ABA
数组类型 AtomicIntegerArray / AtomicLongArray 数组元素的原子更新
字段更新器 AtomicIntegerFieldUpdater 已有类的 volatile 字段原子更新
累加器(JDK8) LongAdder / LongAccumulator 高并发计数,比 AtomicLong 快(分段CAS)

面试官问"volatile 和 synchronized 的区别",标准答案:volatile 保证可见性+有序性但不保证原子性,synchronized 三者都保证。然后主动举 DCL 单例的例子,基本稳了。

ThreadLocal 原理与内存泄漏

ThreadLocal 是什么?

线程本地变量——每个线程独享一份副本,线程之间互不干扰。ThreadLocal = 每个线程有自己的保险箱,别人拿不到你的东西,你也不需要和别人共用。

Thread
ThreadLocalMap
Entry[] table
WeakReference<ThreadLocal>
value
你的数据对象(强引用)

关键:ThreadLocalMap 是 Thread 的成员变量,不是 ThreadLocal 的。每个线程有自己的 Map,所以天然隔离。

ThreadLocal vs synchronized 的区别:synchronized 是让多个线程排队访问同一份数据(共享+互斥);ThreadLocal 是让每个线程各拥有一份数据(隔离+无竞争)。前者用锁保证安全,后者用隔离消除竞争。典型场景:每个线程维护自己的 SimpleDateFormat、数据库连接、用户会话上下文。

内存泄漏问题(面试高频陷阱)

ThreadLocal 对象被 GC 回收
(WeakReference 自动断开)
Entry 的 key 变成 null
但 value 仍然被 Entry 强引用
Thread 长期存活(线程池!)
→ value 永远无法回收 → 内存泄漏

解决方案:

• 使用完必须调用 threadLocal.remove()(在 finally 块中)

• 线程池场景尤其重要!线程被复用,ThreadLocalMap 一直存在

线程池 + ThreadLocal 是内存泄漏重灾区。面试中一定要主动说出:1) 为什么用 WeakReference 还会泄漏 2) 必须手动 remove()。这是区分"看过八股"和"真正理解"的关键点。

线程生命周期 & 同步工具类

线程 6 种状态及转换

NEW
新建,未 start()
RUNNABLE
运行/就绪
BLOCKED
等 synchronized 锁
RUNNABLE
WAITING
wait/join/park
TIMED_WAITING
sleep(n)/wait(n)
RUNNABLE
TERMINATED
执行完毕

三大同步工具类对比

工具类作用典型场景能否复用
CountDownLatch 让一个线程等待其他线程完成。计数器递减到 0 才放行 主线程等待 N 个子任务全部完成后再汇总结果(如多线程查询后合并) ❌ 一次性的
CyclicBarrier 让一组线程互相等待,全部到齐后一起继续执行 多线程分阶段计算,每阶段所有线程都完成后才进入下一阶段(如并行数据处理) ✅ 可循环复用
Semaphore 控制同时访问的线程数量(许可计数) 限制并发访问资源数(如数据库连接池最多 20 个、接口限流) ✅ acquire/release

CountDownLatch 经典场景

// 主线程等待 N 个子任务全部完成
CountDownLatch latch = new CountDownLatch(3);

for (int i = 0; i < 3; i++) {
  pool.submit(() -> {
    try { doWork(); } finally { latch.countDown(); }
  });
}
latch.await(); // 阻塞,直到 count 减到 0
doNextStep(); // 所有子任务完成,继续执行

CountDownLatch vs CyclicBarrier 是高频对比题。核心区别:① CountDownLatch 是计数器递减,CyclicBarrier 是等齐后一起走 ② CountDownLatch 一次性,CyclicBarrier 可复用 ③ CountDownLatch 是一个线程等其他线程,CyclicBarrier 是线程之间互相等。

高频面试问答 TOP 20

1 线程池的执行流程是什么?

① 核心线程未满 → 创建核心线程执行任务
② 核心线程满了 → 任务进入工作队列
③ 队列也满了 → 创建临时线程(到 maximumPoolSize)
④ 全满 → 触发拒绝策略

关键点:先入队列,再扩线程,不是先创建到最大线程数再入队列。

2 核心线程会被回收吗?怎么让它被回收?

默认不会。核心线程即使空闲也会一直存活。

设置 allowCoreThreadTimeOut(true) 后,核心线程在 keepAliveTime 时间内空闲也会被回收。

3 四种拒绝策略分别是什么?生产用哪个?

AbortPolicy(默认):抛 RejectedExecutionException
CallerRunsPolicy:由调用者线程自己执行
DiscardPolicy:默默丢弃,不抛异常
DiscardOldestPolicy:丢弃队列最老任务,重新提交当前任务

生产推荐 CallerRunsPolicy 或自定义策略(日志+持久化+重试)。默认的 AbortPolicy 在高并发下会导致服务异常。

4 线程数怎么配置?CPU密集和IO密集的区别?

CPU 密集型CPU核数 + 1。线程多了上下文切换反而降低性能。
IO 密集型CPU核数 × 2CPU核数 / (1 - 阻塞比)。线程等IO时CPU空闲,可以多配。
• 混合型先按IO密集配,再压测调优。

5 为什么阿里规范禁用 Executors 创建线程池?

newFixedThreadPool / newSingleThreadExecutor:使用 无界 LinkedBlockingQueue,任务堆积会导致 OOM
newCachedThreadPool / newScheduledThreadPool:maximumPoolSize = Integer.MAX_VALUE,可能创建海量线程导致 OOM

应使用 ThreadPoolExecutor 显式指定参数,确保队列有界、线程数可控。

6 常用的阻塞队列有哪些?怎么选?

LinkedBlockingQueue:链表实现,默认无界(必须指定容量!),吞吐量较高
ArrayBlockingQueue:数组实现,有界,创建时必须指定容量,公平/非公平可选
SynchronousQueue:不存储元素,每个put必须等take,适合直接交接(CachedThreadPool用)
PriorityBlockingQueue:支持优先级排序的无界队列
DelayQueue:延迟队列,元素到期才能取出

生产推荐 ArrayBlockingQueue(有界+公平)或指定容量的 LinkedBlockingQueue。

7 submit() 和 execute() 的区别?

execute():提交 Runnable,无返回值,异常直接抛出
submit():提交 Runnable/Callable,返回 Future,异常封装在 Future.get() 中

注意:submit() 的任务如果抛异常,不会直接打印,必须 future.get() 时才会看到。这是常见坑!

8 线程池怎么关闭?shutdown 和 shutdownNow 的区别?

shutdown():平滑关闭。不再接受新任务,等待已提交的任务执行完
shutdownNow():立即关闭。尝试中断正在执行的任务,返回未执行的任务列表
awaitTermination():配合shutdown使用,阻塞等待所有任务完成或超时

pool.shutdown();
pool.awaitTermination(60, TimeUnit.SECONDS);
9 线程池的状态有哪些?怎么转换的?

5 种状态(存在 ctl 的高 3 位):
RUNNING(111):接受新任务,处理队列任务
SHUTDOWN(000):不接受新任务,但处理队列任务(shutdown()后)
STOP(001):不接受新任务,不处理队列,中断进行中任务(shutdownNow()后)
TIDYING(010):所有任务终止,workerCount=0
TERMINATED(011):terminated()方法执行完

转换:RUNNING → SHUTDOWN → STOP → TIDYING → TERMINATED

10 如何实现线程池的动态调整?(美团动态线程池思路)

ThreadPoolExecutor 提供了运行时修改参数的方法:

setCorePoolSize():动态修改核心线程数
setMaximumPoolSize():动态修改最大线程数
setKeepAliveTime():动态修改空闲时间

美团的做法:配置中心(如 Apollo/Nacos)监听参数变更 → 调用上述方法动态调整 → 结合监控告警(队列长度、活跃线程数、拒绝次数)实现自适应。

11 线程池中抛异常了怎么处理?

execute() 提交的任务,异常会直接抛出,线程终止,线程池创建新线程替代
submit() 提交的任务,异常被吞到 Future 中,必须 future.get() 才能捕获
• 3 种处理方式:
  1. try-catch 包住 run() 方法体
  2. 实现 afterExecute() 钩子(继承 ThreadPoolExecutor)
  3. 设置 UncaughtExceptionHandler

12 线程池的工作原理(Worker 机制)?

• ThreadPoolExecutor 内部用 HashSet<Worker> 管理所有线程
• Worker 继承 AQS,实现了 不可重入锁(避免任务中调用池方法导致死锁)
• Worker.runWorker() 是核心方法:不断从队列 getTask() 取任务执行
• 核心线程取任务用 queue.take()(阻塞等待),临时线程用 queue.poll(keepAliveTime)(超时回收)

这就是为什么核心线程不会死、临时线程会超时回收的底层原因。

13 synchronized 和 ReentrantLock 的区别?

实现:synchronized 是 JVM 层面(字节码),ReentrantLock 是 API 层面
释放:synchronized 自动释放,ReentrantLock 必须在 finally 中手动 unlock()
可中断:synchronized 不可中断,ReentrantLock 支持 lockInterruptibly()
公平性:synchronized 非公平,ReentrantLock 可选公平/非公平
条件:synchronized 一个 wait/notify,ReentrantLock 多个 Condition
尝试:synchronized 阻塞等待,ReentrantLock tryLock() 非阻塞

14 AQS 的原理是什么?

AQS = volatile int state + CLH 双向队列

state:表示锁状态。独占锁 0/1,共享锁可大于 1
CLH 队列:存放等锁的线程,FIFO 排队
抢锁:CAS 修改 state,成功则获取锁
排队:抢不到就加入 CLH 队列尾,LockSupport.park() 挂起
释放:state 归零,唤醒后继节点 unpark()

模板方法模式:AQS 搞定排队/阻塞逻辑,子类只需实现 tryAcquire/tryRelease。

15 volatile 能保证线程安全吗?

不能完全保证。volatile 只保证 可见性 + 有序性,不保证原子性。

可见性:一个线程修改,其他线程立即可见(内存屏障)
有序性:禁止指令重排序(DCL 单例的关键)
原子性:i++ 不是原子操作,volatile 无法保证。需要 AtomicInteger 或 synchronized

16 CAS 是什么?有什么问题?

CAS = Compare And Swap,乐观锁核心实现。比较当前值与期望值,相同则更新,不同则重试

三大问题:
ABA 问题:A→B→A 看起来没变。解决:AtomicStampedReference(版本号)
自旋开销:竞争激烈时 CAS 反复失败,CPU 空转
只能保证单变量:多变量需 AtomicReference 或加锁

17 ThreadLocal 为什么会内存泄漏?怎么解决?

ThreadLocalMap 的 key 是 WeakReference,GC 时 key 被回收变为 null,但 value 是强引用,仍被 Entry 引用 → 无法回收 → 泄漏。

线程池场景更严重:线程被复用,ThreadLocalMap 一直存在。

解决方案:使用完必须 threadLocal.remove(),放在 finally 块中。

18 CountDownLatch 和 CyclicBarrier 的区别?

CountDownLatch:一个线程等 N 个线程完成(计数器递减),一次性,不可复用
CyclicBarrier:N 个线程互相等齐后一起执行,可循环复用(自动 reset)
Semaphore:控制并发线程数(许可计数),acquire/release

记忆:CountDown = 倒计时(减法),Cyclic = 循环栅栏(等齐),Semaphore = 信号灯(限流)

19 synchronized 的锁升级过程?

JDK 1.6+ 优化,锁从低到高升级:
无锁 → 偏向锁 → 轻量级锁 → 重量级锁

偏向锁:只有一个线程访问,直接在对象头记录线程 ID,无需 CAS
轻量级锁:两个线程交替访问,CAS 自旋抢锁
重量级锁:竞争激烈,升级为 OS 互斥量,线程阻塞
• 锁只能升级,不能降级(GC 时可以)

20 线程有哪些状态?BLOCKED 和 WAITING 的区别?

6 种状态:NEW → RUNNABLE → BLOCKED / WAITING / TIMED_WAITING → TERMINATED

BLOCKED:等待 synchronized 锁(被动等待)
WAITING:调用 wait()/join()/park()(主动等待,需要被其他线程唤醒)
TIMED_WAITING:调用 sleep(n)/wait(n)/parkNanos(n)(超时自动唤醒)

速记卡片(面试前 5 分钟看)

7大参数速记:

核心数 → 最大数 → 存活时间 → 时间单位 → 工作队列 → 线程工厂 → 拒绝策略

执行流程速记:

核心线程 → 入队列 → 临时线程 → 拒绝策略

拒绝策略速记:

Abort抛异常 / CallerRuns自己跑 / Discard丢掉 / DiscardOldest丢老的

线程数速记:

CPU密集 = 核数+1 | IO密集 = 核数×2 或 核数/(1-阻塞比)

锁对比速记:

synchronized=JVM自动/非公平 | ReentrantLock=API手动/可公平/可中断

AQS 速记:

volatile state + CLH队列 + CAS抢锁 + park挂起 + unpark唤醒

volatile 速记:

可见性✅ 有序性✅ 原子性❌ → i++用AtomicInteger

ThreadLocal 速记:

线程隔离 ✅ | 内存泄漏 ⚠️ | 用完必须remove()

面试官追问链(真实面试路径)

面试不是一个个孤立问题,而是一条链。以下是最常见的追问路径:

链路 1:线程池 → 锁 → AQS

"说说线程池的执行流程" → "线程池内部 Worker 用了什么锁?" → "AQS 原理是什么?" → "synchronized 和 ReentrantLock 区别?"

链路 2:volatile → CAS → 原子类

"volatile 保证什么?" → "i++ 用 volatile 行吗?" → "那用什么?CAS 原理?" → "ABA 问题怎么解决?"

链路 3:ThreadLocal → 内存泄漏 → 线程池

"ThreadLocal 原理?" → "为什么用 WeakReference?" → "还会内存泄漏吗?" → "线程池中怎么用 ThreadLocal?"

链路 4:同步工具 → 线程通信

"CountDownLatch 和 CyclicBarrier 区别?" → "Semaphore 怎么用?" → "wait/notify 和 Condition 区别?"

Java 并发编程面试全攻略 · 线程池·锁·AQS·volatile·ThreadLocal · 全栈资料库
核心三原则速记

并发面试三原则:① synchronized 锁对象要明确,静态方法锁 Class、实例方法锁 this → ② 线程池参数要压测,核心线程数不是拍脑袋 → ③ volatile 保证可见性不保证原子性,能用 Atomic 就别用 synchronized