Skip to content

六、多线程与并发(J51–J62) ​

J51 · 知识点:线程创建方式 ​

【题目】下列哪种方式不能用来创建线程?

A. 继承 Thread 类 B. 实现 Runnable 接口 C. 实现 Callable 接口 + FutureTask D. 实现 Serializable 接口

答案:D

【考点】线程创建的三种方式。

【结论】选 D。Serializable 是序列化标记接口,与线程创建无关;Java 线程只能通过 Thread、Runnable、Callable 三种方式创建。

【逐项辨析】

  • A 错:继承 Thread 并重写 run() 是创建线程的经典方式之一,完全合法。
  • B 错:实现 Runnable 接口并传入 Thread 构造器是推荐做法,可避免单继承限制。
  • C 错:实现 Callable 接口配合 FutureTask 可创建有返回值的异步线程,完全合法。
  • D 正确:Serializable 仅用于对象序列化与反序列化,不具备任何线程语义,不能用于创建线程。

【知识点】 Java 提供三种创建线程的标准方式:

  1. 继承 Thread 类:重写 run() 方法,直接 new Thread() 启动。缺点是 Java 单继承,无法同时继承其他类。
  2. 实现 Runnable 接口:重写 run(),将实例传入 Thread 构造器。解耦任务逻辑与线程机制,最常用。
  3. 实现 Callable 接口:重写 call(),配合 FutureTask 包装后交给 Thread 或线程池执行。支持返回值和抛异常。

Callable 的本质仍是通过 Runnable 落地——FutureTask 实现了 RunnableFuture,而 RunnableFuture 继承自 Runnable。因此方式 ③ 底层也归于 Runnable 执行体系。

【记忆锚点】 「线程创建三剑客:Thread、Runnable、Callable;Serializable 是局外人,专门管存盘。」

【易混对比】 三种方式的差异对照:

创建方式返回值抛异常单继承限制适用场景
继承 Thread无不能抛受检异常有简单演示
实现 Runnable无不能抛受检异常无常规多线程任务
实现 Callable有(通过 Future.get)可以无需返回结果或抛异常

换问法:哪种方式最推荐?——实现 Runnable / Callable,避免单继承限制。

【自测】 以下代码能否编译通过?

java
class MyTask implements Callable<String> {
    public String call() { return "ok"; }
}
public static void main(String[] args) throws Exception {
    FutureTask<String> ft = new FutureTask<>(new MyTask());
    new Thread(ft).start();
    System.out.println(ft.get());
}

答:能编译通过。Callable 任务经 FutureTask 包装后传给 Thread 启动,ft.get() 阻塞获取返回值 "ok"。与第 J51 题连考。

【知识关联】

  • 同库关联:与 J52(start/run)、J61(Callable)、J58(线程池更推荐)同一题群;与 P75(GIL)对照——Python 多线程受 GIL 限制。
  • 实现层:Thread 本身可 start 一次;Runnable 无返回值;JDK5 引入 Callable/Future。
  • 面试追问:① 为何推荐线程池而不是 new Thread?② Runnable 与 Callable 差异?

【拓展延伸】

  • 变式问法:问有返回值用谁;问线程池核心参数(J58)。
  • 版本差异:JDK 21 虚拟线程(Project Loom)极大改变创建成本与调度模型。
  • 工程注意点:生产环境必须用线程池;命名线程便于排障;避免在请求线程里同步开线程。

J52 · 知识点:start() 与 run() ​

【题目】直接调用 thread.run() 与 thread.start() 的区别是?

A. run() 在当前线程中直接执行(普通方法调用),start() 会创建新线程并在新线程中执行 run() B. 没有区别 C. start() 在当前线程中执行 D. run() 会抛出异常

答案:A

【考点】线程启动机制:start 与 run 的本质区别。

【结论】选 A。run() 是普通方法调用,在当前线程同步执行;start() 触发 JVM 创建新线程,由调度器异步执行 run()。

【逐项辨析】

  • A正确:直接调 run() 只在当前调用线程中执行方法体;start() 使线程进入 NEW → RUNNABLE 状态,由 JVM 调度新线程执行。
  • B错:两者有本质区别——run() 不创建新线程(只是普通方法调用),start() 才启动独立执行流。
  • C错:start() 是异步启动新线程,不是在当前线程中执行;「在当前线程执行」的是直接调用 run()。
  • D错:run() 作为普通方法不会主动抛异常(除非方法体内部有异常),它不是异常来源。

【知识点】 Thread.start() 是线程启动的唯一正确入口,内部调用 native 方法 start0(),向 JVM 申请创建操作系统级线程。start() 只能调用一次,重复调用会抛 IllegalThreadStateException。直接调用 run() 绕过了 JVM 的线程调度机制,等同于在当前线程栈中执行一次普通方法调用,不会产生任何并发效果。笔试高频陷阱:题目给出 thread.run() 后问“是否创建了新线程”——答案是否定的。

【记忆锚点】 「想开线程用 start,直接 run 是假把式;start 只准调一次,重复调用必报错。」

【易混对比】 易混:run() 与 start() 在多线程语境下的效果差异

调用方式是否新建 OS 线程执行线程调用次数限制异常
thread.run()否当前线程无限制无特殊异常
thread.start()是新建线程仅一次IllegalThreadStateException

换问法:同一个 Thread 对象连续调用两次 start() 会怎样?——第二次抛 IllegalThreadStateException。

【自测】 以下代码输出什么?

java
Thread t = new Thread(() -> System.out.print(Thread.currentThread().getName()));
t.run();
t.start();

答:先输出 main(run 在当前线程执行),后输出 Thread-0(start 创建新线程执行)。与第 J52 题连考。

【知识关联】

  • 同库关联:与 J51、J55(线程状态:start 后进入 RUNNABLE)。
  • 实现层:start() 请求 JVM 创建新线程并在新栈中调用 run();直接 run() 只是当前线程普通方法调用。
  • 面试追问:① 同一线程能 start 两次吗?② run 抛异常会怎样?

【拓展延伸】

  • 变式问法:给出两种调用方式问输出「是否并发」。
  • 版本差异:语义稳定。
  • 工程注意点:框架中不要随意 start;中断用 interrupt 而不是 stop(已废弃)。

J53 · 知识点:synchronized 的锁对象 ​

【题目】synchronized 修饰静态方法与实例方法时,锁对象分别是? A. 静态方法锁当前类的 Class 对象,实例方法锁当前实例 this B. 都是当前对象 this C. 都是类的 Class 对象 D. 静态方法锁 this,实例方法锁 Class

答案:A

【考点】synchronized 三种用法对应的锁对象。

【结论】选 A。实例方法锁当前对象 this,静态方法锁当前类的 Class 对象,二者互不影响。

【逐项辨析】

  • B错:静态方法不锁 this,锁的是 Class 对象。
  • A正确:synchronized 修饰实例方法时,锁对象为当前实例 this;修饰静态方法时,锁对象为该方法所属类的 Class 对象(如 MyClass.class)。
  • C错:实例方法锁的是 this,不是 Class 对象。
  • D错:描述完全相反。

【知识点】 synchronized 有三种用法,对应不同锁对象:

  1. 修饰实例方法:锁当前实例(this)。
  2. 修饰静态方法:锁当前类的 Class 对象(类级别锁)。
  3. 修饰代码块:显式指定锁对象(synchronized(obj))。

由于锁对象不同,静态 synchronized 方法与实例 synchronized 方法之间不会发生阻塞竞争。但同一实例的多个 synchronized 实例方法之间互斥,不同实例的 synchronized 实例方法互不阻塞。Class 对象在 JVM 中全局唯一,因此所有访问该类的静态 synchronized 方法之间全局互斥。

【记忆锚点】 「实例锁 this,静态锁 Class;两类锁对象,井水不犯河水。」

【易混对比】 易混:synchronized 三种用法的锁粒度

用法锁对象粒度影响范围
synchronized 实例方法this对象级同一实例的同步方法互斥
synchronized 静态方法Class 对象类级所有实例的静态同步方法互斥
synchronized(对象) 代码块指定对象可控仅块内代码互斥

换问法:一个线程执行 obj.staticMethodA()(同步静态),另一个线程执行 obj.methodB()(同步实例),是否互斥?——不互斥,锁对象不同。

【自测】 以下代码中,t1 和 t2 是否互斥?

java
class Demo {
    synchronized void m1() {}
    static synchronized void m2() {}
}

答:不互斥。m1 锁 this,m2 锁 Demo.class,锁对象不同,两个线程可同时进入。与第 J53 题连考。

【知识关联】

  • 同库关联:与 J54(volatile 保证可见不保证原子)、J60(CAS)、J62(死锁)、J70(JMM)、J42(ConcurrentHashMap 锁粒度)。
  • 实现层:JDK6+ 锁升级(偏向→轻量→重量);monitorenter/exit;实例方法锁 this,静态方法锁 Class。
  • 面试追问:① 锁升级过程?② synchronized 与 ReentrantLock 区别?

【拓展延伸】

  • 变式问法:锁不同对象是否互斥;synchronized(this) 与 synchronized(Class)。
  • 版本差异:JDK 15+ 默认关闭偏向锁(JEP 374)。
  • 工程注意点:缩小同步块;锁对象私有 final;优先并发容器/原子类减少手写锁。

J54 · 知识点:volatile ​

【题目】关于 volatile 关键字,下列说法正确的是?

A. 保证操作的原子性 B. 保证可见性并禁止指令重排序 C. 可以完全替代 synchronized D. 只能修饰引用类型

答案:B

【考点】volatile 的两大语义及其局限性。

【结论】选 B。volatile 保证可见性并禁止指令重排序,但不保证原子性,无法替代 synchronized。

【逐项辨析】

  • A 错:volatile 不保证原子性。例如 volatile int i 的多线程 i++ 仍会因读-改-写三步非原子而丢数据。
  • B 正确:volatile 写操作立即刷回主内存,读操作从主内存读取,保证可见性;通过内存屏障禁止编译器和 CPU 的指令重排序。
  • C 错:volatile 不能替代 synchronized。它无锁机制,无法解决复合操作的原子性问题。
  • D 错:volatile 可修饰基本类型和引用类型,不限于引用类型。

【知识点】 volatile 是 Java 轻量级同步机制,核心语义两条:

  1. 可见性:volatile 变量的写操作会触发 Store-Store、Store-Load 内存屏障,将工作内存值刷回主内存;读操作触发 Load-Load、Load-Store 屏障,从主内存重新读取。保证一个线程修改后,其他线程立即可见。
  2. 禁止指令重排序:编译器和 CPU 不会对 volatile 读写进行乱序优化。

但 volatile 不保证原子性。i++ 实质是 read → increment → write 三个独立步骤,多线程并发时仍可能覆盖。适用场景:状态标志位(boolean flag)、单例双重检查锁中的实例引用(instance 声明为 volatile,防止半初始化对象暴露)。

【记忆锚点】 「volatile 两管事:可见加禁排;原子管不了,别当锁来用。」

【易混对比】 volatile vs synchronized 的能力边界:

特性volatilesynchronized
可见性保证保证
原子性不保证保证(临界区互斥)
有序性保证(禁止重排)保证(临界区串行)
阻塞不会会(线程排队)
适用场景状态标志、单例 DCL复合操作、临界区

换问法:volatile 修饰的 long/double 在 32 位 JVM 上是否原子?——volatile 保证读写原子(单步操作),但 i++ 仍是复合操作。

【自测】 以下代码在多线程下能否保证 count 正确累加到 1000?

java
volatile int count = 0;
// 10 个线程各执行 100 次 count++

答:不能。count++ 是读-改-写三步,volatile 不保证这三步整体原子,多线程下仍会丢数据。应改用 AtomicInteger 或 synchronized。与第 J54 题连考。

【知识关联】

  • 同库关联:与 J53、J60、J70(happens-before)核心连考;与 J42 ConcurrentHashMap 可见性。
  • 实现层:volatile 保证可见性与有序性(内存屏障),不保证复合操作原子性;对 long/double 写的原子性有帮助。
  • 面试追问:① i++ 用 volatile 能否线程安全?② DCL 为何需要 volatile?

【拓展延伸】

  • 变式问法:状态标志位用 volatile;双检锁单例。
  • 版本差异:JMM 语义稳定;VarHandle(JDK9+)提供更细粒度。
  • 工程注意点:计数用 AtomicLong;标志位可用 volatile;别用 volatile 替代锁做计数。

J55 · 知识点:线程状态 ​

【题目】Java 线程的生命周期状态中,下列哪个不是合法的状态名?

A. NEW B. RUNNABLE C. WAITING D. DEAD

答案:D

【考点】线程 6 种状态的官方定义。

【结论】选 D。Java 线程的合法状态名为 TERMINATED,不存在 "DEAD" 状态。

【逐项辨析】

  • A 错:NEW 是合法状态,表示线程刚创建尚未调用 start()。
  • B 错:RUNNABLE 是合法状态,JVM 将就绪和运行统一归为 RUNNABLE。
  • C 错:WAITING 是合法状态,表示无限期等待(如 Object.wait()、Thread.join() 无超时)。
  • D 正确:Java 中没有 DEAD 状态,线程终止后的状态名为 TERMINATED。

【知识点】 Thread.State 枚举明确定义 6 种线程状态:

  1. NEW:新建状态,线程对象已创建,未调用 start()。
  2. RUNNABLE:可运行状态,包含操作系统层面的就绪(Ready)和运行(Running),受调度器管理。
  3. BLOCKED:阻塞状态,等待监视器锁(如进入 synchronized 块/方法时被阻塞)。
  4. WAITING:无限期等待,需等待其他线程显式唤醒(wait()、join()、LockSupport.park())。
  5. TIMED_WAITING:限期等待,带超时参数的等待(sleep(long)、wait(long)、join(long))。
  6. TERMINATED:终止状态,run() 正常结束或异常退出。

旧文档或教材中可能用“死亡”描述 TERMINATED,但状态名本身不是 DEAD。TIMED_WAITING 与 WAITING 的区别仅在于是否带超时参数。

【记忆锚点】 「新建可运行,阻塞两等待,最后已终止;DEAD 是江湖绰号,正规军不认。」

【易混对比】 易混:WAITING 与 BLOCKED 的区别

状态触发条件唤醒方式是否持锁
BLOCKED等待进入 synchronized锁释放后竞争否(未进入临界区)
WAITING调用 wait() / join() 等notify() / 线程结束 / 中断否(已释放锁)
TIMED_WAITING调用带超时的等待方法超时自动醒 / 被唤醒否

换问法:调用 Thread.sleep(1000) 后线程进入什么状态?——TIMED_WAITING。

【自测】 以下代码中 t 线程在 sleep 期间处于什么状态?

java
Thread t = new Thread(() -> {
    try { Thread.sleep(5000); } catch (InterruptedException e) {}
});
t.start();

答:TIMED_WAITING。Thread.sleep() 使线程进入限期等待状态,时间到后自动转为 RUNNABLE。与第 J55 题连考。

【知识关联】

  • 同库关联:与 J56(sleep/wait 状态差异)、J57(join)、J52。
  • 实现层:NEW/RUNNABLE/BLOCKED/WAITING/TIMED_WAITING/TERMINATED;BLOCKED 等锁,WAITING 等通知。
  • 面试追问:① sleep 时线程状态?② wait 时状态?

【拓展延伸】

  • 变式问法:给场景选状态枚举。
  • 版本差异:JDK 定义清晰稳定。
  • 工程注意点:jstack 排障先看状态;避免大量 TIMED_WAITING 泄漏。

J56 · 知识点:sleep 与 wait ​

【题目】关于 sleep() 和 wait() 的区别,正确的是? A. 两者都会释放锁 B. sleep 会释放锁,wait 不会释放锁 C. wait 不需要在同步块中调用 D. sleep 是 Thread 的静态方法,wait 是 Object 的方法

答案:D

【考点】sleep/wait 的所属类、是否释放锁、调用条件三组差异。

【结论】选 D。sleep 是 Thread 的静态方法,wait 是 Object 的实例方法;sleep 不释放锁,wait 释放锁并需同步块内调用。

【逐项辨析】

  • A错:sleep 不释放锁(抱着锁睡觉),只有 wait 释放锁,两者并非都释放。
  • B错:恰好说反——sleep 不释放锁,wait 会释放当前持有的对象锁。
  • C错:wait 必须在 synchronized 块/方法内调用,否则抛 IllegalMonitorStateException。
  • D正确:Thread.sleep(long) 是 Thread 类的静态方法;Object.wait() 定义在 Object 类中,任何对象都可调用。

【知识点】 sleep 与 wait 的三组核心差异:

  1. 所属类:sleep 是 Thread 的静态方法;wait 是 Object 的实例方法。
  2. 锁行为:sleep 仅暂停当前线程指定时间,不释放已持有的任何锁;wait 必须持有对象监视器锁,调用后释放该锁,线程进入该对象的等待队列。
  3. 唤醒方式:sleep 到时间自动恢复 RUNNABLE,不依赖外部通知;wait 需依赖其他线程调用同对象的 notify()/notifyAll(),或等待超时(wait(long))。

sleep 可在任意位置调用;wait 必须在同步上下文中调用,且调用后线程状态变为 WAITING 或 TIMED_WAITING。被 notify 唤醒后,线程需重新竞争锁,竞争成功后从 wait 处继续执行。

【记忆锚点】 「sleep 抱锁睡,wait 放锁等;sleep 在 Thread,wait 在 Object;一个到点醒,一个等人叫。」

【易混对比】 sleep vs wait 对照表:

维度sleepwait
所属类ThreadObject
是否释放锁否是
调用前提无限制必须持有对象锁(同步块内)
唤醒条件超时自动醒notify/notifyAll / 超时
异常InterruptedExceptionInterruptedException / IllegalMonitorStateException
用途暂停执行一段时间线程间协调通信

换问法:调用 wait() 后线程立即退出同步块吗?——不是立即,wait 释放锁并进入等待队列,被唤醒后需重新竞争锁才能继续执行。

【自测】 以下代码输出顺序是什么?

java
synchronized (lock) {
    System.out.print("A");
    lock.wait();
    System.out.print("B");
}

答:输出 A,然后线程在 lock 上等待,直到其他线程调用 lock.notify()/notifyAll() 唤醒并重新获得锁后,才输出 B。与第 J56 题连考。

【知识关联】

  • 同库关联:与 J53(wait 必须持锁)、J55 状态;Object.wait/notify 与 JUC Condition 对照。
  • 实现层:sleep 不释放锁;wait 释放锁并进入等待集;wait 必须在同步块中否则 IllegalMonitorStateException。
  • 面试追问:① 为何 wait 要在同步块?② sleep 与 yield 区别?

【拓展延伸】

  • 变式问法:代码问是否抛异常/是否释放锁。
  • 版本差异:语义稳定。
  • 工程注意点:新代码优先 Lock/Condition 或并发工具;wait 用 while 检查条件防虚假唤醒。

J57 · 知识点:join ​

【题目】t.join() 的作用是?

A. 当前线程等待 t 线程执行结束后再继续 B. 暂停 t 线程 C. 让 t 线程开始执行 D. 中断 t 线程

答案:A

【考点】join 的语义:线程间的顺序协调。

【结论】选 A。join() 使当前调用线程阻塞,直到目标线程 t 执行完毕后才继续。

【逐项辨析】

  • A 正确:t.join() 由当前线程调用,当前线程进入 WAITING/TIMED_WAITING,等待 t 线程终止。
  • B 错:join 不会暂停 t 线程,t 线程继续正常运行。
  • C 错:让 t 线程开始执行应调用 t.start(),不是 join()。
  • D 错:中断线程应调用 t.interrupt(),join 不具备中断功能。

【知识点】 join() 是线程间顺序协调的核心 API,底层基于 wait/notifyAll 实现:当线程 t 终止时,JVM 会自动调用 t.notifyAll(),唤醒所有在 t 对象上等待的线程。join() 有多个重载:

  • join():无限期等待 t 结束。
  • join(long millis):最多等待 millis 毫秒,超时后即使 t 未结束也继续执行。
  • join(long millis, int nanos):更精细的超时控制。

join 常被用于主线程等待子线程完成后再汇总结果的场景,如多线程下载后合并文件、并行计算后收集结果等。调用 join 的线程需处理 InterruptedException。

【记忆锚点】 「join 是排队等检票:当前线程站队尾,等 t 线程走完才放行。」

【易混对比】 易混:join、sleep、wait 的线程行为差异

方法作用对象当前线程状态是否释放锁依赖条件
t.join()目标线程 tWAITING/TIMED_WAITING否(若不在同步块内)t 线程终止
Thread.sleep()当前线程自身TIMED_WAITING否超时
obj.wait()当前线程自身WAITING/TIMED_WAITING是notify/超时

换问法:主线程调用 t.join() 时,t 线程如果持有锁,主线程是否会释放自己的锁?——join 不释放当前线程已持有的任何锁。

【自测】 以下代码输出什么?

java
Thread t = new Thread(() -> {
    try { Thread.sleep(100); } catch (InterruptedException e) {}
    System.out.print("B");
});
t.start();
t.join();
System.out.print("A");

答:BA。t.join() 使主线程等待 t 结束;t 休眠 100ms 后输出 B 并终止,主线程被唤醒后输出 A。与第 J57 题连考。

【知识关联】

  • 同库关联:与 J51/J55;与 CompletableFuture.allOf(现代替代)。
  • 实现层:join 实质 wait 目标线程终止;可被中断。
  • 面试追问:① join 与 wait 关系?② 如何带超时等待?

【拓展延伸】

  • 变式问法:多个线程 join 顺序。
  • 版本差异:语义稳定。
  • 工程注意点:避免在请求线程无限 join;用带超时版本;线程池场景用 Future.get。

J58 · 知识点:线程池参数 ​

【题目】下列哪一项不属于 ThreadPoolExecutor 的核心参数?

A. 核心线程数 corePoolSize B. 最大线程数 maximumPoolSize C. 任务队列 workQueue D. 线程优先级 priority

答案:D

【考点】线程池七个核心参数与任务提交流程。

【结论】选 D。ThreadPoolExecutor 的 7 个核心参数中不包含线程优先级 priority。

【逐项辨析】

  • A 错:corePoolSize 是七大核心参数之一,表示常驻核心线程数。
  • B 错:maximumPoolSize 是七大核心参数之一,表示线程池允许的最大线程数。
  • C 错:workQueue 是七大核心参数之一,用于缓存待执行的任务。
  • D 正确:线程优先级(priority)由 Thread 对象或 ThreadFactory 内部设定,不是 ThreadPoolExecutor 构造方法的显式参数。

【知识点】 ThreadPoolExecutor 的七个核心参数:

  1. corePoolSize:核心线程数,线程池维护的最小线程数。
  2. maximumPoolSize:最大线程数,任务队列满后可创建的临时线程上限。
  3. keepAliveTime:非核心线程的空闲存活时间。
  4. unit:keepAliveTime 的时间单位。
  5. workQueue:任务等待队列(如 LinkedBlockingQueue、ArrayBlockingQueue、SynchronousQueue)。
  6. threadFactory:线程工厂,用于创建新线程(可自定义线程名、优先级、守护状态等)。
  7. handler:拒绝策略,当线程数和队列均满时的处理方案。

任务提交流程:① 核心线程未满 → 创建核心线程立即执行;② 核心线程已满、队列未满 → 任务入队等待;③ 队列已满、线程数未达最大 → 创建非核心线程执行;④ 线程数已达最大且队列已满 → 触发拒绝策略。

【记忆锚点】 「线程池七参数:两大一小一时间,一队列一工厂一策略;priority 不在册,藏在工厂里。」

【易混对比】 常见阻塞队列特性对比:

队列类型有界性特点适用场景
LinkedBlockingQueue可选有界链表结构,默认无界任务量不确定,内存充裕
ArrayBlockingQueue有界数组结构,需指定容量任务量可控,防止 OOM
SynchronousQueue无容量不存储任务,直接移交高吞吐,需配合较大 maxPoolSize
PriorityBlockingQueue无界按优先级排序任务有优先级差异

换问法:corePoolSize = 5,max = 10,队列容量 100,同时提交 120 个任务,最终多少个线程在运行?——10 个(5 核心 + 5 临时),其中 10 个任务在执行,100 个在队列,10 个触发拒绝策略。

【自测】 以下线程池配置是否合理?

java
new ThreadPoolExecutor(10, 10, 0L, TimeUnit.MILLISECONDS,
    new LinkedBlockingQueue<>());

答:配置固定 10 线程,队列默认无界,可能因任务积压导致 OOM。应给队列设界或使用 ArrayBlockingQueue。与第 J58 题连考。

【知识关联】

  • 同库关联:与 J59(拒绝策略)、J51;阿里规约要求用 ThreadPoolExecutor 显式创建。
  • 实现层:corePoolSize、maximumPoolSize、keepAliveTime、workQueue、threadFactory、handler;先 core 再 queue 再 max。
  • 面试追问:① 任务提交流程?② 为何不用 Executors 快捷工厂?

【拓展延伸】

  • 变式问法:给参数问触发拒绝的条件。
  • 版本差异:语义稳定;虚拟线程下 Executors.newVirtualThreadPerTaskExecutor 成为新选项(JDK21)。
  • 工程注意点:有界队列防 OOM;线程命名;监控队列积压;区分 CPU/IO 密集型参数。

J59 · 知识点:线程池拒绝策略 ​

【题目】线程池队列已满且线程数达到最大值时,默认的拒绝策略是? A. DiscardPolicy(静默丢弃) B. AbortPolicy(抛出 RejectedExecutionException) C. CallerRunsPolicy(调用者线程执行) D. DiscardOldestPolicy(丢弃最老任务)

答案:B

【考点】四种拒绝策略及其默认值。

【结论】选 B。ThreadPoolExecutor 默认拒绝策略为 AbortPolicy,直接抛出 RejectedExecutionException。

【逐项辨析】

  • B正确:AbortPolicy 是默认策略,任务和队列均满时抛异常,终止提交。
  • A错:DiscardPolicy 静默丢弃新任务,不是默认策略。
  • C错:CallerRunsPolicy 由提交任务的线程自身执行 run(),起到降速效果,不是默认策略。
  • D错:DiscardOldestPolicy 丢弃队列中最老任务后重试提交,不是默认策略。

【知识点】 ThreadPoolExecutor 内置四种拒绝策略,均实现 RejectedExecutionHandler 接口:

  1. AbortPolicy(默认):抛 RejectedExecutionException,调用者感知失败。
  2. CallerRunsPolicy:由提交任务的线程(调用者)亲自执行 run(),相当于反向压力,使提交线程忙于执行任务而无法继续提交新任务,实现自然降速。
  3. DiscardPolicy:静默丢弃新提交的任务,不抛异常也不执行,数据可能丢失,慎用。
  4. DiscardOldestPolicy:丢弃队列中等待时间最长的任务,然后尝试重新提交当前任务,适用于希望优先处理新数据的场景。

自定义拒绝策略:实现 RejectedExecutionHandler 接口,可记录日志、持久化到数据库、或发送到消息队列稍后重试。

【记忆锚点】 「默认 Abort 直接炸,Caller 自己干,Discard 悄悄丢,Oldest 扔老的。」

【易混对比】 四种拒绝策略的适用场景:

策略行为是否丢任务适用场景
AbortPolicy抛异常是(不执行)需要调用者感知并处理失败
CallerRunsPolicy调用者执行否希望自动降速、不丢数据
DiscardPolicy静默丢弃是(无感知)容忍数据丢失的边缘场景
DiscardOldestPolicy丢弃最老任务是(老任务)新数据比旧数据更重要的场景

换问法:如何自定义拒绝策略?——实现 RejectedExecutionHandler 接口的 rejectedExecution 方法。

【自测】 以下代码在任务过多时会怎样?

java
ExecutorService pool = new ThreadPoolExecutor(
    1, 1, 0, TimeUnit.SECONDS,
    new ArrayBlockingQueue<>(1),
    new ThreadPoolExecutor.DiscardPolicy()
);
for (int i = 0; i < 10; i++) pool.submit(() -> System.out.print("A"));

答:最多执行 2 个任务(1 线程 + 1 队列),其余 8 个被静默丢弃,输出 AA。与第 J59 题连考。

【知识关联】

  • 同库关联:与 J58 强绑定;与降级限流(Spring S27 Sentinel)产品层呼应。
  • 实现层:AbortPolicy 抛异常;CallerRunsPolicy 回调提交线程;DiscardPolicy/Oldest 静默策略。
  • 面试追问:① 生产常用哪种?② 如何自定义拒绝策略?

【拓展延伸】

  • 变式问法:背压场景选 CallerRuns 的利弊。
  • 版本差异:语义稳定。
  • 工程注意点:不要默默丢任务;打日志+监控告警;与熔断限流配合。

J60 · 知识点:原子操作与 CAS ​

【题目】多线程环境下保证 i++ 原子性的最佳方案是?

A. 使用 volatile 修饰 int i B. 使用普通 int C. 使用 AtomicInteger(基于 CAS 实现) D. 使用 ThreadLocal

答案:C

【考点】volatile 不保证原子性;CAS 无锁原子操作。

【结论】选 C。volatile 不保证原子性,AtomicInteger 基于 CAS 实现无锁原子操作,是多线程下 i++ 的最佳方案。

【逐项辨析】

  • A错:volatile 仅保证可见性和有序性,i++ 的读-改-写三步仍可能被多线程交错执行,导致数据丢失。
  • C正确:AtomicInteger 的 incrementAndGet() 基于 CAS(Compare And Swap)在循环中尝试原子更新,无锁、无阻塞,保证 i++ 原子性。
  • B错:普通 int 无任何同步机制,多线程 i++ 必然不安全。
  • D错:ThreadLocal 为每个线程提供独立副本,线程间互不共享,与共享计数无关。

【知识点】 CAS(Compare And Swap)是 CPU 提供的原子指令,语义为:先比较内存值是否与预期值相等,若相等则写入新值,整个过程原子不可中断。Java 通过 Unsafe 类封装 CAS 操作,AtomicInteger、AtomicLong、AtomicReference 等原子类均基于此实现。

AtomicInteger.incrementAndGet() 的底层逻辑:

  1. 读取当前值 oldValue。
  2. 计算 newValue = oldValue + 1。
  3. 调用 CAS(oldValue, newValue)。
  4. 若 CAS 成功则返回 newValue;若失败(其他线程已修改),则重新读取并重复步骤 2-3(自旋)。

CAS 的优点是无锁、无线程切换开销;缺点是极端竞争下自旋消耗 CPU(乐观锁的 ABA 问题可通过 AtomicStampedReference 解决)。

【记忆锚点】 「volatile 看不够,CAS 来上手;先比较再交换,自旋不切换,原子又高效。」

【易混对比】 原子操作方案对比:

方案机制是否有锁性能适用场景
volatile可见性+禁重排无高状态标志、单例 DCL
synchronized监视器锁有中复合操作临界区
AtomicInteger(CAS)乐观锁自旋无高简单原子计数、累加
ThreadLocal线程隔离无高线程私有变量,非共享场景

换问法:CAS 的 ABA 问题是什么?——线程 A 读取值为 1,线程 B 改为 2 又改回 1,A 的 CAS 仍成功,但中间状态已变化。可用版本号(AtomicStampedReference)解决。

【自测】 以下代码能否保证最终 count = 10000?

java
AtomicInteger count = new AtomicInteger(0);
// 10 个线程各执行 1000 次 count.incrementAndGet()

答:能保证。incrementAndGet() 基于 CAS 原子更新,无论多少线程并发,最终结果一定是 10000。与第 J60 题连考。

【知识关联】

  • 同库关联:与 J53/J54、J42(CHM 用 CAS)、J70;ABA 问题与版本戳。
  • 实现层:Unsafe/VarHandle compareAndSet;自旋+失败策略;LongAdder 高并发计数优化。
  • 面试追问:① CAS 的 ABA 与解决?② 为何高并发计数用 LongAdder?

【拓展延伸】

  • 变式问法:AtomicInteger 底层;乐观锁与数据库 version 对照。
  • 版本差异:JDK8 LongAdder;JDK9 VarHandle 替代部分 Unsafe。
  • 工程注意点:短临界区用原子类;长逻辑用锁;注意 CAS 自旋耗 CPU。

J61 · 知识点:Callable 与 FutureTask ​

【题目】FutureTask 的主要作用是?

A. 包装 Callable 任务,通过 get() 阻塞获取执行结果,并支持取消 B. 定时执行任务 C. 创建线程池 D. 替代 synchronized

答案:A

【考点】Callable/Future/FutureTask 的异步结果获取机制。

【结论】选 A。FutureTask 包装 Callable 任务,同时实现 Runnable 和 Future 接口,可提交给线程池执行,并通过 get() 阻塞获取结果。

【逐项辨析】

  • A 正确:FutureTask 是 RunnableFuture 的实现类,可包装 Callable,交给 Thread 或线程池执行;get() 阻塞等待结果,cancel() 支持取消任务。
  • B 错:定时执行任务应使用 ScheduledExecutorService,不是 FutureTask。
  • C 错:创建线程池应使用 Executors 工厂方法或 new ThreadPoolExecutor()。
  • D 错:FutureTask 与 synchronized 无关,它是异步任务的结果载体。

【知识点】 Runnable 接口的 run() 无返回值且不能抛受检异常,限制了异步任务的表达能力。Callable<V> 接口的 call() 可返回 V 类型结果,并可抛出异常。但 Thread 只接受 Runnable,因此需要 FutureTask 作为适配器:

FutureTask<V> 实现了 RunnableFuture<V>(Runnable + Future),构造时传入 Callable<V>。执行流程:

  1. 创建 Callable 实现类,重写 call()。
  2. 用 Callable 实例构造 FutureTask。
  3. FutureTask 交给 Thread.start() 或 ExecutorService.submit() 执行。
  4. 调用方通过 futureTask.get() 阻塞获取结果(带超时重载 get(timeout, unit))。
  5. 可通过 futureTask.cancel(mayInterruptIfRunning) 取消未执行或正在执行的任务。

ExecutorService.submit(Callable) 内部也会将 Callable 包装为 FutureTask(或其同类)返回 Future 对象。

【记忆锚点】 「Runnable 没结果,Callable 有结果;FutureTask 是快递盒,装 Callable 交给线程跑,get() 开箱取货。」

【易混对比】 Runnable、Callable、Future、FutureTask 的关系:

接口/类方法返回值异常说明
Runnablerun()void不能抛受检异常任务抽象
Callable<V>call()V可以抛异常有返回值的任务
Future<V>get() / cancel()VExecutionException异步结果视图
FutureTask<V>run() + get()V封装异常Runnable + Future 的结合体

换问法:Future.get() 抛出什么异常?——若任务执行中抛异常,get() 会抛 ExecutionException,其 cause 为实际异常。

【自测】 以下代码输出什么?

java
Callable<Integer> c = () -> { throw new RuntimeException("err"); };
FutureTask<Integer> ft = new FutureTask<>(c);
new Thread(ft).start();
try { ft.get(); } catch (Exception e) { System.out.print(e.getClass().getSimpleName()); }

答:输出 ExecutionException。call() 抛出的 RuntimeException 被封装为 ExecutionException,通过 get() 抛出。与第 J61 题连考。

【知识关联】

  • 同库关联:与 J51、J58;Future.get 阻塞与超时。
  • 实现层:Callable.call 可抛异常并返回值;FutureTask 同时是 RunnableFuture;结果存内存可见。
  • 面试追问:① 如何取消任务?② CompletableFuture 优势?

【拓展延伸】

  • 变式问法:线程池 submit(Callable) 返回什么。
  • 版本差异:JDK8 CompletableFuture;JDK9 改进。
  • 工程注意点:get 必须带超时;异常要 ExecutionException 解包;编排用 CF。

J62 · 知识点:死锁条件 ​

【题目】产生死锁的必要条件不包括?

A. 互斥条件 B. 请求与保持条件 C. 不可剥夺条件 D. 公平调度条件

答案:D

【考点】死锁的四个必要条件与预防思路。

【结论】选 D。死锁四个必要条件为互斥、请求与保持、不可剥夺、循环等待;公平调度不是死锁条件。

【逐项辨析】

  • A错:互斥条件是死锁必要条件之一,资源一次只能被一个线程占用。
  • B错:请求与保持条件(占有且等待)是死锁必要条件之一。
  • C错:不可剥夺条件是死锁必要条件之一,已获资源不能被强制释放。
  • D正确:公平调度是锁或调度器的属性,与死锁产生无关。

【知识点】 死锁(Deadlock)是指多个线程因竞争资源而互相等待,且这种等待无法自行解除的现象。产生死锁必须同时满足四个必要条件:

  1. 互斥条件:资源一次只能被一个线程占用。
  2. 请求与保持条件:线程持有至少一个资源,同时又在请求新的资源。
  3. 不可剥夺条件:线程已获得的资源在未使用完之前不能被其他线程强行剥夺。
  4. 循环等待条件:存在一个线程-资源的等待环路,如 T1 等 T2 的资源,T2 等 T3 的资源,T3 等 T1 的资源。

破坏任一条件即可预防死锁:

  • 破坏互斥:某些资源可共享(如只读文件),但多数资源 inherently 互斥。
  • 破坏请求与保持:一次性申请所有资源,或申请前释放已持有资源。
  • 破坏不可剥夺:使用 tryLock 超时机制,拿不到就释放已有资源重试。
  • 破坏循环等待:按全局统一顺序加锁(如先锁 A 再锁 B)。

【记忆锚点】 「死锁四要件:互斥、占有等、不可抢、成环圈;公平调度是外人,不在此列。」

【易混对比】 死锁、活锁、饥饿的区别:

现象特征线程状态能否自行恢复
死锁互相等待,循环阻塞BLOCKED/WAITING不能(需外部干预)
活锁互相改变状态以响应对方,但无进展RUNNABLE可能(看设计)
饥饿某些线程长期得不到资源RUNNABLE/BLOCKED可能(公平调度可缓解)

换问法:按固定顺序加锁破坏的是哪个条件?——循环等待条件。

【自测】 以下代码是否可能产生死锁?

java
Object a = new Object(), b = new Object();
Thread t1 = new Thread(() -> {
    synchronized (a) { synchronized (b) { System.out.print("A"); } }
});
Thread t2 = new Thread(() -> {
    synchronized (b) { synchronized (a) { System.out.print("B"); } }
});
t1.start(); t2.start();

答:可能死锁。t1 持 a 等 b,t2 持 b 等 a,形成循环等待。解决:按固定顺序(如先 a 后 b)对两个线程加锁。与第 J62 题连考。

【知识关联】

  • 同库关联:与 J53(锁顺序)、J56;与数据库死锁排查方法论类似。
  • 实现层:四条件:互斥、占有并等待、不可剥夺、循环等待;破坏任一即可预防。
  • 面试追问:① 如何用 jstack 发现死锁?② 超时锁如何破局?

【拓展延伸】

  • 变式问法:两线程交叉加锁代码;lockInterruptibly。
  • 版本差异:语义稳定。
  • 工程注意点:全局统一加锁顺序;用 tryLock 超时;缩小锁范围;避免嵌套锁。

持续学习,持续积累。