目录

03-Java并发

发表于
4 223.8~287.8 分钟 100717

3.1 线程基础(上)


进程和线程的区别是什么?什么是协程?

原始问法:

  • 进程和线程的区别是什么?
  • 什么是协程?和线程的区别是什么?

来源题目:SRC-03-31-091, SRC-03-31-106

面试先答

进程是操作系统分配资源的基本单位,线程是 CPU 调度的基本单位。一个进程可以包含多个线程,它们共享进程的内存空间和资源,但每个线程有独立的程序计数器和栈。协程(Coroutine)是用户态的轻量级线程,由应用程序而非操作系统调度,切换成本极低。总结来说:进程间相互隔离、开销大;线程间共享内存、开销中等;协程在用户态、可实现高并发。Java 中主流还是使用线程,虚拟线程(Project Loom)在 JDK 21 中正式引入,本质上就是协程的一种实现。

核心结论
  • 进程是资源分配最小单位,线程是 CPU 调度最小单位
  • 同一进程内的线程共享堆和方法区,各自独立栈和程序计数器
  • 协程是用户态调度,不需要内核介入,可支撑百万级并发
1. 是什么

进程(Process):操作系统创建的资源容器,包含虚拟地址空间、文件描述符表、进程控制块(PCB)等。

线程(Thread):CPU 调度的执行流,同一进程内的线程共享地址空间,但每个线程拥有独立的:

  • 程序计数器(PC)
  • 虚拟机栈
  • 本地变量表
  • 线程状态

协程(Coroutine):由编译器或运行时在用户态实现的轻量级并发单元,不依赖操作系统线程调度。

2. 为什么需要它
  • 进程:需要隔离不同程序的运行环境,防止一个程序崩溃影响其他程序
  • 线程:需要在同一进程内并发执行多个任务,避免进程间通信的高开销
  • 协程:需要在高并发场景下(如百万级 I/O)避免线程切换的高开销
3. 底层原理与完整流程

进程创建流程:操作系统分配 PCB → 分配虚拟地址空间 → 加载可执行文件 → 创建初始线程

线程创建流程:JVM 在 HotSpot 中通过 os::create_thread 调用操作系统 API 创建线程 → 分配 Thread 对象 → 关联 OS 线程 → 加入线程组

协程实现原理

  • 编译器将协程编译为状态机
  • 通过保存/恢复寄存器上下文实现挂起和恢复
  • 无需内核态切换,用户态即可完成
4. 怎么使用
// Java 中使用线程
Thread thread = new Thread(() -> {
    System.out.println("Hello from thread");
});
thread.start();

// JDK 21 虚拟线程(协程)
Thread vt = Thread.ofVirtual()
    .name("vt-1")
    .start(() -> {
        System.out.println("Hello from virtual thread");
    });
5. 适用场景
  • 进程:隔离性要求高的任务,如浏览器多标签页、微服务
  • 线程:CPU 密集型和中等并发的 I/O 密集型任务
  • 协程:高并发 I/O 场景,如网关、连接池、爬虫
6. 不适用场景与替代方案
  • 不要用进程做细粒度并发,开销太大
  • 不要用协程做 CPU 密集型任务,无法利用多核优势
  • 协程的替代方案:Reactor 模式(Netty)、异步回调
7. 优缺点与技术取舍
维度 进程 线程 协程
切换开销 高(内核态) 中(内核态) 低(用户态)
数量上限 数百 数千 数百万
数据共享 需 IPC 直接共享 直接共享
稳定性 高隔离 一个线程崩溃可能影响整个进程 取决于实现
8. 常见问题及解决方案
  • 问题:线程数过多导致频繁切换

    • 解决:使用线程池控制数量,或使用虚拟线程
  • 问题:协程调试困难

    • 解决:使用 IDEA 的协程调试器,或通过日志跟踪
9. 版本差异与实现边界
  • Java 8-20:仅支持平台线程(Platform Thread),每个 Java 线程映射到一个 OS 线程
  • JDK 21:正式引入虚拟线程(Virtual Thread),由 ForkJoinPool 调度
  • 协程不等于虚拟线程:虚拟线程是 JVM 层实现的协程,Kotlin 协程是语言层面实现
10. 常见追问
  • 追问 1:虚拟线程会取代平台线程吗?

    • 回答方向:不会,CPU 密集型任务仍需平台线程;虚拟线程主要解决 I/O 密集型高并发。
  • 追问 2:同一进程内线程共享哪些资源?

    • 回答方向:共享堆、方法区、常量池、全局变量、文件句柄;不共享栈、程序计数器。
11. 易错点
  • ❌ 错误:线程是资源分配的基本单位

  • ✅ 正确:进程是资源分配的基本单位,线程是 CPU 调度的基本单位

  • ❌ 错误:协程一定比线程快

  • ✅ 正确:CPU 密集型场景下协程没有优势,甚至可能因额外开销更慢

一句话总结

进程是资源容器、线程是执行流、协程是用户态轻量执行单元,三者分别解决隔离、并发和高并发问题。


进程间的通信方式有哪些?

原始问法:

  • 进程间的通信方式有哪些?

来源题目:SRC-03-31-092

面试先答

进程间通信(IPC)主要有六种方式:管道、消息队列、共享内存、信号量、信号和套接字。管道适合有亲缘关系的进程;消息队列适合低耦合异步通信;共享内存最快但需额外同步;信号量用于同步;信号用于通知;套接字可用于不同机器间的进程通信。Java 中常用的 IPC 包括:通过 Socket 进行网络通信、通过文件进行持久化通信、通过 Java 的 ProcessHandle 与子进程交互。

核心结论
  • IPC 方式各有特点,选择取决于场景
  • 共享内存是最快的 IPC 方式,但需要配合同步机制
  • 套接字是最通用的 IPC,可跨主机
1. 是什么

IPC(Inter-Process Communication):操作系统提供的进程间数据交换机制。

主要 IPC 方式:

方式 特点 适用场景
管道(Pipe) 半双工、先进先出、有亲缘关系 父子进程通信
命名管道(Named Pipe) 可无亲缘关系 本机进程通信
消息队列 异步、按类型读取 低耦合异步通信
共享内存 最快、需同步 大量数据交换
信号量 纯同步机制 进程间同步
信号(Signal) 异步通知 异常处理、通知
套接字(Socket) 通用、可跨网络 网络通信
2. 为什么需要它

进程地址空间相互隔离,无法直接读写对方内存,需要操作系统提供的 IPC 机制实现数据交换和协同工作。

3. 底层原理与完整流程

以共享内存为例:

  1. 进程 A 调用 shmget 获取共享内存段
  2. 调用 shmat 将内存段映射到自身地址空间
  3. 写入数据后通过信号量通知进程 B
  4. 进程 B 映射同一段内存,读取数据
  5. 使用完毕调用 shmdtshmctl 释放
4. 怎么使用
// Java 中通过 Socket 实现 IPC
try (ServerSocket server = new ServerSocket(8080)) {
    Socket client = server.accept();
    // 读写数据
}

// 通过 Process 与子进程通信
ProcessBuilder pb = new ProcessBuilder("cmd", "/c", "echo", "hello");
Process process = pb.start();
5. 适用场景
  • 管道:Shell 命令管道(ls | grep java
  • 消息队列:分布式系统中异步任务分发
  • 共享内存:高性能计算、实时系统
  • 套接字:微服务通信、分布式系统
6. 不适用场景与替代方案
  • 不要用共享内存跨机器通信,改用套接字
  • 不要用信号传递复杂数据,改用消息队列
  • 单机多进程通信可考虑用 ZeroMQ 抽象层
7. 优缺点与技术取舍

共享内存速度最快但需要额外同步;套接字通用性最强但有网络开销;消息队列解耦但有延迟。选择时需在性能、复杂度和可靠性之间权衡。

8. 常见问题及解决方案
  • 问题:共享内存段泄漏
    • 解决:确保所有进程退出后正确调用 shmctl 释放,或使用 ipcrm 清理
9. 版本差异与实现边界
  • Windows 和 Linux 的 IPC 实现不同
  • Java 的 NIO Pipe 是 JVM 层实现,非 OS 级 IPC
  • 跨平台 IPC 推荐使用 Socket 或 ZeroMQ
10. 常见追问
  • 追问:共享内存为什么最快?
    • 回答方向:因为多个进程映射同一块物理内存,无需数据拷贝。
11. 易错点
  • ❌ 错误:共享内存不需要同步
  • ✅ 正确:多个进程同时访问共享内存需要额外的同步机制(如信号量)
一句话总结

IPC 六大方式各有所长,共享内存最快、套接字最通用、管道最简洁,根据场景选择合适的通信机制。


线程间的通信方式有哪些?

原始问法:

  • 线程间的通信方式有哪些?

来源题目:SRC-03-31-093

面试先答

Java 线程间通信主要有五种方式:volatile 关键字、wait/notify 机制、LockConditionCountDownLatch 等同步工具、以及管道流。volatile 保证可见性但不保证原子性;wait/notify 基于对象监视器实现线程间通知;Lock + Condition 是更灵活的替代方案;同步工具类提供了更高层的同步语义;管道流用于字节流通信。实际开发中最常用的是 wait/notifyLock/Condition

核心结论
  • volatile 解决可见性,wait/notify 解决线程调度
  • Lock/Conditionwait/notify 的升级,支持多条件队列
  • 同步工具类(Latch、Barrier、Semaphore)提供高层同步原语
1. 是什么

线程间通信方式:

方式 机制 特点
volatile 内存可见性 轻量,仅保证可见性和有序性
wait/notify Object 监视器 需在同步块内使用,一次性唤醒
Lock/Condition AQS 实现 可中断、公平、多条件
CountDownLatch AQS 实现 一次性等待,不可重置
CyclicBarrier ReentrantLock 可重置的循环屏障
Semaphore AQS 实现 控制并发数量
管道流 PipedInputStream/OutputStream 字节流方式通信
2. 为什么需要它

多个线程协作时需要协调执行顺序和数据交换。例如生产者-消费者模型中,生产者需要通知消费者数据已就绪。

3. 底层原理与完整流程

wait/notify 机制

  1. 线程 A 获取对象锁
  2. 调用 wait() 释放锁并进入等待队列
  3. 线程 B 获取同一对象锁
  4. 调用 notify() 唤醒等待队列中的一个线程
  5. 线程 B 释放锁后,线程 A 重新竞争锁并从 wait() 返回
4. 怎么使用
// wait/notify 示例
Object lock = new Object();

// 生产者
synchronized (lock) {
    data.produce();
    lock.notify(); // 唤醒消费者
}

// 消费者
synchronized (lock) {
    while (data.isEmpty()) {
        lock.wait(); // 等待数据
    }
    data.consume();
}

// CountDownLatch 示例
CountDownLatch latch = new CountDownLatch(3);

// 等待 3 个任务完成
for (int i = 0; i < 3; i++) {
    new Thread(() -> {
        doWork();
        latch.countDown();
    }).start();
}
latch.await(); // 等待所有任务完成
5. 适用场景
  • volatile:状态标志位(如 running = false
  • wait/notify:简单的线程间通知
  • CountDownLatch:一个线程等待多个任务完成
  • CyclicBarrier:多个线程相互等待
  • Semaphore:限流场景
6. 不适用场景与替代方案
  • 不要用 volatile 做复合操作(如 count++
  • 不要用 CountDownLatch 做循环同步,改用 CyclicBarrier
  • 不要在持有锁的情况下调用 Thread.sleep,改用 Condition.await
7. 优缺点与技术取舍

wait/notify 使用简单但存在信号丢失和虚假唤醒问题;Lock/Condition 更灵活但代码更复杂;同步工具类封装度高但功能有限。

8. 常见问题及解决方案
  • 问题notify() 早于 wait() 调用导致信号丢失

    • 解决:在调用 wait() 前检查条件是否满足,使用 while 循环而非 if
  • 问题:虚假唤醒

    • 解决:始终在 while 循环中检查条件
9. 版本差异与实现边界
  • wait/notify 从 Java 1.0 就存在
  • java.util.concurrent 包从 Java 5 引入
  • Condition 接口与 Lock 搭配使用,提供更精细的线程控制
10. 常见追问
  • 追问:为什么 wait() 必须在同步块内调用?
    • 回答方向:防止信号丢失,保证检查条件和等待的原子性。
11. 易错点
  • ❌ 错误:wait() 会释放锁,sleep() 不会

  • ✅ 正确:这是两者的核心区别之一

  • ❌ 错误:notifyAll()notify()

  • ✅ 正确:根据场景选择,notifyAll() 可能导致性能问题

一句话总结

线程间通信有多种机制,wait/notify 基于对象监视器,Lock/Condition 更灵活,同步工具类提供高层封装,选择取决于场景需求。


线程之间哪些内存是共享的?

原始问法:

  • 线程之间哪些内存是共享的?

来源题目:SRC-03-31-094

面试先答

在 Java 中,同一进程的所有线程共享堆内存和方法区(元空间),包括对象实例、类信息、常量池、静态变量等。每个线程独立拥有程序计数器、虚拟机栈和本地方法栈。堆是线程间通信的主要内存区域,但必须通过同步机制保证线程安全。理解这个划分是掌握 Java 并发编程的基础。

核心结论
  • 共享区域:堆、方法区(元空间)
  • 线程私有:程序计数器、虚拟机栈、本地方法栈
  • 堆是线程安全问题的主要发生区域
1. 是什么

JVM 运行时数据区域划分:

线程共享区域

  • 堆(Heap):存放对象实例和数组
  • 方法区/元空间(Metaspace):存放类信息、常量、静态变量、JIT 编译后的代码

线程私有区域

  • 程序计数器(PC Register):记录当前线程执行的字节码行号
  • 虚拟机栈(VM Stack):存储局部变量表、操作数栈、动态链接、方法出口
  • 本地方法栈(Native Method Stack):为 Native 方法服务
2. 为什么需要它
  • 共享内存使线程间数据交换成为可能
  • 线程私有区域保证线程执行的独立性
  • 合理的内存划分是实现高效并发的基础
3. 底层原理与完整流程

线程启动时:

  1. JVM 为新线程分配程序计数器(CPU 寄存器级别)
  2. 分配虚拟机栈空间(每个方法调用对应一个栈帧)
  3. 新线程与主线程共享堆和方法区
  4. 新线程通过引用访问堆中的共享对象
4. 怎么使用
public class SharedMemoryDemo {
    private static int sharedCount = 0; // 方法区,线程共享
    private int instanceCount = 0;      // 堆,线程共享

    public void method() {
        int localCount = 0; // 栈,线程私有
        // ...
    }
}
5. 适用场景
  • 共享内存用于线程间数据交换:生产者-消费者、计数器等
  • 线程私有内存用于保存执行上下文:方法调用、局部变量
6. 不适用场景与替代方案
  • 不要在没有同步的情况下读写共享内存
  • 不要将临时数据存放在共享区域
  • 需要线程隔离时使用 ThreadLocal
7. 优缺点与技术取舍

共享内存高效但需同步机制保障;线程私有内存安全但无法直接共享。通过 volatilesynchronizedLock 等机制协调两者。

8. 常见问题及解决方案
  • 问题:多线程修改共享变量导致数据竞争

    • 解决:使用 synchronizedLock 或原子类
  • 问题:线程私有内存泄漏

    • 解决:使用 ThreadLocal.remove() 清理
9. 版本差异与实现边界
  • Java 8:方法区永久代,可能 OOM
  • Java 8+:使用元空间(Metaspace),使用本地内存
  • 堆内存可通过 -Xmx-Xms 参数配置
10. 常见追问
  • 追问ThreadLocal 是如何实现线程隔离的?
    • 回答方向:每个 Thread 对象内部有一个 ThreadLocalMap,以 ThreadLocal 为 key 存储值。
11. 易错点
  • ❌ 错误:静态变量是线程安全的

  • ✅ 正确:静态变量存储在方法区,所有线程共享,不是线程安全的

  • ❌ 错误:局部变量是共享的

  • ✅ 正确:局部变量存储在栈帧中,是线程私有的

一句话总结

堆和方法区线程共享,程序计数器和栈线程私有,共享内存是并发编程的基础但需要同步机制保障安全。


创建线程的方式有哪些?本质上的实现方式是什么?

原始问法:

  • 创建线程的方式有哪些?本质上的实现方式是什么?

来源题目:SRC-03-31-095

面试先答

Java 中创建线程主要有四种方式:继承 Thread 类、实现 Runnable 接口、实现 Callable 接口配合 Future、使用线程池。从本质上讲,前三种最终都需要创建 Thread 对象并调用 start() 方法,第四种是复用已有线程。Thread.start() 内部调用 JVM 的 native 方法 start0(),最终通过操作系统 API 创建内核级线程。JDK 21 还引入了虚拟线程,创建方式为 Thread.ofVirtual()

核心结论
  • 创建线程的四种方式:继承 Thread、实现 Runnable、实现 Callable+Future、使用线程池
  • 本质都是调用 Thread.start()start0() → OS 创建线程
  • 线程池是生产环境首选方式
1. 是什么

创建线程的方式:

方式一:继承 Thread

public class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("Thread running");
    }
}
new MyThread().start();

方式二:实现 Runnable

public class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("Runnable running");
    }
}
new Thread(new MyRunnable()).start();

方式三:实现 Callable + Future

Callable<String> callable = () -> "Callable result";
FutureTask<String> futureTask = new FutureTask<>(callable);
new Thread(futureTask).start();
String result = futureTask.get(); // 阻塞获取结果

方式四:使用线程池

ExecutorService executor = Executors.newFixedThreadPool(5);
executor.submit(() -> {
    System.out.println("Task running in pool");
});
2. 为什么需要它
  • Runnable/Callable 实现解耦,线程逻辑与线程创建分离
  • Callable 支持返回值和异常处理
  • 线程池避免频繁创建销毁线程的开销
3. 底层原理与完整流程

Thread.start() 调用流程:

  1. Thread.start()start0()(native 方法)
  2. JVM 调用 Thread::create_thread
  3. HotSpot 中 os::create_thread 调用 OS API(Windows CreateThread,Linux pthread_create
  4. OS 创建内核线程并关联 JVM Thread 对象
  5. 内核线程启动后调用 Thread::run 执行用户逻辑
4. 怎么使用

见上面代码示例。推荐使用实现 RunnableCallable 的方式,因为:

  • 避免单继承限制
  • 便于线程池管理
  • 业务代码与线程逻辑分离
5. 适用场景
  • 简单一次性任务:继承 Thread 或实现 Runnable
  • 需要返回结果:使用 Callable + Future
  • 生产环境:使用线程池
  • JDK 21+ 高并发 I/O:使用虚拟线程
6. 不适用场景与替代方案
  • 不要频繁创建销毁线程,改用线程池
  • 不要在业务代码中直接 new Thread(),统一使用线程池
  • 不要忽略 CallerRunsPolicy 拒绝策略下的任务执行
7. 优缺点与技术取舍
方式 优点 缺点
继承 Thread 简单直接 单继承限制,不可复用
实现 Runnable 解耦,可复用 无返回值
Callable 有返回值,可抛异常 代码复杂
线程池 可复用,可控 需合理配置参数
8. 常见问题及解决方案
  • 问题Thread.start() 调用了 run() 但没有新线程

    • 解决:确认调用的是 start() 而非直接调用 run()
  • 问题:Callable 任务无法取消

    • 解决:使用 Future.cancel(true) 中断正在执行的任务
9. 版本差异与实现边界
  • Java 5:引入 ExecutorCallableFuture
  • Java 7:引入 ForkJoinPool
  • JDK 21:引入虚拟线程 Thread.ofVirtual()
10. 常见追问
  • 追问Thread.start()Thread.run() 的区别?
    • 回答方向:start() 创建新线程执行,run() 在当前线程同步执行。
11. 易错点
  • ❌ 错误:调用 run() 启动线程

  • ✅ 正确:必须调用 start() 才能启动新线程

  • ❌ 错误:Callable 可以直接 new Thread 启动

  • ✅ 正确:需要包装为 FutureTask 才能传给 Thread

一句话总结

创建线程有四种方式,本质都是通过 Thread.start()start0() 调用 OS 创建内核线程,生产环境首选线程池。


继承 Thread 类和实现 Runnable 接口的核心区别是什么?

原始问法:

  • 继承 Thread 类和实现 Runnable 接口的核心区别是什么?

来源题目:SRC-03-31-096

面试先答

核心区别在于:继承 Thread 是强耦合,实现 Runnable 是松耦合。继承 Thread 后无法再继承其他类,且每次只能创建新的线程实例;实现 Runnable 可以继承其他类、可以被线程池复用、多个线程可以共享同一个 Runnable 实例。两者底层实现相同——Thread 类内部也有一个 Runnable 字段,run() 方法会调用该字段的 run() 方法。推荐始终使用 Runnable 或 Callable

核心结论
  • 继承 Thread 是耦合设计,实现 Runnable 是解耦设计
  • Runnable 可以被多个线程共享,Thread 不行
  • Runnable 可以被线程池复用,Thread 不能
  • 推荐始终使用实现 Runnable/Callable 的方式
1. 是什么

继承 Thread

public class MyThread extends Thread {
    @Override
    public void run() {
        // 线程逻辑
    }
}

实现 Runnable

public class MyRunnable implements Runnable {
    @Override
    public void run() {
        // 线程逻辑
    }
}
2. 为什么需要它
  • 继承 Thread 后,线程逻辑和线程生命周期耦合在一起
  • 实现 Runnable 可以将业务逻辑与线程管理分离
  • Runnable 实例可以被多线程共享,也可以被线程池复用
3. 底层原理与完整流程

Thread 类内部:

public class Thread implements Runnable {
    private Runnable target; // 目标 Runnable

    @Override
    public void run() {
        if (target != null) {
            target.run(); // 调用外部 Runnable
        }
    }
}

当继承 Thread 时,target 为 null,直接重写 run()。 当实现 Runnable 时,target 不为 null,Thread 的 run() 转发调用。

4. 怎么使用

推荐使用实现 Runnable 的方式:

// 可复用的业务逻辑
Runnable task = () -> System.out.println("Business logic");

// 被多个线程共享
new Thread(task).start();
new Thread(task).start();

// 被线程池复用
executor.execute(task);
5. 适用场景
  • 简单临时任务:两种方式均可
  • 需要复用、需要继承其他类、需要线程池:必须使用 Runnable
  • 需要返回值:使用 Callable
6. 不适用场景与替代方案
  • 不要在生产代码中直接 new Thread(),使用线程池
  • 不要用继承 Thread 的方式写业务代码,限制扩展性
  • 替代方案:Lambda 表达式、方法引用
7. 优缺点与技术取舍
维度 继承 Thread 实现 Runnable
代码复杂度 简单 稍复杂
单继承限制 受限 不受限
可复用性 不可复用 可复用
线程池支持 不支持 支持
解耦程度 耦合 解耦
8. 常见问题及解决方案
  • 问题:匿名内部类中 this 指针混淆
    • 继承 Thread 时 this 指向线程对象
    • 实现 Runnable 时 this 指向业务对象
    • 解决:使用 Thread.currentThread() 获取当前线程
9. 版本差异与实现边界
  • Java 8+:可以使用 Lambda 简化 Runnable 创建
  • Thread 类也实现了 Runnable 接口
10. 常见追问
  • 追问:为什么推荐使用 Runnable?
    • 回答方向:解耦、可复用、可被线程池管理、不受单继承限制。
11. 易错点
  • ❌ 错误:继承 Thread 的方式性能更好

  • ✅ 正确:两者底层完全相同,性能无差异

  • ❌ 错误:Runnable 不能直接用 new Thread()

  • ✅ 正确:new Thread(new MyRunnable()).start() 是标准用法

一句话总结

继承 Thread 是耦合的简单实现,实现 Runnable 是解耦的推荐方式,生产环境应始终选择 Runnable/Callable。


Runnable 和 Callable 的区别是什么?

原始问法:

  • Runnable 和 Callable 的区别是什么?

来源题目:SRC-03-31-097

面试先答

Runnable 和 Callable 的核心区别有三点:第一,Runnable 的 run() 方法没有返回值,Callable 的 call() 方法有返回值;第二,Runnable 的 run() 方法不能抛出受检查异常,Callable 的 call() 方法可以;第三,Callable 需要配合 Future 使用才能获取结果。两者都是函数式接口,在 Java 8 中都可以用 Lambda 创建。Runnable 自 Java 1.0 就存在,Callable 从 Java 5 引入。

核心结论
  • Runnable.run() 无返回值,Callable.call() 有返回值
  • Callable 可以抛出受检查异常
  • Callable 需配合 Future 获取结果
1. 是什么
// Runnable - Java 1.0
@FunctionalInterface
public interface Runnable {
    void run(); // 无返回值,不可抛受检异常
}

// Callable - Java 5
@FunctionalInterface
public interface Callable<V> {
    V call() throws Exception; // 有返回值,可抛受检异常
}
2. 为什么需要它
  • Runnable 无法获取线程执行结果
  • Runnable 无法抛出受检查异常(必须 try-catch)
  • Callable 解决了这两个痛点,使线程可以像方法一样有返回值
3. 底层原理与完整流程

Callable 执行流程:

  1. Callable 包装为 FutureTask(同时实现 RunnableFuture
  2. FutureTask 传给 ThreadExecutorService
  3. 线程执行 FutureTask.run() → 调用 Callable.call()
  4. 结果存储在 FutureTaskoutcome 字段中
  5. 其他线程通过 Future.get() 获取结果(阻塞等待)
4. 怎么使用
// Callable 配合 FutureTask
Callable<String> task = () -> {
    // 可抛受检异常
    return computeResult();
};

FutureTask<String> futureTask = new FutureTask<>(task);
new Thread(futureTask).start();

// 获取结果(阻塞)
String result = futureTask.get();

// 使用线程池
ExecutorService executor = Executors.newCachedThreadPool();
Future<String> future = executor.submit(task);
String result = future.get();
5. 适用场景
  • 需要返回计算结果的异步任务:使用 Callable
  • 简单无返回值的任务:使用 Runnable
  • 需要抛出异常的任务:必须使用 Callable
6. 不适用场景与替代方案
  • 不要在 Callable.call() 中执行无限循环
  • 不要在主线程中立即调用 future.get(),会变成同步
  • 需要链式调用时使用 CompletableFuture
7. 优缺点与技术取舍
维度 Runnable Callable
返回值 泛型返回值
异常 不可抛受检异常 可抛任意异常
使用复杂度 简单 需配合 Future
结果获取 回调或共享变量 Future.get()
8. 常见问题及解决方案
  • 问题future.get() 阻塞导致主线程卡死

    • 解决:使用带超时的 get(timeout, unit),或使用 CompletableFuture 异步回调
  • 问题:Callable 内部异常被 Future 吞掉

    • 解决:调用 future.get() 时异常会被包装为 ExecutionException 抛出
9. 版本差异与实现边界
  • Java 1.0:引入 Runnable
  • Java 5:引入 CallableFutureFutureTask
  • Java 8:CompletableFuture 提供更强大的异步能力
10. 常见追问
  • 追问FutureFutureTask 的区别?
    • 回答方向:Future 是接口,FutureTask 是实现类,同时实现了 Runnable
11. 易错点
  • ❌ 错误:Callable 可以直接 new Thread(callable).start()
  • ✅ 正确:需要包装为 FutureTasknew Thread(new FutureTask<>(callable))
一句话总结

Runnable 无返回值、不可抛受检异常;Callable 有返回值、可抛异常,需配合 Future 获取结果,是实现有返回值异步任务的标准方式。


线程的生命周期有哪些状态?状态之间如何切换?

原始问法:

  • 线程的生命周期有哪些状态?状态之间如何切换?

来源题目:SRC-03-31-098

面试先答

Java 线程有六种状态:NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED。NEW 是创建未启动;RUNNABLE 是正在执行或等待 CPU 调度;BLOCKED 是等待锁;WAITING 是无限期等待;TIMED_WAITING 是有限期等待;TERMINATED 是执行结束。状态转换路径为:NEW→RUNNABLE→(BLOCKED/WAITING/TIMED_WAITING)→RUNNABLE→TERMINATED。注意 Java 的 RUNNABLE 状态包含了操作系统的就绪和运行两种状态。

核心结论
  • 六种线程状态由 Thread.State 枚举定义
  • BLOCKED 等待锁、WAITING 无限等待、TIMED_WAITING 有限等待
  • 只有 RUNNABLE 状态能直接进入 TERMINATED
  • Java 的 RUNNABLE 对应 OS 的就绪态和运行态
1. 是什么

线程状态枚举(Thread.State):

public enum State {
    NEW,          // 新建,未启动
    RUNNABLE,     // 可运行(就绪+运行中)
    BLOCKED,      // 等待锁释放
    WAITING,      // 等待其他线程通知(无限期)
    TIMED_WAITING,// 有时间限制的等待
    TERMINATED    // 已终止
}
2. 为什么需要它

理解线程状态有助于:

  • 诊断死锁和线程阻塞问题
  • 分析线程池的使用情况
  • 优化并发程序性能
3. 底层原理与完整流程

状态转换图

NEW ──start()──→ RUNNABLE
                      │
         ┌────────────┼────────────┐
         │            │            │
    BLOCKED       WAITING      TIMED_WAITING
    (等锁)       (wait)      (sleep/await)
         │            │            │
         └─────锁释放/通知────────→ RUNNABLE
                      │
                   执行结束
                      │
                  TERMINATED

各状态进入条件

  • NEW:new Thread() 后、start()
  • RUNNABLE:start() 后,或从其他状态恢复
  • BLOCKED:等待 synchronized 锁或 ReentrantLock
  • WAITING:调用 Object.wait()Thread.join()Condition.await()
  • TIMED_WAITING:调用 Thread.sleep(timeout)Object.wait(timeout)Condition.await(timeout)Thread.join(timeout)
  • TERMINATED:run() 方法执行完毕或抛出未捕获异常
4. 怎么使用
Thread thread = new Thread(() -> {
    try {
        Thread.sleep(1000); // RUNNABLE → TIMED_WAITING
    } catch (InterruptedException e) {
        // 处理中断
    }
});

System.out.println(thread.getState()); // NEW
thread.start();
System.out.println(thread.getState()); // RUNNABLE

Thread.sleep(100);
System.out.println(thread.getState()); // TIMED_WAITING(可能)
5. 适用场景
  • 监控线程状态:通过 Thread.getState() 诊断线程问题
  • 调试死锁:通过 JStack 查看线程状态
  • 线程池调优:分析线程池中线程的状态分布
6. 不适用场景与替代方案
  • 不要依赖线程状态做业务逻辑判断
  • 不要用 stop() 强制终止线程(已废弃)
  • 需要线程间通知用 wait/notifyLock/Condition
7. 优缺点与技术取舍

线程状态是 JVM 层面的抽象,方便理解并发行为;但实际线程状态转换受 OS 调度影响,getState() 返回的值可能立即变化。

8. 常见问题及解决方案
  • 问题:线程长时间处于 BLOCKED 状态

    • 解决:使用 JStack 查看线程栈,分析锁竞争情况
  • 问题:线程处于 WAITING 状态无法恢复

    • 解决:检查是否有其他线程调用了 notify()notifyAll()
9. 版本差异与实现边界
  • 从 Java 1.0 就存在六种线程状态
  • JDK 21 中虚拟线程的状态可能更复杂(由 Carrier 线程映射)
  • Thread.getState() 返回的是 JVM 层的状态,不完全对应 OS 线程状态
10. 常见追问
  • 追问:BLOCKED 和 WAITING 的区别?
    • 回答方向:BLOCKED 等待锁(竞争式),WAITING 等待通知(协作式)。
11. 易错点
  • ❌ 错误:RUNNABLE 状态就是正在运行

  • ✅ 正确:RUNNABLE 包含就绪和运行两种状态,可能正在等待 CPU 调度

  • ❌ 错误:线程可以从 BLOCKED 直接到 TERMINATED

  • ✅ 正确:必须先经过 RUNNABLE 状态

一句话总结

Java 线程有六种状态,状态转换遵循固定路径,理解状态转换是诊断并发问题的基础。


线程 WAITING 状态和 TIMED_WAITING 状态的核心区别是什么?

原始问法:

  • 线程 WAITING 状态和 TIMED_WAITING 状态的核心区别是什么?

来源题目:SRC-03-31-099

面试先答

WAITING 和 TIMED_WAITING 的核心区别在于是否有超时机制。WAITING 是无限期等待,必须由其他线程显式调用 notify()notifyAll() 才能唤醒;TIMED_WAITING 是有超时的等待,除了被其他线程通知外,超时时间到达后会自动唤醒。两者进入的方法不同:WAITING 通过 wait()join()await()(无参)进入;TIMED_WAITING 通过 sleep(time)wait(time)join(time)await(time) 进入。

核心结论
  • WAITING 无超时,必须被通知;TIMED_WAITING 有超时,可自动恢复
  • WAITING 方法:wait()join()await()
  • TIMED_WAITING 方法:sleep(time)wait(time)join(time)await(time)
  • 两者都需要被其他线程从外部改变状态
1. 是什么

WAITING:无限期等待其他线程的通知(notify)。

TIMED_WAITING:有超时时间的等待,超时后自动返回。

2. 为什么需要它
  • WAITING 用于需要精确控制通知时机的场景
  • TIMED_WAITING 用于需要超时保护的场景,避免永久阻塞
3. 底层原理与完整流程

WAITING 状态转换

  1. 线程 A 获取对象锁
  2. 调用 wait() → 释放锁,进入 WAITING 状态,加入等待队列
  3. 线程 B 获取同一对象锁,调用 notify() → 线程 A 进入同步队列
  4. 线程 B 释放锁 → 线程 A 竞争锁,获取后从 wait() 返回

TIMED_WAITING 状态转换

  1. 与 WAITING 类似,但有超时定时器
  2. 超时时间到达后,即使没有 notify(),线程也会自动进入同步队列
4. 怎么使用
// WAITING 示例
synchronized (lock) {
    while (!condition) {
        lock.wait(); // 无限期等待
    }
}

// TIMED_WAITING 示例
synchronized (lock) {
    while (!condition) {
        lock.wait(1000); // 最多等待 1 秒
        // 超时后继续循环检查
    }
}

// sleep 进入 TIMED_WAITING
Thread.sleep(1000); // 不释放锁,进入 TIMED_WAITING
5. 适用场景
  • WAITING:条件明确需要外部通知的场景
  • TIMED_WAITING:需要超时保护的场景,如等待网络响应
6. 不适用场景与替代方案
  • 不要在 WAITING 中无限制等待,可能导致线程泄漏
  • 不要用 sleep() 替代 wait()sleep() 不释放锁
  • 替代方案:使用 CountDownLatchSemaphore 等同步工具
7. 优缺点与技术取舍
维度 WAITING TIMED_WAITING
超时机制
唤醒方式 必须被通知 通知或超时
锁释放 释放锁 wait/await 释放锁,sleep 不释放
风险 永久阻塞风险 超时自动恢复
8. 常见问题及解决方案
  • 问题:WAITING 线程永久阻塞

    • 解决:改用 TIMED_WAITING,或使用 try-catch 处理中断
  • 问题sleep() 期间持有锁

    • 解决:使用 wait()Condition.await() 替代
9. 版本差异与实现边界
  • 从 Java 1.0 就存在 WAITING 和 TIMED_WAITING 状态
  • Thread.sleep() 不释放锁,Object.wait() 释放锁
  • JDK 21 中虚拟线程的 sleep 实现有所不同
10. 常见追问
  • 追问wait()sleep() 的区别?
    • 回答方向:wait() 释放锁、需在同步块、可被中断;sleep() 不释放锁、无需同步、可被中断。
11. 易错点
  • ❌ 错误:WAITING 状态可以自动恢复

  • ✅ 正确:WAITING 必须被其他线程显式通知

  • ❌ 错误:sleep() 也会释放锁

  • ✅ 正确:sleep() 不释放锁,wait() 才释放锁

一句话总结

WAITING 无限期等待通知,TIMED_WAITING 有超时保护,核心区别在于是否需要外部通知才能恢复。


什么是线程上下文切换?触发场景有哪些?有什么性能影响?

原始问法:

  • 什么是线程上下文切换?触发场景有哪些?有什么性能影响?

来源题目:SRC-03-31-100

面试先答

线程上下文切换是指 CPU 从一个线程切换到另一个线程的过程,涉及保存当前线程的运行状态(程序计数器、寄存器等)和恢复目标线程的状态。触发场景包括:线程时间片用完、线程主动阻塞(I/O、锁等待)、高优先级线程抢占、操作系统中断等。上下文切换开销在微秒级别,频繁切换会显著降低 CPU 缓存命中率,成为性能瓶颈。生产环境应通过合理设置线程数量、使用协程等方式减少上下文切换。

核心结论
  • 上下文切换是保存/恢复线程执行状态的过程
  • 触发场景:时间片轮转、阻塞、抢占、中断
  • 切换开销:保存/恢复寄存器、更新内核调度数据、缓存失效、TLB 失效
  • 优化方向:减少线程数、使用协程、批量处理
1. 是什么

上下文切换(Context Switch):CPU 从当前线程切换到另一个线程时,需要保存当前线程的上下文(寄存器、栈指针、程序计数器等),并恢复目标线程的上下文。

2. 为什么需要它
  • CPU 核心数有限,需要在多个线程间共享
  • 操作系统通过时间片轮转实现"同时"执行多个线程的假象
  • 上下文切换是时间片轮转的必要代价
3. 底层原理与完整流程

上下文切换步骤

  1. 触发切换(时间片用尽/阻塞/中断)
  2. 保存当前 CPU 寄存器到当前线程的 Thread 结构
  3. 加载目标线程的 Thread 结构到 CPU 寄存器
  4. 更新内存管理单元(MMU)的页表(进程切换时)
  5. 刷新 CPU 缓存(部分场景)
  6. CPU 从新上下文恢复执行

开销来源

  • 内核态/用户态切换(涉及系统调用)
  • 缓存失效(L1/L2 Cache 被污染)
  • TLB 失效(地址翻译缓存失效)
  • 内存屏障
4. 怎么使用

上下文切换无法完全避免,但可以通过以下方式优化:

// 1. 使用线程池控制线程数量
int processors = Runtime.getRuntime().availableProcessors();
ExecutorService pool = Executors.newFixedThreadPool(processors * 2);

// 2. 使用 ForkJoinPool 实现工作窃取
ForkJoinPool fjp = new ForkJoinPool(processors);

// 3. JDK 21 使用虚拟线程(协程)
Thread.startVirtualThread(() -> {
    // 用户态切换,开销极低
});
5. 适用场景
  • 不可避免:操作系统层面的调度
  • 可优化:应用层面减少不必要的线程创建
  • 高并发场景:优先使用协程/虚拟线程
6. 不适用场景与替代方案
  • 不要创建远超过 CPU 核数的线程
  • 不要在循环中创建销毁线程
  • 替代方案:使用协程、Reactor 模式、异步 I/O
7. 优缺点与技术取舍
维度 进程切换 线程切换 协程切换
耗时 数十微秒 数微秒 亚微秒
缓存影响 严重 中等 极小
内核参与
8. 常见问题及解决方案
  • 问题:高并发场景下 CPU 使用率低但吞吐量低

    • 解决:可能是上下文切换过多,使用 pidstat -wvmstat 诊断
  • 问题:GC 导致频繁 STW 和上下文切换

    • 解决:调优 JVM GC 参数,减少 STW 时间
9. 版本差异与实现边界
  • 上下文切换由操作系统内核负责,JVM 无法直接控制
  • Linux 2.6+ 使用 CFS(Completely Fair Scheduler)调度
  • JDK 21 虚拟线程使用用户态调度,避免内核态上下文切换
10. 常见追问
  • 追问:如何减少上下文切换?
    • 回答方向:控制线程数量、使用线程池、使用协程、批量处理。
11. 易错点
  • ❌ 错误:上下文切换就是线程阻塞

  • ✅ 正确:阻塞是线程状态变化,上下文切换是 CPU 调度行为,两者相关但不同

  • ❌ 错误:上下文切换对性能没有影响

  • ✅ 正确:频繁切换会严重影响缓存命中率和系统吞吐量

一句话总结

上下文切换是 CPU 在线程间切换时保存恢复状态的过程,有显著性能开销,应通过合理控制线程数量和使用协程来减少切换。

3.1 线程基础(下)


sleep() 和 wait() 的区别是什么?

原始问法:

  • sleep()和wait()的区别是什么?

来源题目:SRC-03-31-101

面试先答

sleep()wait() 的核心区别有五点:第一,sleep()Thread 类的静态方法,wait()Object 类的实例方法;第二,sleep() 不释放锁,wait() 释放锁;第三,sleep() 可以在任何地方调用,wait() 必须在同步块内调用;第四,sleep() 进入 TIMED_WAITING 状态,时间到自动恢复,wait() 进入 WAITING 状态,必须被 notify()/notifyAll() 唤醒;第五,sleep() 直接操作系统层面的定时,wait() 基于对象监视器实现。

核心结论
  • sleep() 不释放锁,wait() 释放锁
  • sleep()Thread 静态方法,wait()Object 实例方法
  • sleep() 可在任何地方调用,wait() 必须在同步块内
  • sleep() 进入 TIMED_WAITING,wait() 进入 WAITING
  • sleep() 靠时间恢复,wait() 靠通知恢复
1. 是什么
// Thread.sleep() - JDK 源码
public static void sleep(long millis) throws InterruptedException {
    // native 方法实现
}

// Object.wait() - JDK 源码
public final void wait() throws InterruptedException {
    // native 方法实现
}
2. 为什么需要它
  • sleep() 用于时间控制,不需要释放锁
  • wait() 用于线程间协作,必须释放锁让其他线程有机会获取
3. 底层原理与完整流程

sleep() 流程

  1. 调用 Thread.sleep(time)
  2. JVM 设置定时器
  3. 线程进入 TIMED_WAITING 状态
  4. 不释放任何锁
  5. 时间到后自动恢复到 RUNNABLE

wait() 流程

  1. 必须在 synchronized 块内调用
  2. 调用 Object.wait()
  3. 释放对象锁
  4. 线程进入 WAITING 状态,加入对象的等待队列
  5. 其他线程调用 notify()/notifyAll() 唤醒
  6. 线程重新竞争锁,获取后从 wait() 返回
4. 怎么使用
// sleep 使用场景:暂停执行,不需要线程协作
Thread.sleep(1000); // 暂停 1 秒,持有锁不会释放

// wait 使用场景:线程间协作
synchronized (lock) {
    while (!dataReady) {
        lock.wait(); // 释放锁,等待通知
    }
    processData();
}

// notify 唤醒
synchronized (lock) {
    dataReady = true;
    lock.notify(); // 唤醒等待的线程
}
5. 适用场景
  • sleep():轮询间隔、定时任务、测试延迟
  • wait():生产者-消费者、线程间条件同步
6. 不适用场景与替代方案
  • 不要用 sleep() 替代 wait(),会导致死锁
  • 不要用 wait() 做简单延迟,过于复杂
  • 需要超时等待时用 wait(timeout)Condition.await(timeout)
7. 优缺点与技术取舍
维度 sleep() wait()
所属类 Thread Object
是否释放锁
调用位置 任意位置 同步块内
状态 TIMED_WAITING WAITING
恢复条件 超时/中断 通知/超时/中断
作用 时间控制 线程协作
8. 常见问题及解决方案
  • 问题sleep() 期间持有锁导致其他线程无法获取

    • 解决:使用 wait()/notify() 机制
  • 问题wait() 未在同步块内调用抛出 IllegalMonitorStateException

    • 解决:确保在 synchronized 块内调用 wait()
9. 版本差异与实现边界
  • 两者从 Java 1.0 就存在
  • JDK 21 中虚拟线程的 sleep() 实现有优化
10. 常见追问
  • 追问:为什么 wait() 要放在 while 循环中?
    • 回答方向:防止虚假唤醒,确保条件满足后再继续执行。
11. 易错点
  • ❌ 错误:sleep() 会释放锁

  • ✅ 正确:sleep() 不释放锁,这是重要区别

  • ❌ 错误:wait() 可以不在同步块中调用

  • ✅ 正确:必须在同步块中调用,否则抛 IllegalMonitorStateException

一句话总结

sleep()Thread 的静态方法、不释放锁、用于时间控制;wait()Object 的实例方法、释放锁、用于线程协作。


并发和并行的区别是什么?

原始问法:

  • 并发和并行的区别是什么?

来源题目:SRC-03-31-102

面试先答

并发(Concurrency)是指在同一时间段内交替执行多个任务,强调的是"同时处理多个事情的能力",不要求真正同时执行,单核 CPU 通过时间片轮转也能实现并发。并行(Parallelism)是指在同一时刻真正同时执行多个任务,需要多个 CPU 核心。简单来说:并发是宏观上同时、微观上交替;并行是宏观和微观上都同时。Java 中 线程 实现并发,多核 + 多线程 实现并行,ForkJoinPoolCompletableFuture 是实现并行的工具。

核心结论
  • 并发:同一时间段交替执行,单核即可
  • 并行:同一时刻同时执行,需要多核
  • 并行一定是并发,但并发不一定是并行
  • Java 线程在单核上实现并发,在多核上实现并行
1. 是什么

并发(Concurrency):多个任务在时间上重叠执行,通过 CPU 时间片轮转实现。

并行(Parallelism):多个任务在同一时刻真正同时执行,依赖多核 CPU。

2. 为什么需要它
  • 并发:提高系统吞吐量,减少 I/O 等待时间
  • 并行:利用多核 CPU,提高计算效率
3. 底层原理与完整流程

并发实现(单核)

时间轴:[线程1] [线程2] [线程1] [线程3] [线程2]

操作系统通过时间片(通常 5-10ms)在多个线程间快速切换。

并行实现(多核)

Core 0: [线程1] [线程3]
Core 1: [线程2] [线程4]

多个核心真正同时执行不同线程。

4. 怎么使用
// 并发:单线程内交替执行
new Thread(() -> {
    // 任务 A
    doTaskA();
}).start();

new Thread(() -> {
    // 任务 B
    doTaskB();
}).start();

// 并行:使用 ForkJoinPool
ForkJoinPool pool = new ForkJoinPool(4); // 4 核并行
pool.submit(() -> {
    // 并行任务
}).join();

// CompletableFuture 实现并行
CompletableFuture<String> f1 = CompletableFuture.supplyAsync(taskA, pool);
CompletableFuture<String> f2 = CompletableFuture.supplyAsync(taskB, pool);
CompletableFuture.allOf(f1, f2).join();
5. 适用场景
  • 并发:I/O 密集型任务,如数据库操作、网络请求
  • 并行:CPU 密集型任务,如大数据处理、复杂计算
6. 不适用场景与替代方案
  • 不要在单核 CPU 上做并行计算,会增加切换开销
  • 不要在低并发场景下过度设计并行
  • 替代方案:使用 Stream.parallel() 自动并行化
7. 优缺点与技术取舍
维度 并发 并行
CPU 需求 单核即可 需要多核
执行方式 交替执行 同时执行
适用场景 I/O 密集 CPU 密集
复杂度 较高(同步问题) 高(数据竞争)
8. 常见问题及解决方案
  • 问题:并行流性能反而下降
    • 解决:数据量小时并行开销更大,使用 parallel() 前评估数据规模
9. 版本差异与实现边界
  • Java 8 引入并行流 Stream.parallel()
  • ForkJoinPool 在 Java 7 引入,Java 8 优化
  • 并行度默认等于 CPU 核数,可通过 System.setProperty 调整
10. 常见追问
  • 追问:并行一定比串行快吗?
    • 回答方向:不一定,数据量小时并行开销可能超过收益。
11. 易错点
  • ❌ 错误:并发就是并行

  • ✅ 正确:并发是交替执行,并行是同时执行

  • ❌ 错误:单核 CPU 不支持并发

  • ✅ 正确:单核 CPU 通过时间片轮转支持并发

一句话总结

并发是宏观同时微观交替,并行是宏观微观都同时,并发解决 I/O 等待,并行利用多核计算。


同步和异步的区别是什么?

原始问法:

  • 同步和异步的区别是什么?

来源题目:SRC-03-31-103

面试先答

同步是指调用方发出请求后必须等待结果返回才能继续执行,异步是指调用方发出请求后无需等待,继续执行后续逻辑,结果通过回调、通知或轮询获取。同步的优点是逻辑简单、结果立即可用,缺点是阻塞;异步的优点是不阻塞、响应快,缺点是逻辑复杂。Java 中 Future/CompletableFuture、事件驱动模型、消息队列都是异步的实现方式。

核心结论
  • 同步:阻塞等待结果,逻辑简单
  • 异步:非阻塞,通过回调/通知获取结果
  • 同步适合实时性要求高的场景
  • 异步适合高并发、低延迟场景
1. 是什么

同步(Synchronous):调用方发出调用后阻塞等待结果返回。

异步(Asynchronous):调用方发出调用后不阻塞,结果通过回调或通知返回。

2. 为什么需要它
  • 同步:保证数据一致性、逻辑清晰
  • 异步:提高系统吞吐量、降低延迟
3. 底层原理与完整流程

同步调用流程

调用方 → 请求 → 被调用方 → 处理 → 返回结果 → 调用方继续
              ↑阻塞等待↓

异步调用流程

调用方 → 请求 → 被调用方
     ↓继续执行    ↓处理
     回调/通知 ← 结果
4. 怎么使用
// 同步方式
String result = service.getData(); // 阻塞等待

// 异步方式 - Future
Future<String> future = executor.submit(() -> service.getData());
// 继续执行其他逻辑
String result = future.get(); // 需要时获取结果

// 异步方式 - CompletableFuture
CompletableFuture<String> future = CompletableFuture
    .supplyAsync(() -> service.getData())
    .thenApply(result -> processResult(result));

// 异步回调
future.thenAccept(result -> {
    System.out.println("结果: " + result);
});
5. 适用场景
  • 同步:数据库事务操作、需要立即返回结果的接口
  • 异步:消息队列、异步日志、批量数据处理
6. 不适用场景与替代方案
  • 不要在需要实时结果的场景使用异步
  • 不要为了异步而异步,增加不必要的复杂度
  • 替代方案:使用 CompletableFuture 简化异步编程
7. 优缺点与技术取舍
维度 同步 异步
复杂度 简单 复杂
阻塞
实时性
吞吐量
8. 常见问题及解决方案
  • 问题:异步回调嵌套过深(回调地狱)
    • 解决:使用 CompletableFuture 的链式调用
9. 版本差异与实现边界
  • Java 5:Future 接口
  • Java 8:CompletableFuture 大幅简化异步编程
  • Reactor/Spring WebFlux:响应式编程
10. 常见追问
  • 追问Future.get() 是同步还是异步?
    • 回答方向:get() 本身是同步阻塞的,但 submit() 是异步提交。
11. 易错点
  • ❌ 错误:异步一定比同步快
  • ✅ 正确:异步吞吐量更高,但单次调用延迟可能更高
一句话总结

同步阻塞等待结果、逻辑简单,异步非阻塞、吞吐量高,根据场景选择合适的通信方式。


单核 CPU 支持 Java 多线程吗?

原始问法:

  • 单核CPU支持Java多线程吗?

来源题目:SRC-03-31-104

面试先答

单核 CPU 完全支持 Java 多线程。多线程是操作系统级别的特性,通过时间片轮转实现并发执行。单核 CPU 上的多线程是"并发"而非"并行"——多个线程在不同时间段交替执行,给人同时执行的错觉。Java 多线程在单核上的主要价值是处理 I/O 密集型任务(减少等待时间)和提高程序的响应性。对于 CPU 密集型任务,单核多线程反而可能因上下文切换而变慢。

核心结论
  • 单核 CPU 支持多线程,通过时间片轮转实现并发
  • 单核上的多线程是并发不是并行
  • I/O 密集型任务在单核多线程上有收益
  • CPU 密集型任务在单核多线程上可能更慢
1. 是什么

单核 CPU 通过操作系统的时间片轮转调度,支持多个线程并发执行。

2. 为什么需要它
  • I/O 操作时 CPU 空闲,多线程可以利用等待时间执行其他任务
  • 提高程序响应性,一个线程阻塞时其他线程可继续执行
3. 底层原理与完整流程

单核 CPU 调度流程:

  1. 操作系统为每个线程分配时间片(通常 5-10ms)
  2. 时间片用完或线程阻塞时,切换到下一个线程
  3. 切换涉及上下文保存/恢复(开销约微秒级)
4. 怎么使用
// 单核 CPU 上使用多线程 - I/O 密集型场景
new Thread(() -> {
    // 数据库查询(I/O 等待时 CPU 可执行其他线程)
    queryDatabase();
}).start();

new Thread(() -> {
    // 同时处理其他任务
    processOtherTask();
}).start();
5. 适用场景
  • 单核多线程适用:I/O 密集型、事件驱动、提高响应性
  • 单核多线程不适用:CPU 密集型(如下载大文件、大数据计算)
6. 不适用场景与替代方案
  • 不要在单核上创建过多线程,会增加切换开销
  • CPU 密集型任务建议单线程执行
  • 替代方案:使用 NIO 单线程处理 I/O
7. 优缺点与技术取舍
维度 单核多线程 单核单线程
I/O 等待 可执行其他任务 阻塞等待
切换开销
CPU 利用率 更高(I/O 场景) 更高(CPU 场景)
复杂度
8. 常见问题及解决方案
  • 问题:单核 CPU 上线程越多越慢
    • 解决:对于 CPU 密集型任务减少线程数,对于 I/O 密集型任务适当增加
9. 版本差异与实现边界
  • 所有版本的 Java 都支持单核多线程
  • 操作系统调度由内核负责,与 JVM 版本无关
10. 常见追问
  • 追问:单核 CPU 上线程数最佳实践?
    • 回答方向:CPU 密集型=1,I/O 密集型=CPU核数 * (1 + I/O等待时间/CPU时间)。
11. 易错点
  • ❌ 错误:单核 CPU 不能运行多线程

  • ✅ 正确:单核 CPU 通过时间片轮转支持多线程

  • ❌ 错误:多线程在所有场景下都更快

  • ✅ 正确:CPU 密集型场景多线程在单核上更慢

一句话总结

单核 CPU 通过时间片轮转支持多线程并发执行,适合 I/O 密集型场景,不适合 CPU 密集型场景。


Java 的线程调度方式有哪些?

原始问法:

  • Java的线程调度方式有哪些?

来源题目:SRC-03-31-105

面试先答

Java 线程调度分为两种:抢占式调度和协作式调度。Java 默认使用抢占式调度,即每个线程由操作系统分配时间片,时间片用完后强制让出 CPU。同时 Java 也支持通过 Thread.yield() 主动让出 CPU(协作式),以及通过线程优先级 setPriority() 调整调度权重,但优先级不保证严格遵守。实际上 Java 的线程调度主要由操作系统内核负责,JVM 只是映射,Windows 采用抢占式,Linux 采用 CFS 完全公平调度。

核心结论
  • Java 使用抢占式调度(默认)+ 协作式调度(可选)
  • 线程优先级只是建议,不保证严格执行
  • 实际调度由操作系统内核负责
  • Thread.yield() 主动让出 CPU,但不保证成功
1. 是什么

抢占式调度:操作系统强制分配 CPU 时间片,时间到强制切换。

协作式调度:线程主动让出 CPU,需要线程配合。

2. 为什么需要它
  • 抢占式:防止单个线程长时间占用 CPU,保证公平性
  • 协作式:允许线程主动让出 CPU,提高响应性
3. 底层原理与完整流程

抢占式调度流程

  1. 操作系统维护就绪队列
  2. 每个线程分配时间片(5-10ms)
  3. 时间片用完,强制调度下一个线程
  4. 高优先级线程可以抢占低优先级线程

协作式调度

Thread.yield(); // 主动让出 CPU,但不保证成功

线程优先级

thread.setPriority(Thread.MAX_PRIORITY); // 1-10
// 优先级只是给操作系统的建议
4. 怎么使用
// 设置线程优先级
Thread highPriority = new Thread(task);
highPriority.setPriority(Thread.MAX_PRIORITY); // 10

Thread lowPriority = new Thread(task);
lowPriority.setPriority(Thread.MIN_PRIORITY); // 1

// 主动让出 CPU
Thread.currentThread().yield();

// 线程中断(协作式取消)
thread.interrupt(); // 设置中断标志
5. 适用场景
  • 抢占式:通用场景,保证公平性
  • 协作式:需要精细控制的场景,如大任务分批执行
6. 不适用场景与替代方案
  • 不要依赖线程优先级保证执行顺序
  • 不要用 yield() 做同步,不可靠
  • 替代方案:使用 synchronizedLock、同步工具类
7. 优缺点与技术取舍
维度 抢占式 协作式
公平性 依赖线程配合
响应性 可能低
实现复杂度
CPU 利用率 可能低(切换多)
8. 常见问题及解决方案
  • 问题:设置高优先级线程没有先执行
    • 解决:优先级只是建议,不保证严格遵守,依赖操作系统实现
9. 版本差异与实现边界
  • Java 调度依赖操作系统:
    • Windows:抢占式,优先级敏感
    • Linux:CFS 完全公平调度,优先级影响小
    • macOS:类似 Linux
  • Thread.yield() 在不同 OS 上行为不同
10. 常见追问
  • 追问Thread.sleep()Thread.yield() 的区别?
    • 回答方向:sleep() 阻塞指定时间,yield() 只是让出时间片且不保证成功。
11. 易错点
  • ❌ 错误:高优先级线程一定先执行

  • ✅ 正确:优先级只是建议,不保证严格遵守

  • ❌ 错误:yield() 一定让其他线程执行

  • ✅ 正确:yield() 只是建议让出 CPU,调度器可能立即重新选中当前线程

一句话总结

Java 默认抢占式调度,支持协作式让出,实际调度由操作系统决定,优先级和 yield() 都只是建议不保证执行。

3.2 线程池(上)


什么是线程池?为什么要使用线程池?如何给线程池命名?

原始问法:

  • 什么是线程池?为什么要使用线程池?
  • 如何给线程池命名?有什么作用?

来源题目:SRC-03-32-107, SRC-03-32-115

面试先答

线程池是预先创建并维护一定数量线程的池子,任务提交后复用池中的线程执行,避免频繁创建销毁。使用线程池的核心原因有三:第一,降低资源消耗,复用已创建的线程;第二,提高响应速度,任务到达时无需等待线程创建;第三,统一管理线程的创建、销毁和监控。给线程池命名是为了便于问题排查,通过自定义 ThreadFactory 给每个线程加上有意义的前缀,如"订单处理-"、"邮件发送-",在 JStack 输出和日志中可以快速定位线程归属。

核心结论
  • 线程池复用线程,避免频繁创建销毁
  • 三大优势:降低消耗、提高响应、统一管理
  • 通过 ThreadFactory 给线程命名,便于监控排查
1. 是什么

线程池(ThreadPool):存放已创建线程的容器,任务提交时从池中取线程执行,用完后归还。

Java 中核心实现为 ThreadPoolExecutor

2. 为什么需要它
  • 频繁创建销毁线程开销大(涉及系统调用、栈内存分配)
  • 线程数量难以控制,可能导致资源耗尽
  • 缺乏统一的监控和管理手段
3. 底层原理与完整流程

线程池核心组件:

  • corePoolSize:核心线程数(常驻)
  • maximumPoolSize:最大线程数
  • workQueue:任务队列
  • keepAliveTime:非核心线程空闲存活时间
  • threadFactory:线程创建工厂
  • rejectedExecutionHandler:拒绝策略
4. 怎么使用
// 自定义命名线程工厂
ThreadFactory namedFactory = new ThreadFactory() {
    private final AtomicInteger counter = new AtomicInteger(0);
    private final String prefix;

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

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

// 创建带命名的线程池
ExecutorService pool = new ThreadPoolExecutor(
    4, 8, 60L, TimeUnit.SECONDS,
    new LinkedBlockingQueue<>(100),
    new NamedThreadFactory("order-pool"),
    new ThreadPoolExecutor.CallerRunsPolicy()
);
5. 适用场景
  • 需要频繁创建线程的场景
  • 高并发 Web 服务
  • 异步任务处理
6. 不适用场景与替代方案
  • 不要为每个简单任务创建独立的线程池
  • 不要在关闭的线程池上提交任务
  • 单任务场景:直接用 CompletableFuture
7. 优缺点与技术取舍
维度 线程池 手动创建线程
性能 高(复用) 低(频繁创建销毁)
可控性 高(统一管理)
复杂度 需配置 简单
8. 常见问题及解决方案
  • 问题:线程池命名后 JStack 仍看不清
    • 解决:使用有意义的前缀,如业务域+功能
9. 版本差异与实现边界
  • Java 5:引入 Executor 框架和 ThreadPoolExecutor
  • Java 7:ForkJoinPool
  • Java 8:CompletableFuture 默认使用 ForkJoinPool.commonPool()
10. 常见追问
  • 追问:线程池名字在 JStack 中如何显示?
    • 回答方向:Thread.getName() 返回的名字会出现在 JStack 输出中。
11. 易错点
  • ❌ 错误:线程池越多越好
  • ✅ 正确:线程池需要合理规划,过多反而增加开销
一句话总结

线程池复用线程、统一管理,通过命名便于监控,是生产环境并发编程的基础设施。


线程池的核心参数有哪些?拒绝策略有哪些?

原始问法:

  • 线程池的核心参数有哪些?分别是什么含义?
  • 线程池有哪些拒绝策略?分别是什么含义?

来源题目:SRC-03-32-108, SRC-03-32-110

面试先答

ThreadPoolExecutor 有七个核心参数:corePoolSize(核心线程数)、maximumPoolSize(最大线程数)、keepAliveTime(非核心线程存活时间)、unit(存活时间单位)、workQueue(任务队列)、threadFactory(线程工厂)、handler(拒绝策略)。四种拒绝策略:AbortPolicy(抛异常,默认)、CallerRunsPolicy(调用线程执行)、DiscardPolicy(静默丢弃)、DiscardOldestPolicy(丢弃队列最老任务)。生产环境推荐自定义拒绝策略记录日志,避免任务丢失。

核心结论
  • 七个参数:核心线程数、最大线程数、存活时间、时间单位、任务队列、线程工厂、拒绝策略
  • 四种拒绝策略:抛异常、调用者执行、静默丢弃、丢弃最老
  • 默认拒绝策略是 AbortPolicy(抛异常)
  • 生产环境推荐 CallerRunsPolicy 或自定义记录日志
1. 是什么

七个核心参数

参数 类型 含义
corePoolSize int 常驻核心线程数
maximumPoolSize int 最大线程数
keepAliveTime long 非核心线程空闲存活时间
unit TimeUnit 存活时间单位
workQueue BlockingQueue 任务等待队列
threadFactory ThreadFactory 线程创建工厂
handler RejectedExecutionHandler 拒绝策略

四种拒绝策略

策略 行为 适用场景
AbortPolicy RejectedExecutionException 默认,快速失败
CallerRunsPolicy 调用线程执行任务 不允许任务丢失
DiscardPolicy 静默丢弃,不抛异常 允许任务丢失
DiscardOldestPolicy 丢弃队列最老任务 新任务更重要
2. 为什么需要它
  • 不同业务场景需要不同的线程池配置
  • 拒绝策略决定了系统过载时的行为
  • 合理配置可避免系统崩溃和任务丢失
3. 底层原理与完整流程
public ThreadPoolExecutor(
    int corePoolSize,
    int maximumPoolSize,
    long keepAliveTime,
    TimeUnit unit,
    BlockingQueue<Runnable> workQueue,
    ThreadFactory threadFactory,
    RejectedExecutionHandler handler
)
4. 怎么使用
// 生产环境推荐配置
ThreadPoolExecutor executor = new ThreadPoolExecutor(
    4,                          // corePoolSize
    8,                          // maximumPoolSize
    30L,                        // keepAliveTime
    TimeUnit.SECONDS,           // unit
    new LinkedBlockingQueue<>(500), // workQueue(有界队列)
    new ThreadFactoryBuilder()
        .setNameFormat("biz-pool-%d")
        .setDaemon(false)
        .build(),              // threadFactory
    (r, e) -> {
        log.warn("任务被拒绝,记录日志");
        // 可选:存入数据库或重试队列
    }                          // 自定义拒绝策略
);
5. 适用场景
  • AbortPolicy:关键业务,任务不能丢失
  • CallerRunsPolicy:一般业务,允许降级
  • DiscardPolicy:可丢弃的日志、通知
  • DiscardOldestPolicy:实时性要求高
6. 不适用场景与替代方案
  • 不要使用无界队列,会导致 OOM
  • 不要用 Executors 默认创建,参数不可控
  • 替代方案:使用 Spring 的 ThreadPoolTaskExecutor
7. 优缺点与技术取舍
维度 有界队列 无界队列
内存安全 安全 可能 OOM
线程复用 充分 不充分
任务堆积 有上限 无限堆积
8. 常见问题及解决方案
  • 问题:使用无界 LinkedBlockingQueue 导致 OOM

    • 解决:使用有界队列
  • 问题AbortPolicy 导致业务异常

    • 解决:改用 CallerRunsPolicy 或自定义策略
9. 版本差异与实现边界
  • 所有参数从 Java 5 就存在
  • Executors 创建的线程池:
    • newFixedThreadPool:无界队列
    • newCachedThreadPool:最大线程数为 Integer.MAX_VALUE
    • newSingleThreadExecutor:无界队列
    • 生产环境应避免使用
10. 常见追问
  • 追问:核心线程数可以动态调整吗?
    • 回答方向:可以,通过 setCorePoolSize()setMaximumPoolSize()
11. 易错点
  • ❌ 错误:maximumPoolSize 一定生效
  • ✅ 正确:如果使用无界队列,maximumPoolSize 不会触发
一句话总结

线程池七个参数控制行为,四种拒绝策略应对过载,生产环境必须自定义配置。


线程池的任务执行流程是怎样的?

原始问法:

  • 线程池的任务执行流程是怎样的?

来源题目:SRC-03-32-109

面试先答

线程池的任务执行流程遵循以下步骤:第一,判断核心线程数是否已满,未满则创建核心线程执行任务;第二,核心线程已满则尝试将任务加入队列;第三,队列已满则判断最大线程数是否已满,未满则创建非核心线程执行;第四,最大线程数已满则执行拒绝策略。这个流程的设计理念是优先复用核心线程,其次使用队列缓冲,最后才考虑扩容。使用无界队列时,最大线程数配置不会生效。

核心结论
  • 执行流程:核心线程 → 队列 → 非核心线程 → 拒绝
  • 无界队列时最大线程数不生效
  • 非核心线程空闲超过 keepAliveTime 后销毁
1. 是什么

任务提交后的判断流程图:

提交任务
  ↓
核心线程已满? ──否──→ 创建核心线程执行
  ↓是
队列已满? ──否──→ 加入队列等待
  ↓是
最大线程已满? ──否──→ 创建非核心线程执行
  ↓是
执行拒绝策略
2. 为什么需要它
  • 优先使用核心线程,保证常驻线程的高复用
  • 使用队列缓冲,应对突发流量
  • 最后扩容非核心线程,兼顾吞吐量和资源
3. 底层原理与完整流程

ThreadPoolExecutor.execute() 源码流程:

public void execute(Runnable command) {
    int c = ctl.get();
    // 1. 核心线程未满
    if (workerCountOf(c) < corePoolSize) {
        if (addWorker(command, true)) return;
        c = ctl.get();
    }
    // 2. 尝试加入队列
    if (isRunning(c) && workQueue.offer(command)) {
        // 重新检查状态
        if (!isRunning(c) && remove(command))
            reject(command);
        else if (workerCountOf(c) == 0)
            addWorker(null, false);
    }
    // 3. 尝试创建非核心线程
    else if (!addWorker(command, false))
        // 4. 执行拒绝策略
        reject(command);
}
4. 怎么使用
// 合理配置参数
ThreadPoolExecutor pool = new ThreadPoolExecutor(
    4,          // corePoolSize
    8,          // maximumPoolSize
    60,         // keepAliveTime
    TimeUnit.SECONDS,
    new ArrayBlockingQueue<>(100) // 有界队列
);

pool.execute(() -> {
    // 任务逻辑
});
5. 适用场景
  • 高并发 Web 服务
  • 异步任务处理
  • 批量数据处理
6. 不适用场景与替代方案
  • 不要使用无界队列,会跳过最大线程限制
  • 不要在任务中递归提交任务(可能死锁)
  • 替代方案:CompletableFutureForkJoinPool
7. 优缺点与技术取舍

流程设计保证了系统的稳定性:优先复用核心线程,通过队列削峰,最后才扩容。但队列选择不当会影响整体行为。

8. 常见问题及解决方案
  • 问题:线程池参数设置后最大线程数无效
    • 解决:检查是否使用了无界队列(如 LinkedBlockingQueue 默认构造)
9. 版本差异与实现边界
  • 从 Java 5 起流程不变
  • Executors 工具类简化了创建但隐藏了配置细节
10. 常见追问
  • 追问:为什么要先加入队列再创建非核心线程?
    • 回答方向:优先保证核心线程的复用,队列起到削峰填谷的作用。
11. 易错点
  • ❌ 错误:提交任务一定会执行

  • ✅ 正确:极端情况下会触发拒绝策略

  • ❌ 错误:maximumPoolSize 总是生效

  • ✅ 正确:使用无界队列时不生效

一句话总结

线程池执行流程:核心线程→队列→非核心线程→拒绝,设计理念是优先复用、缓冲削峰、最后扩容。


常见的线程池有哪些?使用 Executors 创建线程池有什么坑?

原始问法:

  • 常见的线程池有哪些?使用Executors创建线程池有什么坑?

来源题目:SRC-03-32-111

面试先答

常见的线程池有四种:newFixedThreadPool(固定大小)、newCachedThreadPool(缓存型)、newSingleThreadExecutor(单线程)、newScheduledThreadPool(定时调度)。它们的底层都是 ThreadPoolExecutor。使用 Executors 创建线程池有两个大坑:第一,newFixedThreadPoolnewSingleThreadExecutor 使用无界 LinkedBlockingQueue,高并发下可能导致 OOM;第二,newCachedThreadPoolmaximumPoolSizeInteger.MAX_VALUE,可能创建大量线程导致 OOM。生产环境必须手动创建 ThreadPoolExecutor,合理设置所有参数。

核心结论
  • 四种常用线程池,底层都是 ThreadPoolExecutor
  • Executors 创建的线程池有 OOM 风险
  • 生产环境必须手动配置 ThreadPoolExecutor
  • 禁止在生产代码中直接使用 Executors
1. 是什么

四种常用线程池

线程池 特点 队列 最大线程数
newFixedThreadPool 固定线程数 无界 LinkedBlockingQueue = corePoolSize
newCachedThreadPool 按需创建,空闲回收 SynchronousQueue Integer.MAX_VALUE
newSingleThreadExecutor 单线程串行 无界 LinkedBlockingQueue 1
newScheduledThreadPool 定时调度 DelayedWorkQueue 可配置
2. 为什么需要它
  • Executors 提供了快捷创建方式,简化使用
  • 但默认参数不适合生产环境,存在 OOM 风险
3. 底层原理与完整流程

Executors.newFixedThreadPool() 源码:

public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(
        nThreads, nThreads, 0L, TimeUnit.MILLISECONDS,
        new LinkedBlockingQueue<Runnable>() // 无界队列!
    );
}

Executors.newCachedThreadPool() 源码:

public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(
        0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS,
        new SynchronousQueue<Runnable>()
    );
}
4. 怎么使用
// 生产环境正确方式
int processors = Runtime.getRuntime().availableProcessors();

ThreadPoolExecutor executor = new ThreadPoolExecutor(
    processors,                     // 核心线程数
    processors * 2,                 // 最大线程数
    60L, TimeUnit.SECONDS,          // 存活时间
    new ArrayBlockingQueue<>(200),  // 有界队列
    new ThreadFactoryBuilder()
        .setNameFormat("app-pool-%d")
        .build(),
    new ThreadPoolExecutor.CallerRunsPolicy()
);
5. 适用场景
  • newFixedThreadPool:CPU 密集型任务
  • newCachedThreadPool:短时异步任务
  • newSingleThreadExecutor:串行任务
  • newScheduledThreadPool:定时任务
6. 不适用场景与替代方案
  • 不要在生产环境使用 Executors 默认创建
  • 替代方案:手动创建 ThreadPoolExecutor,或使用 Spring ThreadPoolTaskExecutor
7. 优缺点与技术取舍

Executors 方便但不安全;手动创建复杂但可控。

8. 常见问题及解决方案
  • 问题newFixedThreadPool 导致 OOM
    • 解决:改用有界队列的 ThreadPoolExecutor
9. 版本差异与实现边界
  • 从 Java 5 起 Executors 存在
  • Alibaba Java 开发手册明确禁止生产环境使用 Executors
10. 常见追问
  • 追问ForkJoinPoolThreadPoolExecutor 的区别?
    • 回答方向:ForkJoinPool 基于工作窃取算法,适合分治任务。
11. 易错点
  • ❌ 错误:Executors 创建的线程池是安全的
  • ✅ 正确:无界队列和超大最大线程数都可能导致 OOM
一句话总结

Executors 创建快捷但有 OOM 风险,生产环境必须手动创建 ThreadPoolExecutor 合理配置所有参数。


线程池参数可以动态调整吗?通过什么方式?

原始问法:

  • 线程池参数可以动态调整吗?通过什么方式?

来源题目:SRC-03-32-112

面试先答

ThreadPoolExecutor 的核心参数全部支持动态调整,包括:核心线程数(setCorePoolSize)、最大线程数(setMaximumPoolSize)、存活时间(setKeepAliveTime)、线程工厂(setThreadFactory)、拒绝策略(setRejectedExecutionHandler)。调整后立即对后续任务生效。队列类型无法动态调整,但可以通过 BlockingQueue 的实现间接地改变行为。线上可通过 JMX、Archaius 或自定义管理接口实现参数的动态调整。

核心结论
  • 核心线程数、最大线程数可动态调整
  • 存活时间、线程工厂、拒绝策略可动态调整
  • 调整后立即生效
  • 队列无法动态切换
1. 是什么

ThreadPoolExecutor 提供的动态调整方法:

public void setCorePoolSize(int corePoolSize)
public void setMaximumPoolSize(int maximumPoolSize)
public void setKeepAliveTime(long time, TimeUnit unit)
public void setThreadFactory(ThreadFactory threadFactory)
public void setRejectedExecutionHandler(RejectedExecutionHandler handler)
2. 为什么需要它
  • 线上业务量变化,需要弹性调整
  • 突发流量时扩容,低峰时缩容
  • 运行时切换拒绝策略应对不同情况
3. 底层原理与完整流程

setCorePoolSize() 为例:

  1. 如果新值 > 旧值:立即创建新线程执行任务
  2. 如果新值 < 旧值:不影响已有线程,等空闲后自然回收
4. 怎么使用
ThreadPoolExecutor executor = new ThreadPoolExecutor(
    4, 8, 60L, TimeUnit.SECONDS,
    new ArrayBlockingQueue<>(100)
);

// 动态调整核心线程数
executor.setCorePoolSize(8);

// 动态调整最大线程数
executor.setMaximumPoolSize(16);

// 动态调整拒绝策略
executor.setRejectedExecutionHandler(
    new ThreadPoolExecutor.CallerRunsPolicy()
);
5. 适用场景
  • 流量高峰时动态扩容
  • 流量低谷时缩容节省资源
  • 紧急情况下切换为降级策略
6. 不适用场景与替代方案
  • 不要频繁调整参数,可能导致抖动
  • 队列无法动态切换
  • 替代方案:使用 Spring Cloud Gateway 的动态线程池配置
7. 优缺点与技术取舍

动态调整灵活但需要监控支撑,调整不当可能导致线程数剧烈波动。

8. 常见问题及解决方案
  • 问题:动态调整后线程数立即变化
    • 解决:核心线程数增加时立即创建,减少时等空闲回收
9. 版本差异与实现边界
  • 从 Java 5 就支持动态调整
  • Spring ThreadPoolTaskExecutor 也支持动态调整
10. 常见追问
  • 追问:动态调整核心线程数时,已在执行的线程会被回收吗?
    • 回答方向:不会,已在执行的线程继续执行完毕后才回收。
11. 易错点
  • ❌ 错误:动态调整后所有线程立即按新参数执行
  • ✅ 正确:已在执行的线程不受影响
一句话总结

ThreadPoolExecutor 支持动态调整核心参数,调整后立即生效,可用于弹性应对业务量变化。


线程池任务执行完后线程如何回收?

原始问法:

  • 线程池任务执行完后线程如何回收?

来源题目:SRC-03-32-113

面试先答

线程池中线程的回收分为两种情况:核心线程和非核心线程。核心线程默认不会回收,即使空闲也会一直存在(除非设置 allowCoreThreadTimeOut)。非核心线程在空闲超过 keepAliveTime 后自动回收。回收流程是:线程执行完任务后,进入 getTask() 方法从队列取任务,如果队列没有任务且空闲超时,线程退出。最终当 workerCount 降为 0 时,线程池的 runState 变为 TERMINATED

核心结论
  • 核心线程默认不回收,除非设置 allowCoreThreadTimeOut
  • 非核心线程空闲超过 keepAliveTime 后回收
  • 回收通过 getTask() 返回 null 触发线程退出
  • 线程池关闭时所有线程被中断回收
1. 是什么

线程回收机制:

线程类型 回收条件 默认行为
核心线程 allowCoreThreadTimeOut=true 且空闲超时 不回收
非核心线程 空闲超过 keepAliveTime 自动回收
2. 为什么需要它
  • 避免长期空闲线程占用资源
  • 核心线程常驻以提高响应速度
  • 非核心线程按需回收以节省资源
3. 底层原理与完整流程

Worker 内部类的 runWorker() 方法:

final void runWorker(Worker w) {
    Thread wt = Thread.currentThread();
    Runnable task = w.firstTask;
    w.firstTask = null;
    boolean completedAbruptly = true;
    try {
        while (task != null || (task = getTask()) != null) {
            // 执行任务
            task.run();
            // 清除 task 引用
            task = null;
        }
        completedAbruptly = false;
    } finally {
        // 线程退出,处理回收
        processWorkerExit(w, completedAbruptly);
    }
}

getTask() 方法判断是否返回 null

  • 线程池正在关闭 → 返回 null
  • 工作线程数超过核心线程数且空闲超时 → 返回 null
4. 怎么使用
// 设置核心线程也可超时回收
ThreadPoolExecutor executor = new ThreadPoolExecutor(
    4, 8, 60L, TimeUnit.SECONDS,
    new ArrayBlockingQueue<>(100)
);
executor.allowCoreThreadTimeOut(true); // 核心线程也可回收

// 优雅关闭线程池
executor.shutdown(); // 不再接受新任务,等待已有任务完成
executor.awaitTermination(60, TimeUnit.SECONDS); // 等待完成
5. 适用场景
  • 长期固定负载:核心线程常驻
  • 波动负载:非核心线程动态回收
  • 资源紧张:开启核心线程超时回收
6. 不适用场景与替代方案
  • 不要频繁创建销毁线程,失去线程池的意义
  • 替代方案:使用 keepAliveTime 合理配置
7. 优缺点与技术取舍

核心线程常驻保证响应速度,非核心线程回收节省资源,通过参数平衡两者。

8. 常见问题及解决方案
  • 问题:线程池关闭时有任务丢失
    • 解决:使用 shutdown() + awaitTermination() 优雅关闭
9. 版本差异与实现边界
  • 从 Java 5 起回收机制不变
  • allowCoreThreadTimeOut 在 Java 5 就存在
10. 常见追问
  • 追问shutdown()shutdownNow() 的区别?
    • 回答方向:shutdown() 等待任务完成,shutdownNow() 立即中断所有线程。
11. 易错点
  • ❌ 错误:核心线程执行完任务后立即回收
  • ✅ 正确:核心线程默认常驻,循环等待新任务
一句话总结

核心线程常驻不回收,非核心线程空闲超时回收,通过 getTask() 判断退出,保证响应速度与资源节省的平衡。


线程池中线程异常后,会被销毁还是复用?

原始问法:

  • 线程池中线程异常后,会被销毁还是复用?

来源题目:SRC-03-32-114

面试先答

线程池中线程抛出未捕获异常后,会被销毁而非复用。ThreadPoolExecutor.Worker.runWorker() 方法中,如果任务执行时抛出 RuntimeException 且未被捕获,completedAbruptly 标志为 true,然后在 finally 块中调用 processWorkerExit() 移除该 Worker 并创建新的 Worker 替代。如果异常被 try-catch 捕获,线程会继续复用。线程池会自动补充新线程,保证 corePoolSize 数量。

核心结论
  • 未捕获异常 → 线程销毁,新线程替代
  • 已捕获异常 → 线程继续复用
  • 核心线程异常后会补充新线程
  • 可通过 afterExecute 钩子处理异常
1. 是什么

异常处理流程在 runWorker() 中:

try {
    while (task != null || (task = getTask()) != null) {
        task.run(); // 异常在此抛出
    }
    completedAbruptly = false; // 正常完成
} finally {
    processWorkerExit(w, completedAbruptly);
    // completedAbruptly=true 时销毁线程并补充
}
2. 为什么需要它
  • 防止异常线程影响后续任务
  • 保证线程池的健壮性
  • 自动维护线程数量
3. 底层原理与完整流程
  1. Worker 执行 task.run()
  2. 如果抛出未捕获异常 → completedAbruptly = true
  3. processWorkerExit() 中判断 completedAbruptly
  4. 如果为 true → addWorker() 创建新线程替代
  5. 新线程与原线程同等配置
4. 怎么使用
// 方式一:任务内部捕获异常
executor.execute(() -> {
    try {
        doWork();
    } catch (Exception e) {
        log.error("任务异常", e);
    }
});

// 方式二:覆盖 afterExecute 钩子
ThreadPoolExecutor executor = new ThreadPoolExecutor(
    4, 8, 60L, TimeUnit.SECONDS,
    new ArrayBlockingQueue<>(100)
) {
    @Override
    protected void afterExecute(Runnable r, Throwable t) {
        if (t != null) {
            log.error("线程池任务异常", t);
        }
    }
};
5. 适用场景
  • 所有线程池场景都需要处理异常
  • 通过 afterExecute 统一监控异常
6. 不适用场景与替代方案
  • 不要依赖线程池自动补充线程来容错
  • 替代方案:在任务内部 try-catch + 重试机制
7. 优缺点与技术取舍

自动补充保证线程数不减少,但频繁异常会导致线程频繁创建销毁。

8. 常见问题及解决方案
  • 问题:线程池不断创建新线程
    • 解决:检查是否有未捕获异常,修复业务逻辑
9. 版本差异与实现边界
  • 从 Java 5 起行为一致
  • afterExecute 钩子从 Java 5 就存在
10. 常见追问
  • 追问:如何获取线程池中的异常?
    • 回答方向:Future.get() 会抛出 ExecutionException,或通过 afterExecute 钩子。
11. 易错点
  • ❌ 错误:线程池会自动处理所有异常
  • ✅ 正确:未捕获异常会导致线程销毁
一句话总结

线程池中未捕获异常导致线程销毁并自动补充,已捕获异常则继续复用,通过 afterExecute 统一处理异常。


线程池大小如何规划配置?CPU 密集型和 IO 密集型如何考量?

原始问法:

  • 线程池大小如何规划配置?CPU密集型和IO密集型如何考量?

来源题目:SRC-03-32-116

面试先答

线程池大小规划需要区分 CPU 密集型和 I/O 密集型:CPU 密集型的核心线程数一般设为 CPU 核心数 + 1(+1 防止页面缺失导致线程短暂阻塞),最大线程数与核心线程数相同即可;I/O 密集型的核心线程数可设为 CPU 核心数 ×(1 + I/O 等待时间/CPU 计算时间),通常在 CPU 核数的 2-4 倍。此外还需考虑:任务优先级、执行时长、系统内存、最大并发请求数等因素。最终应通过压测确定最佳值。

核心结论
  • CPU 密集型:corePoolSize = CPU 核数 + 1
  • I/O 密集型:corePoolSize = CPU 核数 × (1 + I/O等待时间/CPU计算时间)
  • 需配合压测确定最优值
  • 不能脱离实际场景空谈配置
1. 是什么

CPU 密集型:大量计算、序列化、加密等,CPU 使用率接近 100%。

I/O 密集型:数据库查询、网络请求、文件读写等,CPU 等待 I/O 数据。

2. 为什么需要它
  • 线程数过少:CPU 利用率不足
  • 线程数过多:上下文切换开销增大
  • 合理配置可最大化吞吐量
3. 底层原理与完整流程

CPU 密集型公式

corePoolSize = N + 1
N = CPU 核心数

+1 是为了防止某个线程因页面缺失或其他原因短暂阻塞时,CPU 能继续工作。

I/O 密集型公式

corePoolSize = N × (1 + W/C)
N = CPU 核心数
W = I/O 等待时间
C = CPU 计算时间
4. 怎么使用
int cpuCores = Runtime.getRuntime().availableProcessors();

// CPU 密集型
int cpuIntensivePoolSize = cpuCores + 1;
ThreadPoolExecutor cpuPool = new ThreadPoolExecutor(
    cpuIntensivePoolSize,
    cpuIntensivePoolSize,
    0L, TimeUnit.MILLISECONDS,
    new ArrayBlockingQueue<>(200)
);

// I/O 密集型(假设 I/O 等待时间是计算时间的 4 倍)
int ioIntensivePoolSize = cpuCores * (1 + 4); // CPU核数 * 5
ThreadPoolExecutor ioPool = new ThreadPoolExecutor(
    ioIntensivePoolSize,
    ioIntensivePoolSize,
    60L, TimeUnit.SECONDS,
    new ArrayBlockingQueue<>(500)
);
5. 适用场景
  • CPU 密集型:大数据计算、视频编码、加解密
  • I/O 密集型:Web 服务、数据库访问、API 调用
6. 不适用场景与替代方案
  • 不要盲目套用公式,必须结合压测
  • 不要考虑 CPU 超线程,按物理核数计算
  • 替代方案:使用动态调整 + 监控
7. 优缺点与技术取舍
类型 线程数 原因
CPU 密集 N+1 最大化 CPU 利用率
I/O 密集 N×(1+W/C) 利用 I/O 等待时间
8. 常见问题及解决方案
  • 问题:按公式配置后性能不如预期
    • 解决:通过压测调整,考虑 GC、JIT、上下文切换等因素
9. 版本差异与实现边界
  • 线程池大小规划与 JDK 版本无关
  • 需考虑 CPU 架构:超线程、大小核等
10. 常见追问
  • 追问:如何估算 I/O 等待时间和 CPU 计算时间?
    • 回答方向:通过性能监控工具(如 JFR)测量,或通过 APM 工具采集。
11. 易错点
  • ❌ 错误:线程数越多性能越好
  • ✅ 正确:超过最优值后,上下文切换开销反而降低性能
一句话总结

CPU 密集型线程数为 N+1,I/O 密集型为 N×(1+W/C),最终通过压测确定最优配置。


什么是 Future 类?有什么作用?

原始问法:

  • 什么是Future类?有什么作用?

来源题目:SRC-03-32-117

面试先答

Future 是 Java 5 引入的异步计算结果接口,代表一个异步操作的未来结果。它提供了:判断任务是否完成(isDone())、获取结果(get(),阻塞等待)、取消任务(cancel())等能力。Future 本身是接口,常用实现为 FutureTaskFuture 的核心价值是将同步调用转换为异步调用——提交任务后立即返回,结果通过 Future 在需要时获取。但 Future 有局限:阻塞获取、无法链式调用、无法组合多个 Future。

核心结论
  • Future 是异步结果容器,提供获取、取消、判断完成等能力
  • 核心方法:get()cancel()isDone()isCancelled()
  • FutureTaskFuture 的实现类,同时实现了 Runnable
  • 局限:阻塞获取、无法链式组合
1. 是什么
public interface Future<V> {
    boolean cancel(boolean mayInterruptIfRunning);
    boolean isCancelled();
    boolean isDone();
    V get() throws InterruptedException, ExecutionException;
    V get(long timeout, TimeUnit unit) throws ...;
}
2. 为什么需要它
  • 异步执行任务,不阻塞主线程
  • 可以控制任务的生命周期(取消、判断完成)
  • 将异步结果与执行逻辑解耦
3. 底层原理与完整流程

FutureTask 状态机:

NEW → COMPLETING → NORMAL(正常完成)
                 → EXCEPTIONAL(异常完成)
                 → CANCELLED(取消)

get() 流程:

  1. 检查状态,若已完成则直接返回结果
  2. 若未完成,进入等待队列
  3. 任务完成后唤醒等待线程
  4. 返回结果或抛出异常
4. 怎么使用
// 使用 Callable + Future
ExecutorService executor = Executors.newCachedThreadPool();
Future<String> future = executor.submit(() -> {
    return "Hello Future";
});

// 其他业务逻辑
doOtherWork();

// 获取结果(阻塞)
String result = future.get();

// 带超时获取
try {
    String result = future.get(5, TimeUnit.SECONDS);
} catch (TimeoutException e) {
    future.cancel(true); // 超时则取消
}

// 直接使用 FutureTask
FutureTask<String> task = new FutureTask<>(() -> "Result");
new Thread(task).start();
String result = task.get();
5. 适用场景
  • 异步获取计算结果
  • 并行执行多个独立任务
  • 需要超时控制的异步调用
6. 不适用场景与替代方案
  • 不要用 Future 做链式调用和组合
  • 不要在提交后立即 get(),失去异步意义
  • 替代方案:CompletableFuture
7. 优缺点与技术取舍
维度 Future CompletableFuture
链式调用 不支持 支持
组合 不支持 支持
回调 不支持 支持
复杂度 简单 稍复杂
8. 常见问题及解决方案
  • 问题get() 阻塞导致性能问题
    • 解决:使用带超时的 get() 或改用 CompletableFuture
9. 版本差异与实现边界
  • Java 5:引入 FutureFutureTaskCallable
  • Java 8:CompletableFuture 扩展了 Future 接口
10. 常见追问
  • 追问FutureFutureTask 的区别?
    • 回答方向:Future 是接口,FutureTask 是实现类且实现了 Runnable
11. 易错点
  • ❌ 错误:Future.get() 会立即返回
  • ✅ 正确:get() 阻塞等待任务完成
一句话总结

Future 是异步结果容器,提供获取、取消、判断能力,简单但功能有限,复杂场景使用 CompletableFuture


CompletableFuture 相比 Future 有什么优势?

原始问法:

  • CompletableFuture相比Future有什么优势?

来源题目:SRC-03-32-118

面试先答

CompletableFuture 是 Java 8 引入的 Future 增强版,相比传统 Future 有三大优势:第一,支持链式调用(thenApplythenAccept),实现异步流水线;第二,支持组合多个 CompletableFuturethenCombineallOfanyOf),实现复杂异步编排;第三,支持回调式编程(whenCompleteexceptionally),无需阻塞等待。此外还支持手动完成(complete)、异常处理(exceptionally)等。CompletableFuture 的引入使 Java 拥有了类似 JavaScript Promise 的异步编程能力。

核心结论
  • 支持链式调用、组合编排、回调处理
  • 支持手动完成和异常处理
  • 类似 JavaScript Promise 的编程模型
  • 是 Java 异步编程的首选
1. 是什么
public class CompletableFuture<T> implements Future<T>, CompletionStage<T> {
    // 创建
    static CompletableFuture<T> supplyAsync(Supplier<T> supplier);
    static CompletableFuture<T> runAsync(Runnable runnable);

    // 链式调用
    <U> CompletableFuture<U> thenApply(Function<T, U> fn);
    CompletableFuture<Void> thenAccept(Consumer<T> action);

    // 组合
    <U,V> CompletableFuture<V> thenCombine(CompletionStage<U> other, BiFunction<T,U,V> fn);
    static CompletableFuture<Void> allOf(CompletableFuture<?>... cfs);
    static CompletableFuture<Object> anyOf(CompletableFuture<?>... cfs);

    // 异常处理
    CompletableFuture<T> exceptionally(Function<Throwable, T> fn);
    CompletableFuture<T> whenComplete(BiConsumer<T, Throwable> action);
}
2. 为什么需要它
  • 传统 Future 只能阻塞获取结果,无法链式处理
  • 多任务组合需要手动编排
  • CompletableFuture 让异步编程更自然、更灵活
3. 底层原理与完整流程

CompletableFuture 内部使用:

  • volatile 状态字段(NEWCOMPLETEDCANCELLED
  • Treiber 栈保存等待者(通过 CAS 操作)
  • 完成时遍历栈触发回调

回调链在 complete() 方法中触发,通过 uniApplybiApply 等方法传播。

4. 怎么使用
// 链式调用
CompletableFuture.supplyAsync(() -> fetchUser())
    .thenApply(user -> user.getOrders())
    .thenAccept(orders -> System.out.println("订单: " + orders))
    .exceptionally(ex -> {
        log.error("异常", ex);
        return null;
    });

// 组合多个任务
CompletableFuture<String> userFuture =
    CompletableFuture.supplyAsync(() -> fetchUser());
CompletableFuture<List> orderFuture =
    CompletableFuture.supplyAsync(() -> fetchOrders());

// 两个都完成后处理
CompletableFuture.allOf(userFuture, orderFuture)
    .thenRun(() -> {
        User user = userFuture.join();
        List orders = orderFuture.join();
        // 处理
    });

// 任意一个完成就处理
CompletableFuture.anyOf(fastApi, slowApi)
    .thenAccept(result -> {
        // 使用最先返回的结果
    });
5. 适用场景
  • 异步 API 调用
  • 并行获取多个独立数据
  • 流式数据处理管道
  • 替代传统的回调地狱
6. 不适用场景与替代方案
  • 不要在简单同步场景中过度使用
  • 不要忘记异常处理
  • 替代方案:RxJava、Reactor
7. 优缺点与技术取舍
维度 Future CompletableFuture
链式 不支持 支持
组合 不支持 支持
回调 不支持 支持
学习曲线 平缓 稍陡
8. 常见问题及解决方案
  • 问题:回调链中的异常丢失

    • 解决:在每个 thenApply 后添加 exceptionallyhandle
  • 问题join() 阻塞在主线程

    • 解决:使用非阻塞方式,通过回调处理结果
9. 版本差异与实现边界
  • Java 8:引入 CompletableFuture
  • Java 9:添加了 orTimeout()completeOnTimeout() 等超时方法
  • Java 12:改进了 exceptionallyCompose 等方法
10. 常见追问
  • 追问CompletableFuture 和 RxJava 的区别?
    • 回答方向:CompletableFuture 是 JDK 标准库,RxJava 是第三方库,功能更强大。
11. 易错点
  • ❌ 错误:thenApply 中的异常会自动传播
  • ✅ 正确:需要通过 exceptionallyhandle 显式处理
一句话总结

CompletableFuture 支持链式调用、组合编排和回调处理,是 Java 异步编程的首选,功能类似 JavaScript Promise。

3.2 线程池(下)—— 任务依赖设计


一个任务依赖另外两个任务才能执行,该怎么设计?

原始问法:

  • 一个任务依赖另外两个任务才能执行,该怎么设计?

来源题目:SRC-03-32-119

面试先答

任务依赖场景有多种实现方式,按复杂度递增:第一,使用 CountDownLatch,前置任务完成后 countDown(),后置任务 await() 等待;第二,使用 CyclicBarrier,所有前置任务在屏障处等待,全部到达后同时放行;第三,使用 CompletableFuture.allOf(),将多个 CompletableFuture 组合,全部完成后触发回调;第四,使用线程池 + 阻塞队列,生产者-消费者模型。推荐使用 CompletableFutureCountDownLatch,代码简洁且功能完善。

核心结论
  • 四种实现方式:CountDownLatchCyclicBarrierCompletableFuture.allOf()、生产者-消费者
  • 推荐 CompletableFutureCountDownLatch
  • 简单一次性等待用 CountDownLatch
  • 需要循环重复用 CyclicBarrier
  • 需要链式组合用 CompletableFuture
1. 是什么

任务依赖是指某个任务(Task C)必须在其他任务(Task A、Task B)都完成后才能开始执行。

2. 为什么需要它
  • 并行任务的后续处理
  • 数据聚合场景(多个数据源全部返回后统一处理)
  • 分阶段并行计算
3. 底层原理与完整流程

方案一:CountDownLatch

CountDownLatch latch = new CountDownLatch(2); // 计数=依赖的任务数

// 前置任务 A
new Thread(() -> {
    doTaskA();
    latch.countDown(); // 完成后 -1
}).start();

// 前置任务 B
new Thread(() -> {
    doTaskB();
    latch.countDown(); // 完成后 -1
}).start();

// 后置任务 C
new Thread(() -> {
    latch.await(); // 等待计数归零
    doTaskC();
}).start();

方案二:CompletableFuture.allOf()

CompletableFuture<Void> futureA = CompletableFuture.runAsync(() -> doTaskA());
CompletableFuture<Void> futureB = CompletableFuture.runAsync(() -> doTaskB());

CompletableFuture.allOf(futureA, futureB)
    .thenRun(() -> doTaskC()); // A 和 B 都完成后执行 C

方案三:CyclicBarrier

CyclicBarrier barrier = new CyclicBarrier(3, () -> {
    doTaskC(); // 屏障触发任务
});

new Thread(() -> {
    doTaskA();
    barrier.await(); // 等待其他参与者
}).start();

new Thread(() -> {
    doTaskB();
    barrier.await();
}).start();

// 参与者 3
barrier.await(); // 主线程也参与
4. 怎么使用

生产环境推荐 CompletableFuture

public class TaskDependencyDemo {

    private final ExecutorService executor =
        Executors.newFixedThreadPool(4);

    public void executeWithDependency() {
        // 并行执行两个前置任务
        CompletableFuture<String> futureA =
            CompletableFuture.supplyAsync(this::fetchDataA, executor);
        CompletableFuture<String> futureB =
            CompletableFuture.supplyAsync(this::fetchDataB, executor);

        // 全部完成后执行后置任务
        CompletableFuture.allOf(futureA, futureB)
            .thenRun(() -> {
                String resultA = futureA.join();
                String resultB = futureB.join();
                processResult(resultA, resultB);
            })
            .exceptionally(ex -> {
                log.error("任务执行异常", ex);
                return null;
            });
    }

    private String fetchDataA() { /* ... */ }
    private String fetchDataB() { /* ... */ }
    private void processResult(String a, String b) { /* ... */ }
}
5. 适用场景
  • 数据聚合:多个 API 调用完成后汇总
  • 并行计算:分块计算后合并结果
  • 初始化流程:多个组件初始化完成后启动服务
6. 不适用场景与替代方案
  • 不要用 Thread.join() 手动实现,太繁琐
  • 不要用 wait/notify 实现,容易出错
  • 替代方案:使用消息队列实现异步解耦
7. 优缺点与技术取舍
方案 优点 缺点
CountDownLatch 简单直接 一次性不可重置
CyclicBarrier 可循环使用 所有参与者必须到位
CompletableFuture 链式组合、异常处理 学习曲线稍高
生产者-消费者 解耦 需额外实现
8. 常见问题及解决方案
  • 问题CountDownLatchcountDown() 在异常中未调用

    • 解决:在 finally 块中调用 countDown()
  • 问题:某个前置任务永久阻塞

    • 解决:使用带超时的 await(timeout)completeOnTimeout()
9. 版本差异与实现边界
  • CountDownLatch:Java 5
  • CyclicBarrier:Java 5
  • CompletableFuture:Java 8
  • CompletableFuture.orTimeout():Java 9+
10. 常见追问
  • 追问CountDownLatchCyclicBarrier 的区别?
    • 回答方向:CountDownLatch 一次性、一个线程等待多个;CyclicBarrier 可循环、多个线程相互等待。
11. 易错点
  • ❌ 错误:CountDownLatch 可以重复使用
  • ✅ 正确:CountDownLatch 是一次性的,需要重用请用 CyclicBarrier
一句话总结

任务依赖有多种实现方案,推荐使用 CompletableFuture.allOf()CountDownLatch,根据是否需要重用和复杂度选择。

3.3 锁机制(上)


什么是死锁?死锁的四个必要条件是什么?

原始问法:

  • 什么是死锁?死锁的四个必要条件是什么?

来源题目:SRC-03-33-120

面试先答

死锁是指两个或多个线程互相持有对方需要的资源,并且都不释放,导致永远阻塞的状态。死锁的四个必要条件(缺一不可):第一,互斥条件——资源一次只能被一个线程使用;第二,占有且等待条件——持有资源同时等待其他资源;第三,不可剥夺条件——已获得的资源不能被强制夺走;第四,循环等待条件——形成等待环路。四个条件同时满足就会发生死锁,破坏任意一个条件即可避免。

核心结论
  • 死锁是多线程互相持有对方所需资源导致的永久阻塞
  • 四个必要条件:互斥、占有且等待、不可剥夺、循环等待
  • 破坏任一条件即可避免死锁
  • 死锁一旦发生无法自动恢复
1. 是什么

死锁(Deadlock):多个线程在执行过程中,因争夺资源而造成的一种互相等待的现象。

线程 A:持有资源 1,等待资源 2
线程 B:持有资源 2,等待资源 1
→ 死锁!
2. 为什么需要它

理解死锁的成因是预防和解决死锁的前提。死锁是并发编程中最严重的问题之一,会导致系统卡死。

3. 底层原理与完整流程

四个必要条件

  1. 互斥条件(Mutual Exclusion):一个资源每次只能被一个线程使用
  2. 占有且等待条件(Hold and Wait):一个线程因请求资源而阻塞时,对已获得的资源保持不放
  3. 不可剥夺条件(No Preemption):线程已获得的资源,在未使用完之前不能强行剥夺
  4. 循环等待条件(Circular Wait):若干线程形成一种头尾相接的循环等待资源关系
4. 怎么使用

死锁示例:

Object lock1 = new Object();
Object lock2 = new Object();

// 线程 A
new Thread(() -> {
    synchronized (lock1) {
        // 持有 lock1,等待 lock2
        synchronized (lock2) {
            // 死锁!
        }
    }
}).start();

// 线程 B
new Thread(() -> {
    synchronized (lock2) {
        // 持有 lock2,等待 lock1
        synchronized (lock1) {
            // 死锁!
        }
    }
}).start();
5. 适用场景

死锁是所有并发系统都可能遇到的问题,必须在设计阶段预防。

6. 不适用场景与替代方案
  • 不要在持有锁的情况下获取其他锁
  • 替代方案:统一加锁顺序、使用 tryLock 超时获取
7. 优缺点与技术取舍

死锁没有任何优点,必须通过设计避免。常见的预防策略:

  • 破坏占有且等待:一次性申请所有资源
  • 破坏不可剥夺:使用 tryLock 超时
  • 破坏循环等待:按固定顺序获取锁
8. 常见问题及解决方案
  • 问题:线上出现死锁
    • 解决:通过 jstack 查看线程栈,找到互相等待的锁
9. 版本差异与实现边界
  • 死锁是 OS 层面的概念,所有版本的 Java 都可能发生
  • JVM 提供了 jstackjcmd Thread.print 等诊断工具
10. 常见追问
  • 追问:如何通过 jstack 检测死锁?
    • 回答方向:jstack <pid> 输出中会明确标注 "Found one Java-level deadlock"。
11. 易错点
  • ❌ 错误:死锁可以通过 JVM 自动检测并恢复
  • ✅ 正确:死锁一旦发生,必须人工介入处理
一句话总结

死锁是四条件同时满足导致的永久阻塞,破坏任一条件可预防,统一加锁顺序是最实用的方案。


如何检测死锁?如何避免死锁?

原始问法:

  • 如何检测死锁?如何避免死锁?

来源题目:SRC-03-33-121

面试先答

死锁检测和避免是两个层面的问题:检测可以通过 jstack 查看线程栈、ThreadMXBean 编程检测、或使用 jcmd Thread.print避免则从设计层面解决,常用方法有四种:第一,统一加锁顺序,所有线程按固定顺序获取锁;第二,一次性申请所有资源;第三,使用 tryLock 超时获取锁,超时释放已持有的锁并重试;第四,使用 Lock 的可中断获取。生产实践中最有效的是统一加锁顺序,从根源消除循环等待。

核心结论
  • 检测方法:jstackThreadMXBeanjcmd
  • 避免方法:统一加锁顺序、一次性申请、超时获取、可中断获取
  • 最实用的是统一加锁顺序
  • 定期审查代码中的嵌套加锁
1. 是什么

死锁检测:运行时发现已经发生的死锁。

死锁避免:设计时预防死锁的发生。

2. 为什么需要它
  • 死锁一旦发生无法自动恢复
  • 检测帮助快速定位问题
  • 避免是根本解决方案
3. 底层原理与完整流程

检测方法

// 编程式检测
ThreadMXBean mxBean = ManagementFactory.getThreadMXBean();
long[] threadIds = mxBean.findDeadlockedThreads();
if (threadIds != null) {
    // 发现死锁
    ThreadInfo[] infos = mxBean.getThreadInfo(threadIds, true, true);
    for (ThreadInfo info : infos) {
        System.out.println(info.getThreadName() +
            " waiting for " + info.getLockName());
    }
}

避免方法一:统一加锁顺序

// 所有线程都按 lock1 → lock2 顺序获取
synchronized (lock1) {
    synchronized (lock2) {
        // 业务逻辑
    }
}

避免方法二:tryLock 超时

ReentrantLock lock1 = new ReentrantLock();
ReentrantLock lock2 = new ReentrantLock();

while (true) {
    if (lock1.tryLock()) {
        try {
            if (lock2.tryLock(1, TimeUnit.SECONDS)) {
                try {
                    // 业务逻辑
                    break;
                } finally {
                    lock2.unlock();
                }
            }
        } finally {
            lock1.unlock();
        }
    }
    // 短暂重试
    Thread.sleep(100);
}
4. 怎么使用

生产环境最佳实践:

// 1. 统一加锁顺序(最重要)
// 所有涉及这两把锁的代码,都按 lockA → lockB 顺序获取

// 2. 使用 tryLock 而非 synchronized
try {
    if (lock.tryLock(5, TimeUnit.SECONDS)) {
        // 业务逻辑
    }
} catch (InterruptedException e) {
    Thread.currentThread().interrupt();
} finally {
    if (lock.isHeldByCurrentThread()) {
        lock.unlock();
    }
}

// 3. 定期审计:避免嵌套锁获取
// 代码审查时重点关注 synchronized 嵌套
5. 适用场景
  • 所有涉及多锁的并发场景
  • 需要可靠防止死锁的系统
6. 不适用场景与替代方案
  • 不要在持有锁时调用外部方法(可能导致嵌套获取)
  • 替代方案:使用 ConcurrentHashMap 分段锁、无锁数据结构
7. 优缺点与技术取舍
方法 优点 缺点
统一加锁顺序 简单可靠 需全团队遵守
一次性申请 彻底避免 可能长时间阻塞
tryLock 超时 灵活 需重试逻辑
可中断获取 可响应中断 代码复杂
8. 常见问题及解决方案
  • 问题:第三方库内部加锁顺序不可控
    • 解决:使用 tryLock 超时策略
9. 版本差异与实现边界
  • ThreadMXBean.findDeadlockedThreads() 从 Java 5 就存在
  • JDK 21 中虚拟线程的死锁检测有所不同
10. 常见追问
  • 追问tryLock 超时后如何处理?
    • 回答方向:释放已持有锁、短暂等待后重试,或向上抛出让调用方处理。
11. 易错点
  • ❌ 错误:synchronized 不会死锁
  • ✅ 正确:synchronized 同样会发生死锁
一句话总结

通过 jstack 检测死锁,通过统一加锁顺序、超时获取等方法避免,统一加锁顺序是最实用的方案。


什么是悲观锁?什么是乐观锁?分别的适用场景?

原始问法:

  • 什么是悲观锁?什么是乐观锁?分别的适用场景?

来源题目:SRC-03-33-122

面试先答

悲观锁认为并发冲突一定会发生,所以每次操作前先加锁(如 synchronizedReentrantLock),适合写多或冲突频繁的场景。乐观锁认为并发冲突不会发生,操作时不加锁,更新时检查数据是否被修改(如 CAS、版本号机制),适合读多写少或冲突少的场景。Java 中 synchronizedReentrantLock 是悲观锁,AtomicIntegerStampedLock、数据库版本号是乐观锁。注意:synchronized 在 JDK 6 后引入了偏向锁和轻量级锁,会在竞争少的时候表现出乐观特性。

核心结论
  • 悲观锁:先加锁再操作,适合写多冲突多的场景
  • 乐观锁:先操作再检查,适合读多冲突少的场景
  • synchronized/ReentrantLock 是悲观锁
  • CAS/版本号/时间戳是乐观锁实现
1. 是什么

悲观锁(Pessimistic Locking):假设冲突必然发生,每次操作前先获取锁。

乐观锁(Optimistic Locking):假设冲突不会发生,操作时不加锁,更新时检查是否被修改。

2. 为什么需要它
  • 悲观锁保证数据一致性,但开销大
  • 乐观锁无阻塞,吞吐量高,但冲突时需重试
3. 底层原理与完整流程

悲观锁流程

获取锁 → 操作数据 → 释放锁

乐观锁流程

读取数据(带版本号)→ 操作数据 → 检查版本号是否变化 → 更新或重试
4. 怎么使用
// 悲观锁:synchronized
public synchronized void update() {
    count++; // 加锁后操作
}

// 乐观锁:AtomicInteger
AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet(); // CAS 实现,乐观锁

// 乐观锁:版本号(数据库)
// UPDATE table SET value=new, version=version+1
// WHERE id=? AND version=old_version
5. 适用场景
  • 悲观锁适用:写操作多、冲突频繁、一致性要求高
    • 如:金融交易、库存扣减
  • 乐观锁适用:读多写少、冲突少、容忍重试
    • 如:统计计数、配置更新
6. 不适用场景与替代方案
  • 不要在高冲突场景使用乐观锁(大量重试反而更慢)
  • 不要在低冲突场景全用悲观锁(不必要的开销)
  • 替代方案:StampedLock 可在悲观和乐观之间切换
7. 优缺点与技术取舍
维度 悲观锁 乐观锁
吞吐量
一致性 需重试
冲突处理 阻塞等待 失败重试
实现复杂度 简单 稍复杂
8. 常见问题及解决方案
  • 问题:乐观锁大量冲突导致高 CPU
    • 解决:改用悲观锁,或评估冲突根源
9. 版本差异与实现边界
  • synchronized 在 JDK 6 引入锁升级(偏向锁→轻量锁→重量锁),低竞争时表现乐观特性
  • StampedLock(JDK 8)支持乐观读模式
10. 常见追问
  • 追问synchronized 是乐观锁还是悲观锁?
    • 回答方向:本质是悲观锁,但锁升级过程中会有偏向锁和轻量级锁的优化,低竞争时接近乐观锁性能。
11. 易错点
  • ❌ 错误:乐观锁一定比悲观锁快
  • ✅ 正确:冲突多时乐观锁重试开销更大
一句话总结

悲观锁先加锁操作、适合高冲突;乐观锁先操作检查、适合低冲突,根据场景选择锁策略。


如何实现乐观锁?

原始问法:

  • 如何实现乐观锁?

来源题目:SRC-03-33-123

面试先答

乐观锁的实现方式主要有三种:第一,CAS(Compare-And-Swap),CPU 指令级别的原子操作,Java 原子类基于此实现;第二,版本号机制,读取数据时获取版本号,更新时比较版本号是否变化,广泛用于数据库;第三,时间戳机制,类似版本号但用时间戳比较。核心思想都是"先操作后检查"——不加锁执行操作,提交时判断数据是否被其他线程修改过,若被修改则放弃并重试。Java 中 AtomicIntegerLongAdderStampedLock 的乐观读模式都是乐观锁的实现。

核心结论
  • 三种实现:CAS、版本号、时间戳
  • 核心思想:先操作后检查
  • CAS 是最常用的乐观锁实现
  • 冲突时需重试
1. 是什么

CAS(Compare-And-Swap):CPU 级别的原子指令,比较内存值与期望值,相同则修改。

版本号:数据中增加 version 字段,更新时比较。

2. 为什么需要它
  • 避免悲观锁的阻塞开销
  • 提高系统吞吐量
  • 适合读多写少场景
3. 底层原理与完整流程

CAS 流程

1. 读取内存值 V
2. 比较 V 与预期值 A
3. 如果 V == A,则写入新值 B → 成功
4. 如果 V != A,则重试 → 失败

版本号流程

1. 读取数据和版本号 version=1
2. 修改数据
3. UPDATE ... SET value=?, version=2 WHERE id=? AND version=1
4. 如果影响行数=1 → 成功
5. 如果影响行数=0 → 重试(版本号已变化)
4. 怎么使用
// CAS 实现:Java 原子类
AtomicInteger count = new AtomicInteger(0);
int oldValue = count.get();
int newValue = oldValue + 1;
boolean success = count.compareAndSet(oldValue, newValue);

// 版本号实现(伪代码)
@Entity
public class Product {
    private Long id;
    private int stock;
    @Version
    private Long version; // JPA 乐观锁版本号
}

// 数据库版本号
UPDATE product SET stock = stock - 1, version = version + 1
WHERE id = 1 AND version = 100;
5. 适用场景
  • CAS:计数器、状态标志、无锁数据结构
  • 版本号:数据库并发更新、订单状态变更
6. 不适用场景与替代方案
  • 不要在高冲突场景使用乐观锁
  • 替代方案:使用分布式锁(Redis)、悲观锁
7. 优缺点与技术取舍
实现方式 优点 缺点
CAS CPU 级原子,高性能 ABA 问题、自旋开销
版本号 简单直观 需数据库支持
时间戳 解决 ABA 时间精度问题
8. 常见问题及解决方案
  • 问题:CAS 自旋导致 CPU 高
    • 解决:使用 LongAdder 分拆累加、添加退避策略
9. 版本差异与实现边界
  • CAS 在所有现代 CPU 上支持(x86: CAS 指令,ARM: LDREX/STREX)
  • Java 原子类从 Java 5 开始基于 CAS 实现
  • JDK 8 的 LongAdder 优化了高并发下的 CAS 自旋问题
10. 常见追问
  • 追问:CAS 如何解决 ABA 问题?
    • 回答方向:使用 AtomicStampedReference 添加时间戳或版本号。
11. 易错点
  • ❌ 错误:CAS 不会失败
  • ✅ 正确:CAS 在并发冲突时会失败,需要重试
一句话总结

乐观锁通过 CAS 或版本号实现"先操作后检查",适合读多写少场景,冲突时重试保证最终一致性。


CAS 的原理、定义、解决方案是什么?

原始问法:

  • 什么是 CAS?CAS 的原理是什么?
  • CAS 的 ABA 问题是什么?如何解决?

来源题目:SRC-03-33-124, SRC-03-33-125

面试先答

CAS(Compare-And-Swap,比较并交换)是 CPU 级别的原子操作指令。它包含三个操作数:内存位置 V、预期旧值 A、新值 B。只有当 V == A 时,才将 V 更新为 B,整个操作是原子的。CAS 是 Java 原子类、synchronized(轻量级锁)、ReentrantLock 的底层实现。CAS 的 ABA 问题是指值从 A→B→A,CAS 误认为没有变化,解决方案是添加版本号(AtomicStampedReference)。CAS 的其他局限包括:自旋开销、只能保证单个变量原子性。

核心结论
  • CAS 是 CPU 原子指令,比较并交换
  • 三个操作数:内存值 V、预期值 A、新值 B
  • ABA 问题:值 A→B→A,CAS 无法检测
  • ABA 解决方案:AtomicStampedReference(添加版本号)
  • 是原子类和锁的底层实现
1. 是什么

CAS(Compare-And-Swap):一条 CPU 原子指令,实现比较-交换的原子操作。

CAS(V, A, B):
    if V == A:
        V = B
        return true
    else:
        return false
2. 为什么需要它
  • 实现无锁(Lock-Free)并发数据结构
  • 比互斥锁性能更高
  • 是 Java 原子类的底层实现
3. 底层原理与完整流程

CPU 层面

  • x86:CMPXCHG 指令(带 LOCK 前缀保证原子性)
  • ARM:LDREX(独占读取)+ STREX(独占写入)

Java 层面

// AtomicInteger.compareAndSet() 源码
public final boolean compareAndSet(int expect, int update) {
    return U.compareAndSwapInt(this, VALUE, expect, update);
}
// U = Unsafe,直接调用 native 方法

ABA 问题

线程 1: 读取 value = A
线程 2: 将 A → B
线程 3: 将 B → A
线程 1: CAS(A, B) → 成功!(但值已经被修改过了)
4. 怎么使用
// 基本 CAS
AtomicInteger counter = new AtomicInteger(0);
counter.compareAndSet(0, 1); // CAS

// 解决 ABA 问题
AtomicStampedReference<String> stampedRef =
    new AtomicStampedReference<>("A", 0);

String oldRef = stampedRef.getReference();
int oldStamp = stampedRef.getStamp();

// 更新(带版本号)
stampedRef.compareAndSet(oldRef, "B", oldStamp, oldStamp + 1);

// LongAdder 优化高并发
LongAdder adder = new LongAdder();
adder.increment(); // 分拆累加,减少自旋
5. 适用场景
  • 原子计数器(AtomicIntegerLongAdder
  • 状态标志位(AtomicBoolean
  • 无锁队列(ConcurrentLinkedQueue
6. 不适用场景与替代方案
  • 不要用 CAS 做多变量原子操作,改用锁
  • 高并发下自旋开销大,改用 LongAdder
  • ABA 敏感场景使用 AtomicStampedReference
7. 优缺点与技术取舍
维度 CAS 互斥锁
性能 高(无阻塞) 中(阻塞唤醒)
原子性 单变量 多变量
ABA 问题
复杂度 需处理失败 自动处理
8. 常见问题及解决方案
  • 问题:CAS 自旋导致 CPU 使用率高

    • 解决:使用 LongAdder、添加退避(Thread.yield())、限制自旋次数
  • 问题:ABA 问题

    • 解决:使用 AtomicStampedReference 添加版本号
9. 版本差异与实现边界
  • CAS 是 CPU 指令,与 JDK 版本无关
  • Java 5:原子类基于 CAS 实现
  • JDK 8:LongAdder 优化高并发 CAS 自旋
  • AtomicStampedReference 从 Java 5 存在
10. 常见追问
  • 追问LongAdder 为什么比 AtomicInteger 快?
    • 回答方向:LongAdder 将单一 value 分散到多个 cell,减少 CAS 竞争。
11. 易错点
  • ❌ 错误:CAS 能保证复合操作原子性

  • ✅ 正确:CAS 只能保证单个变量的原子性

  • ❌ 错误:CAS 一定比锁快

  • ✅ 正确:低冲突时 CAS 快,高冲突时锁可能更快

一句话总结

CAS 是 CPU 级原子比较交换指令,是原子类的底层实现,存在 ABA 问题和自旋开销,通过 AtomicStampedReferenceLongAdder 优化。


CAS 有哪些局限性?

原始问法:

  • CAS有哪些局限性?

来源题目:SRC-03-33-126

面试先答

CAS 有四大局限性:第一,ABA 问题——值从 A→B→A,CAS 误认为没有变化,解决方案是 AtomicStampedReference;第二,自旋时间过长——高并发下 CAS 反复失败导致 CPU 空转,解决方案是 LongAdder 分拆或添加退避策略;第三,只能保证单个变量的原子性——无法同时原子操作多个变量,解决方案是使用锁或 AtomicReference 包装多个变量;第四,CPU 指令顺序——CAS 依赖 CPU 缓存一致性协议,在某些架构上可能需要显式内存屏障。

核心结论
  • 四大局限性:ABA 问题、自旋开销、单变量限制、指令顺序
  • ABA 用 AtomicStampedReference 解决
  • 自旋开销用 LongAdder 或退避策略解决
  • 多变量用锁或包装解决
1. 是什么

CAS 局限性的具体表现:

局限 原因 影响
ABA 问题 值变化后回到原值 无法检测修改
自旋开销 高并发下反复失败 CPU 高占用
单变量限制 一条指令只能操作一个值 无法复合操作
指令顺序 CPU 可能重排指令 可见性问题
2. 为什么需要它

理解 CAS 的局限性有助于在实际开发中正确选择并发方案。

3. 底层原理与完整流程

ABA 问题流程

初始: value = A
T1 读 value = A
T2: CAS(A, B) → 成功
T3: CAS(B, A) → 成功
T1: CAS(A, X) → 成功(但值已经被修改过!)

自旋开销

// 高并发下的自旋
while (!counter.compareAndSet(oldValue, oldValue + 1)) {
    // 反复失败,CPU 空转
    // 可能需要添加退避
    Thread.yield(); // 或 LockSupport.parkNanos(1)
}
4. 怎么使用
// 解决 ABA 问题
AtomicStampedReference<Integer> atomic =
    new AtomicStampedReference<>(0, 0);
int stamp = atomic.getStamp();
// CAS 时同时比较值和版本号
atomic.compareAndSet(0, 1, stamp, stamp + 1);

// 解决自旋开销
LongAdder adder = new LongAdder();
adder.increment(); // 内部使用分段 CAS

// 解决多变量问题
AtomicReference<Point> pointRef = new AtomicReference<>();
pointRef.set(new Point(0, 0));
// 一次性替换整个对象
pointRef.compareAndSet(oldPoint, newPoint);
5. 适用场景
  • 低冲突场景下 CAS 性能最优
  • 高冲突场景考虑使用 LongAdder 或锁
6. 不适用场景与替代方案
  • 不要在高冲突场景下使用单一 CAS
  • 不要用 CAS 实现多变量同步
  • 替代方案:LongAdderReentrantLocksynchronized
7. 优缺点与技术取舍

CAS 在低冲突下性能优异,但需要处理其局限性。在 Java 8+ 中,LongAdderStampedLock 提供了更好的选择。

8. 常见问题及解决方案
  • 问题:CAS 导致 CPU 100%
    • 解决:使用 LongAdder、添加 Thread.yield() 退避、限制重试次数
9. 版本差异与实现边界
  • 所有 JDK 版本都存在这些局限性
  • JDK 8 的 LongAdderStampedLock 部分解决了自旋开销问题
10. 常见追问
  • 追问AtomicStampedReference 的 stamp 是什么?
    • 回答方向:一个 int 类型的版本号,每次更新递增,用于检测 ABA 问题。
11. 易错点
  • ❌ 错误:CAS 可以完全替代锁
  • ✅ 正确:CAS 有 ABA、自旋、单变量等局限
一句话总结

CAS 有 ABA、自旋、单变量、指令顺序四大局限,通过 AtomicStampedReferenceLongAdder 等方案弥补。


synchronized 的定义、使用方法、原理是什么?

原始问法:

  • synchronized关键字的作用是什么?怎么使用?
  • synchronized的底层原理是什么?
  • synchronized的锁升级过程是什么?

来源题目:SRC-03-33-127, SRC-03-33-129, SRC-03-33-130

面试先答

synchronized 是 Java 内置的关键字,用于实现线程同步,保证原子性、可见性和有序性。使用方式有三种:修饰实例方法(锁对象是 this)、修饰静态方法(锁对象是 Class 对象)、修饰代码块(锁对象是指定的任意对象)。底层原理:每个对象关联一个 monitor(监视器),通过 monitorenter/monitorexit 字节码指令实现加锁解锁。JDK 6 引入了锁优化:偏向锁→轻量级锁→重量级锁的升级过程,在低竞争下无需 OS 介入即可实现同步。

核心结论
  • synchronized 保证原子性、可见性、有序性
  • 三种使用方式:实例方法、静态方法、代码块
  • 底层基于对象监视器(monitor)+ 字节码指令
  • 锁升级:偏向锁 → 轻量级锁 → 重量级锁
  • JDK 6 后有锁升级优化,性能显著提升
1. 是什么

synchronized:Java 内置的互斥锁关键字,确保同一时刻只有一个线程执行被保护的代码。

2. 为什么需要它
  • 保证多线程环境下数据的一致性
  • 提供 JVM 级别的同步原语
  • 相比 Lock 无需手动管理,更安全
3. 底层原理与完整流程

三种使用方式

// 1. 实例方法(锁 this)
public synchronized void method() {
    // 临界区
}

// 2. 静态方法(锁 Class 对象)
public static synchronized void staticMethod() {
    // 临界区
}

// 3. 代码块(锁任意对象)
Object lock = new Object();
synchronized (lock) {
    // 临界区
}

锁升级过程

无锁 → 偏向锁 → 轻量级锁 → 重量级锁
  ↑         ↑          ↑          ↑
  |  线程再入  竞争检测   竞争升级
  |
  └──────────────────────────────
           不可降级(G1 后可批量偏向)
  • 偏向锁:同一线程多次获取锁,只需检测线程 ID,无需 CAS
  • 轻量级锁:不同线程竞争,通过 CAS 交换锁指针
  • 重量级锁:CAS 失败,升级为 OS 互斥量,阻塞线程

对象头存储

Mark Word: [锁状态(2bit) | 线程ID/指向锁记录的指针 | 哈希码 | GC年龄]
4. 怎么使用
public class Counter {
    private int count = 0;

    // 方式一:修饰实例方法
    public synchronized void increment() {
        count++;
    }

    // 方式二:修饰代码块(更灵活)
    public void incrementBlock() {
        synchronized (this) {
            count++;
        }
    }

    // 方式三:修饰静态方法
    public static synchronized void staticMethod() {
        // 锁 Counter.class
    }
}
5. 适用场景
  • 简单同步需求:优先使用 synchronized
  • 方法级同步:修饰方法最简洁
  • 需要 JVM 自动管理锁生命周期
6. 不适用场景与替代方案
  • 不要在持有锁时调用外部方法
  • 不要用 synchronized 做长时间操作
  • 替代方案:ReentrantLock(更灵活)、ReadWriteLock(读写分离)
7. 优缺点与技术取舍
维度 synchronized ReentrantLock
自动解锁 否(需 finally)
可中断
公平锁 不支持 支持
多条件 不支持 支持
锁升级 支持 不支持
8. 常见问题及解决方案
  • 问题synchronized 锁不住的情况

    • 解决:检查是否锁了正确的对象(实例方法锁 this,静态方法锁 Class)
  • 问题:锁升级导致性能抖动

    • 解决:通过 -XX:-UseBiasedLocking 关闭偏向锁或调整阈值
9. 版本差异与实现边界
  • JDK 6 之前:只有重量级锁,性能差
  • JDK 6:引入偏向锁和轻量级锁(锁升级)
  • JDK 8:默认开启偏向锁
  • JDK 15:废弃偏向锁(批量偏向保留)
  • JVM 实现细节:HotSpot 中偏向锁的撤销和升级策略
10. 常见追问
  • 追问synchronizedReentrantLock 怎么选?
    • 回答方向:简单同步用 synchronized,需要灵活控制(可中断、公平锁、多条件)用 ReentrantLock
11. 易错点
  • ❌ 错误:synchronized 方法锁的是方法

  • ✅ 正确:锁的是对象(this 或 Class)

  • ❌ 错误:synchronized 不会死锁

  • ✅ 正确:synchronized 同样会死锁

一句话总结

synchronized 基于对象监视器实现,通过锁升级优化性能,保证原子性、可见性和有序性,是最常用的同步关键字。


构造方法能加 synchronized 吗?

原始问法:

  • 构造方法能加synchronized吗?

来源题目:SRC-03-33-128

面试先答

构造方法可以加 synchronized,但通常没有必要。因为构造方法是在对象创建时调用,synchronized 修饰构造方法锁的是正在创建的对象(this),而对象尚未完全构造完毕,其他线程无法访问该对象(除非通过引用逃逸)。实际上 synchronized 修饰构造方法极少使用,更多是用在普通方法上。从语法上看,synchronized 关键字可以用于任何方法包括构造方法,但 JVM 规范中对构造方法有特殊处理。

核心结论
  • 语法上可以加,但没有实际意义
  • 构造方法锁的是正在创建的对象
  • 通常没有必要,因为对象尚未对外暴露
  • 线程安全更多通过 final 或内部同步保证
1. 是什么
public class Demo {
    public synchronized Demo() {
        // 可以编译通过,但极少使用
    }
}
2. 为什么需要它

实际开发中几乎不会在构造方法上使用 synchronized,因为:

  • 对象创建过程中只有一个线程能持有引用
  • 构造方法执行完毕后才会将对象暴露给外部
3. 底层原理与完整流程

synchronized 修饰构造方法时:

  1. JVM 在方法上添加 ACC_SYNCHRONIZED 标志
  2. 调用时获取对象监视器(monitor)
  3. 构造方法执行完毕后释放 monitor
  4. 与普通 synchronized 方法机制相同
4. 怎么使用

通常不需要在构造方法上加 synchronized,如果需要在构造过程中保护共享资源,应使用其他锁:

public class SafeInit {
    private static final Object INIT_LOCK = new Object();

    public SafeInit() {
        synchronized (INIT_LOCK) {
            // 保护初始化过程
        }
    }
}
5. 适用场景
  • 理论上可行,实际极少使用
6. 不适用场景与替代方案
  • 不要依赖构造方法的 synchronized 保证线程安全
  • 替代方案:构造时使用静态锁或在方法内部同步
7. 优缺点与技术取舍

没有实际优点,反而可能误导。

8. 常见问题及解决方案
  • 问题:构造方法加 synchronized 导致死锁
    • 解决:避免在构造方法中加锁,不要在持有锁时调用外部方法
9. 版本差异与实现边界
  • 所有版本的 Java 都支持在构造方法上加 synchronized
  • 实际使用中极为罕见
10. 常见追问
  • 追问:为什么构造方法不需要 synchronized
    • 回答方向:因为对象在构造完成前不会被其他线程访问(除非引用逃逸)。
11. 易错点
  • ❌ 错误:构造方法加 synchronized 没有意义
  • ✅ 正确:语法上可以加,但通常不需要
一句话总结

构造方法可以加 synchronized,但因对象未对外暴露通常没有必要,实际开发中极少使用。


volatile 关键字的作用是什么?

原始问法:

  • volatile关键字的作用是什么?

来源题目:SRC-03-33-131

面试先答

volatile 关键字有两大作用:第一,保证可见性——一个线程修改了 volatile 变量的值,其他线程能立即看到最新值,通过强制主内存读写实现;第二,禁止指令重排序——编译器和 CPU 不得对 volatile 变量相关的指令进行重排序优化。注意:volatile 不能保证原子性,如 volatile int count; count++ 不是原子操作(读取-修改-写入三步)。volatile 是最轻量的同步机制,适合状态标志位、双重检查锁单例等场景。

核心结论
  • 两大作用:保证可见性、禁止指令重排序
  • 不保证原子性
  • 最轻量的同步机制
  • 适合状态标志位和 DCL 单例
1. 是什么

volatile:Java 内存模型(JMM)提供的轻量级同步关键字。

volatile boolean running = true;
volatile int count = 0;
2. 为什么需要它
  • 多线程间需要实时感知状态变化
  • 防止指令重排序导致的程序错误
  • 比锁更轻量,无需上下文切换
3. 底层原理与完整流程

可见性实现

  • 写操作:强制刷新到主内存
  • 读操作:强制从主内存读取(不用缓存)

禁止重排序

  • volatile 写之前插入 StoreStore 屏障
  • volatile 写之后插入 StoreLoad 屏障
  • volatile 读之前插入 LoadLoad 屏障
  • volatile 读之后插入 LoadStore 屏障
4. 怎么使用
// 状态标志位
public class Worker implements Runnable {
    private volatile boolean running = true;

    @Override
    public void run() {
        while (running) {
            doWork();
        }
    }

    public void stop() {
        running = false; // 其他线程立即可见
    }
}

// 双重检查锁单例
public class Singleton {
    private static volatile Singleton instance; // 必须 volatile!

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}
5. 适用场景
  • 状态标志位(如 runninginitialized
  • 双重检查锁单例
  • 配置数据的安全发布
6. 不适用场景与替代方案
  • 不要用于复合操作(count++),改用 AtomicInteger
  • 不要作为锁的替代品
  • 替代方案:AtomicIntegersynchronizedReentrantLock
7. 优缺点与技术取舍
维度 volatile synchronized
可见性 保证 保证
原子性 不保证 保证
有序性 保证 保证
性能
阻塞
8. 常见问题及解决方案
  • 问题volatile 变量 count++ 不是线程安全的
    • 解决:改用 AtomicInteger.incrementAndGet()
9. 版本差异与实现边界
  • 从 Java 1.2 引入(准确说是 1.1?实际规范从 Java 1.0 就有)
  • JDK 5 之后 volatile 的语义被加强(完全建立在 happens-before 原则上)
  • JDK 9+ 中 VarHandle 提供了更灵活的 volatile 访问
10. 常见追问
  • 追问:为什么 DCL 单例中 instance 必须用 volatile
    • 回答方向:防止指令重排序(对象创建分为分配内存、初始化、引用赋值三步,重排序可能导致其他线程看到未初始化的对象)。
11. 易错点
  • ❌ 错误:volatile 保证原子性
  • ✅ 正确:volatile 只保证可见性和有序性,不保证原子性
一句话总结

volatile 保证可见性和禁止重排序,不保证原子性,是最轻量的同步机制,适合状态标志位和 DCL 单例。


volatile 如何保证可见性和禁止指令重排序?

原始问法:

  • volatile如何保证可见性和禁止指令重排序?

来源题目:SRC-03-33-132

面试先答

volatile 通过 JMM 的内存屏障(Memory Barrier) 实现可见性和有序性。可见性volatile 写时通过 Store 屏障强制将缓存数据刷新到主内存,volatile 读时通过 Load 屏障强制从主内存读取,绕过 CPU 缓存,保证所有线程看到的值一致。禁止重排序:编译器在 volatile 写之前插入 StoreStore 屏障(禁止写-写重排),写之后插入 StoreLoad 屏障(禁止写-读重排);在 volatile 读之前插入 LoadLoad 屏障(禁止读-读重排),读之后插入 LoadStore 屏障(禁止读-写重排)。内存屏障同时具有刷新缓存的效果。

核心结论
  • 可见性:强制主内存读写(Store/Load 屏障)
  • 有序性:插入内存屏障禁止重排序
  • 内存屏障同时提供可见性和有序性保证
  • 底层通过 CPU 指令实现(x86 的 MFENCE、ARM 的 DMB)
1. 是什么

内存屏障(Memory Barrier):一种 CPU 指令,保证屏障前后的指令执行顺序和内存可见性。

四种内存屏障:

  • LoadLoad:保证屏障前的读在屏障后的读之前完成
  • StoreStore:保证屏障前的写在屏障后的写之前完成
  • LoadStore:保证屏障前的读在屏障后的写之前完成
  • StoreLoad:保证屏障前的写在屏障后的读之前完成
2. 为什么需要它
  • CPU 执行指令时可能重排序以提高性能
  • CPU 缓存导致不同线程看到的值不一致
  • 内存屏障强制 CPU 按顺序执行并刷新缓存
3. 底层原理与完整流程

volatile 写的屏障插入

... 写操作 1 → StoreStore → volatile 写 → StoreLoad → ... 读操作 2
  • StoreStore:保证前面的写不会被重排到后面
  • StoreLoad:保证后面的读不会被重排到前面(同时刷新缓存)

volatile 读的屏障插入

... 写操作 1 → LoadLoad → volatile 读 → LoadStore → ... 写操作 2
  • LoadLoad:保证前面的读不会被重排到后面
  • LoadStore:保证后面的写不会被重排到前面

CPU 实现

  • x86:MFENCE(全屏障)、SFENCE(Store 屏障)、LFENCE(Load 屏障)
  • ARM:DMB(Data Memory Barrier)、DSB(Data Synchronization Barrier)
4. 怎么使用
// volatile 写 - 隐含 StoreStore + StoreLoad
volatileFlag = true;

// volatile 读 - 隐含 LoadLoad + LoadStore
if (volatileFlag) {
    // ...
}

// 查看字节码中的屏障
// javap -c Demo.class
// 会看到 volatile 变量的读写带有特定的屏障指令

// VarHandle(JDK 9+)
VarHandle handle = MethodHandles.lookup()
    .in(MyClass.class)
    .findVarHandle(MyClass.class, "counter", int.class)
    .getVarHandle();
handle.setVolatile(this, 10); // volatile 写
int val = handle.getVolatile(this); // volatile 读
5. 适用场景
  • 所有使用 volatile 的场景都依赖这些机制
  • 理解 JMM 的基础
6. 不适用场景与替代方案
  • 不要手动添加内存屏障,JMM 已自动处理
  • 替代方案:synchronized(通过 monitor 实现可见性和有序性)
7. 优缺点与技术取舍

内存屏障提供了强可见性和有序性保证,但会带来一定的性能开销(阻止 CPU 优化、强制缓存刷新)。

8. 常见问题及解决方案
  • 问题volatile 变量性能不如普通变量
    • 解决:理解这是为了可见性和有序性的必要开销
9. 版本差异与实现边界
  • JDK 5+ 中 volatile 的语义完全基于 happens-before
  • x86 架构的 CPU 缓存一致性协议(MESI)在一定程度上自动处理可见性
  • ARM 架构更严格,需要显式屏障
10. 常见追问
  • 追问:x86 架构下 volatile 的性能为什么比 ARM 好?
    • 回答方向:x86 采用强内存模型(TSO),StoreLoad 屏障默认存在;ARM 采用弱内存模型,需要更多屏障。
11. 易错点
  • ❌ 错误:volatile 可以完全替代 synchronized
  • ✅ 正确:volatile 不保证原子性,无法替代锁
一句话总结

volatile 通过内存屏障实现可见性(强制主内存读写)和有序性(禁止指令重排序),是 JMM 轻量级同步的核心机制。

3.3 锁机制(下)


为什么 volatile 不能保证原子性?

原始问法:

  • 为什么volatile不能保证原子性?

来源题目:SRC-03-33-133

面试先答

volatile 不能保证原子性的核心原因是:volatile 的写操作虽然对单个变量的读写具有原子性(CPU 层面),但复合操作(如 count++)不是原子的。count++ 实际包含三个步骤:读取 count 的值(读)、加 1(计算)、写回 count(写)。在多线程环境下,一个线程在读和写之间,另一个线程可能已经修改了 count,导致丢失更新。volatile 只保证可见性和有序性,不保证读-改-写操作的原子性。要保证原子性需要使用 synchronizedAtomicInteger 等。

核心结论
  • volatile 不保证复合操作(读-改-写)的原子性
  • count++ 是三个步骤,中间可能被其他线程打断
  • 单个变量的读写在 CPU 层面通常是原子的(对齐情况下)
  • 需要原子性用 AtomicIntegersynchronized
1. 是什么

原子性:一个操作或者多个操作,要么全部执行且不被中断,要么就不执行。

count++ 的三个步骤:

步骤1: 读取 count 的值 → 如 0
步骤2: 将值加 1        → 得到 1
步骤3: 写回 count       → 设置为 1
2. 为什么需要它

理解 volatile 的局限性,正确选择同步机制。

3. 底层原理与完整流程

非原子操作示例

线程 A: 读 count(0) → 计算加1(1) → 写 count(1)
线程 B: 读 count(0) → 计算加1(1) → 写 count(1)
结果: count = 1(丢失了一次更新!预期应该是 2)

CPU 层面

  • 单个 volatile 变量的读写在对齐情况下是原子的(x86 保证)
  • 但读-改-写需要多次 CPU 操作,不是原子的
  • volatile 不提供 CAS 语义
4. 怎么使用
// 错误:volatile 不保证原子性
public class WrongCounter {
    private volatile int count = 0;

    public void increment() {
        count++; // 非原子操作!
    }
}

// 正确:使用 AtomicInteger
public class RightCounter {
    private final AtomicInteger count = new AtomicInteger(0);

    public void increment() {
        count.incrementAndGet(); // CAS 原子操作
    }
}

// 正确:使用 synchronized
public class SyncCounter {
    private int count = 0;

    public synchronized void increment() {
        count++; // 整个方法是原子的
    }
}
5. 适用场景
  • volatile:状态标志位(只读或单一写)
  • AtomicInteger:计数器、累加器
  • synchronized:复合操作、多变量同步
6. 不适用场景与替代方案
  • 不要用 volatile 做计数器
  • 不要用 volatile 做任何读-改-写操作
  • 替代方案:原子类、synchronizedLock
7. 优缺点与技术取舍
方案 原子性 性能 复杂度
volatile 仅可见性 最高
AtomicInteger 复合操作
synchronized 方法级
8. 常见问题及解决方案
  • 问题volatile 变量的 ++ 操作在单线程下没问题,多线程下出错
    • 解决:使用 AtomicInteger.incrementAndGet()
9. 版本差异与实现边界
  • volatile 的原子性局限与 JDK 版本无关
  • VarHandle(JDK 9+)提供了与 AtomicInteger 类似的功能
10. 常见追问
  • 追问:为什么 volatile 单个变量的读写是原子的?
    • 回答方向:x86 CPU 对齐的内存访问是原子的,volatile 保证了对齐。
11. 易错点
  • ❌ 错误:volatile 保证 count++ 的原子性
  • ✅ 正确:count++ 是读-改-写复合操作,volatile 不保证原子性
一句话总结

volatile 不保证原子性因为复合操作(读-改-写)可能被其他线程打断,需要用原子类或锁保证原子性。


volatile 和 synchronized 的区别是什么?ReentrantLock 是什么?和 synchronized 的区别是什么?

原始问法:

  • volatile和synchronized的区别是什么?
  • ReentrantLock是什么?和synchronized的区别是什么?

来源题目:SRC-03-33-134, SRC-03-33-135

面试先答

volatilesynchronized 的核心区别:volatile 是关键字、轻量级、保证可见性和有序性但不保证原子性;synchronized 是关键字、重量级(有锁升级优化)、保证可见性、有序性和原子性。ReentrantLock 是 JUC 包的类、基于 AQS 实现、功能比 synchronized 更灵活——支持可中断获取、超时获取、公平锁、多条件队列,但需要手动管理锁(finally unlock)。选择建议:简单同步优先 synchronized,需要灵活控制选 ReentrantLock

核心结论
  • volatile:可见性+有序性,不保证原子性
  • synchronized:可见性+有序性+原子性,自动管理
  • ReentrantLock:更灵活,需手动管理
  • 选择原则:简单用 synchronized,复杂用 ReentrantLock
1. 是什么

三者对比

维度 volatile synchronized ReentrantLock
原子性 不保证 保证 保证
可见性 保证 保证 保证
有序性 保证 保证 保证
锁释放 无需释放 自动释放 手动释放
可中断 不支持 支持
超时获取 不支持 支持
公平锁 不支持 支持
多条件 不支持 支持
类型 关键字 关键字
2. 为什么需要它
  • volatile:最轻量同步,适合状态标志
  • synchronized:自动管理锁,简单安全
  • ReentrantLock:灵活控制,适合复杂同步
3. 底层原理与完整流程

synchronized

  • 基于对象监视器(monitor)
  • 锁升级:偏向锁 → 轻量级锁 → 重量级锁
  • JVM 自动管理锁获取和释放

ReentrantLock

  • 基于 AQS(AbstractQueuedSynchronizer)
  • 使用 CAS 修改状态变量
  • 可中断:lockInterruptibly()
  • 超时:tryLock(timeout)
  • 公平锁:按等待顺序获取
4. 怎么使用
// volatile 使用
private volatile boolean flag = false;

// synchronized 使用
public synchronized void method() {
    // 自动获取和释放锁
}

// ReentrantLock 使用
private final ReentrantLock lock = new ReentrantLock();
private final Condition condition = lock.newCondition();

public void method() {
    lock.lock();
    try {
        // 业务逻辑
        condition.await(); // 等待条件
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    } finally {
        lock.unlock(); // 必须在 finally 中释放
    }
}

// 可中断获取
try {
    lock.lockInterruptibly();
} catch (InterruptedException e) {
    // 处理中断
}
5. 适用场景
  • volatile:状态标志、DCL 单例
  • synchronized:简单方法同步、代码块同步
  • ReentrantLock:需要灵活控制的复杂同步
6. 不适用场景与替代方案
  • 不要用 volatile 做原子操作
  • 不要忘记 ReentrantLock.unlock()(在 finally 中)
  • 不要过度使用 ReentrantLock,简单场景用 synchronized
7. 优缺点与技术取舍

synchronized 自动管理、简单安全;ReentrantLock 灵活但需手动管理。JDK 6 后 synchronized 性能已接近 ReentrantLock

8. 常见问题及解决方案
  • 问题ReentrantLock 忘记 unlock()

    • 解决:必须在 finally 块中 unlock()
  • 问题synchronized 无法实现公平锁

    • 解决:使用 new ReentrantLock(true) 实现公平锁
9. 版本差异与实现边界
  • volatile:Java 1.0+
  • synchronized:Java 1.0+
  • ReentrantLock:Java 5+(JUC 包)
  • JDK 6 后 synchronized 性能大幅提升(锁升级)
  • JDK 8 中两者性能基本持平
10. 常见追问
  • 追问synchronized 为什么比 ReentrantLock 使用更广?
    • 回答方向:自动管理、不会忘记释放、代码更简洁、JVM 优化持续改进。
11. 易错点
  • ❌ 错误:volatile 可以替代 synchronized

  • ✅ 正确:volatile 不保证原子性

  • ❌ 错误:ReentrantLock 一定比 synchronized

  • ✅ 正确:JDK 6+ 后 synchronized 性能已接近甚至在低竞争下更好

一句话总结

volatile 最轻量、synchronized 最安全、ReentrantLock 最灵活,根据场景选择合适的同步机制。


公平锁和非公平锁的区别是什么?底层如何实现?

原始问法:

  • 公平锁和非公平锁的区别是什么?底层如何实现?

来源题目:SRC-03-33-136

面试先答

公平锁按线程请求顺序分配锁,遵循"先来先服务";非公平锁允许插队,新线程可以抢占老线程。区别在于:公平锁不会饥饿但吞吐量低,非公平锁吞吐量高但可能线程饥饿。底层实现基于 AQS(AbstractQueuedSynchronizer):公平锁获取锁时先检查等待队列,非公平锁直接 CAS 尝试。ReentrantLock 默认非公平锁,可通过构造参数 new ReentrantLock(true) 创建公平锁。synchronized 只有非公平锁。

核心结论
  • 公平锁:按等待顺序获取,无饥饿,吞吐量低
  • 非公平锁:允许插队,吞吐量高,可能饥饿
  • 底层基于 AQS 的 CLH 队列
  • ReentrantLock 支持公平/非公平,synchronized 只有非公平
1. 是什么

公平锁:线程按请求锁的顺序获取锁。

非公平锁:新线程可以插队,不按顺序获取锁。

2. 为什么需要它
  • 公平锁保证所有线程都能获取锁(无饥饿)
  • 非公平锁吞吐量更高(减少 CPU 唤醒开销)
3. 底层原理与完整流程

AQS CLH 队列

  • 双向链表队列,每个节点代表一个等待线程
  • 线程入队后自旋等待前驱节点释放锁

非公平锁获取流程

// ReentrantLock.NonfairSync.lock()
final void lock() {
    if (compareAndSetState(0, 1)) // 直接 CAS 尝试
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1); // 入队等待
}

公平锁获取流程

// ReentrantLock.FairSync.lock()
final void lock() {
    acquire(1); // 直接入队,不插队
}

// AQS.acquire()
public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

// FairSync.tryAcquire()
protected final boolean tryAcquire(int acquires) {
    Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        if (!hasQueuedPredecessors() && // 检查是否有前驱等待
            compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    // 重入逻辑...
}
4. 怎么使用
// 非公平锁(默认)
ReentrantLock unfairLock = new ReentrantLock();

// 公平锁
ReentrantLock fairLock = new ReentrantLock(true);

// 使用
fairLock.lock();
try {
    // 业务逻辑
} finally {
    fairLock.unlock();
}
5. 适用场景
  • 公平锁:需要保证所有线程都能获取锁的场景,如任务队列
  • 非公平锁:高吞吐量要求的场景,如 Web 服务
6. 不适用场景与替代方案
  • 不要在对延迟敏感的场景使用公平锁(增加 CPU 唤醒)
  • 替代方案:synchronized(非公平锁)、StampedLock(乐观读)
7. 优缺点与技术取舍
维度 公平锁 非公平锁
吞吐量
线程饥饿 可能
实现复杂度 稍高
唤醒开销 全部唤醒 有选择唤醒
8. 常见问题及解决方案
  • 问题:公平锁性能比非公平锁差很多
    • 解决:只有在确实需要公平性时才使用公平锁
9. 版本差异与实现边界
  • ReentrantLock 的公平/非公平从 Java 5 支持
  • synchronized 只有非公平锁(不可选)
  • JDK 15 中偏向锁被移除后 synchronized 性能略有调整
10. 常见追问
  • 追问:为什么 synchronized 只有非公平锁?
    • 回答方向:JVM 设计时为了吞吐量选择非公平策略,公平锁实现更复杂且吞吐更低。
11. 易错点
  • ❌ 错误:公平锁比非公平锁快
  • ✅ 正确:非公平锁吞吐量更高
一句话总结

公平锁按顺序获取、无饥饿但吞吐低;非公平锁允许插队、吞吐高但可能饥饿,底层基于 AQS 的 CLH 队列实现。


ReentrantReadWriteLock 是什么?适用场景?

原始问法:

  • ReentrantReadWriteLock是什么?适用场景?

来源题目:SRC-03-33-137

面试先答

ReentrantReadWriteLock 是 Java 提供的读写锁实现,基于 AQS。它将锁分为读锁和写锁:读锁可以被多个线程同时持有(读-读不互斥),写锁独占(写-读、写-写互斥)。核心优势是在读多写少的场景下提高并发性能。支持公平/非公平模式、读锁重入、写锁降级为读锁(不支持读锁升级为写锁)。适用场景:缓存实现、配置读取、统计查询等读多写少的场景。

核心结论
  • 读写分离:读锁共享、写锁独占
  • 适合读多写少场景
  • 支持重入和降级(写→读),不支持升级(读→写)
  • 基于 AQS 实现,可公平/非公平
1. 是什么
public interface ReadWriteLock {
    Lock readLock();  // 读锁
    Lock writeLock(); // 写锁
}

public class ReentrantReadWriteLock implements ReadWriteLock {
    // 实现
}

锁的互斥关系

读锁 写锁
读锁 ✅ 共享 ❌ 互斥
写锁 ❌ 互斥 ❌ 互斥
2. 为什么需要它
  • 读多写少场景下,允许多个线程同时读取
  • 比互斥锁(synchronizedReentrantLock)并发度更高
3. 底层原理与完整流程

基于 AQS 的实现

  • 使用 state 变量的高 16 位表示读锁计数
  • 低 16 位表示写锁计数
  • 读锁通过 ThreadLocal 记录每个线程的读锁重入次数
state: [读锁计数(16bit) | 写锁计数(16bit)]

降级(写→读)

// 写锁降级为读锁
rwLock.writeLock().lock();
try {
    // 写操作
    rwLock.readLock().lock(); // 降级为读锁
    try {
        // 读操作(持有写锁,不会被阻塞)
    } finally {
        rwLock.readLock().unlock();
    }
} finally {
    rwLock.writeLock().unlock();
}
4. 怎么使用
public class ReadWriteCache {
    private final ReentrantReadWriteLock rwLock =
        new ReentrantReadWriteLock();
    private final Map<String, String> cache = new HashMap<>();

    public String get(String key) {
        rwLock.readLock().lock();
        try {
            return cache.get(key);
        } finally {
            rwLock.readLock().unlock();
        }
    }

    public void put(String key, String value) {
        rwLock.writeLock().lock();
        try {
            cache.put(key, value);
        } finally {
            rwLock.writeLock().unlock();
        }
    }
}
5. 适用场景
  • 读多写少:缓存、配置读取
  • 统计查询:计数器、仪表盘
  • 文档编辑:读取多、写入少
6. 不适用场景与替代方案
  • 不要在写多读多场景使用(反而增加开销)
  • 不要在持有读锁时尝试获取写锁(升级不支持,会死锁)
  • 替代方案:StampedLock(更灵活)、ConcurrentHashMap(分段锁)
7. 优缺点与技术取舍
维度 ReentrantReadWriteLock ReentrantLock
读并发 高(多线程读) 低(单线程)
写并发 低(独占) 低(独占)
实现复杂度 稍高
适用场景 读多写少 通用同步
8. 常见问题及解决方案
  • 问题:读线程长时间阻塞写线程

    • 解决:使用公平模式或调整读写超时
  • 问题:读锁升级为写锁导致死锁

    • 解决:读写锁只支持写→读降级,不支持读→写升级
9. 版本差异与实现边界
  • Java 5 引入 ReentrantReadWriteLock
  • JDK 8 引入 StampedLock(更灵活的读写锁,支持乐观读)
  • StampedLock 不支持重入
10. 常见追问
  • 追问StampedLockReentrantReadWriteLock 的区别?
    • 回答方向:StampedLock 支持乐观读、不支持重入、读锁可升级为写锁。
11. 易错点
  • ❌ 错误:读写锁支持读锁升级为写锁
  • ✅ 正确:只支持写锁降级为读锁,不支持读锁升级为写锁
一句话总结

ReentrantReadWriteLock 实现读写分离,读锁共享、写锁独占,适合读多写少场景,基于 AQS 实现。


什么是 AQS?AQS 的原理是什么?

原始问法:

  • 什么是AQS?AQS的原理是什么?

来源题目:SRC-03-33-138

面试先答

AQS(AbstractQueuedSynchronizer,抽象队列同步器)是 JUC 包的基础同步框架,是 ReentrantLockCountDownLatchSemaphoreReentrantReadWriteLock 等同步工具的底层实现。AQS 的核心设计是:使用一个 volatile int state 表示同步状态,基于 CLH 双向队列管理等待线程,通过 CAS 修改 state 实现原子状态切换,模板方法模式让子类实现具体的获取和释放逻辑。AQS 是理解 Java 并发包的关键。

核心结论
  • AQS 是 JUC 包的核心基础框架
  • 核心组件:state 变量 + CLH 队列 + CAS
  • 模板方法模式:子类实现 tryAcquire/tryRelease
  • ReentrantLockCountDownLatchSemaphore 等的底层
1. 是什么

AQS(AbstractQueuedSynchronizer):抽象队列同步器,JUC 包的核心基础类。

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {

    private volatile int state; // 同步状态
    private transient Node head; // 队列头
    private transient Node tail; // 队列尾

    // 模板方法,子类实现
    protected boolean tryAcquire(int arg) { throw ... }
    protected boolean tryRelease(int arg) { throw ... }

    // 框架方法
    public final void acquire(int arg) { ... }
    public final boolean release(int arg) { ... }
}
2. 为什么需要它
  • 统一 JUC 包中各种同步工具的实现
  • 提供可复用的同步基础框架
  • 子类只需实现少量方法即可获得完整的同步能力
3. 底层原理与完整流程

核心设计

AQS 结构:
┌─────────────────────────────────┐
│  state (volatile int)            │ ← 同步状态
│  - ReentrantLock: 锁计数         │
│  - CountDownLatch: 剩余计数       │
│  - Semaphore: 剩余许可数          │
├─────────────────────────────────┤
│  CLH 双向队列                    │ ← 等待线程队列
│  head → [Node1] ← [Node2] ← tail│
├─────────────────────────────────┤
│  CAS 操作                        │ ← 原子修改 state
└─────────────────────────────────┘

acquire 流程

  1. 调用 tryAcquire(arg) 尝试获取(子类实现)
  2. 失败则封装为 Node 加入 CLH 队列尾部
  3. 在队列中自旋或阻塞等待前驱节点释放
  4. 前驱释放后唤醒当前线程
  5. 循环直到获取成功

release 流程

  1. 调用 tryRelease(arg) 释放(子类实现)
  2. 释放成功后唤醒队列中的下一个线程

CLH 队列结构

static final class Node {
    volatile int waitStatus; // 状态
    volatile Node prev;      // 前驱
    volatile Node next;      // 后继
    volatile Thread thread;  // 线程

    // 状态常量
    static final int CANCELLED =  1;
    static final int SIGNAL    = -1;
    static final int CONDITION = -2;
}
4. 怎么使用

AQS 是抽象类,通过继承实现自定义同步器:

// 自定义独占锁
public class MyLock extends AbstractQueuedSynchronizer {

    @Override
    protected boolean tryAcquire(int arg) {
        return compareAndSetState(0, 1);
    }

    @Override
    protected boolean tryRelease(int arg) {
        setState(0);
        return true;
    }

    public void lock() {
        acquire(1);
    }

    public void unlock() {
        release(1);
    }
}
5. 适用场景
  • JUC 包中所有同步工具的基础
  • 自定义同步器
  • 理解并发包的关键
6. 不适用场景与替代方案
  • 不要直接使用 AQS,应该使用 JUC 包的具体实现
  • 替代方案:使用现成的 ReentrantLockCountDownLatch
7. 优缺点与技术取舍
维度 AQS synchronized
灵活性
功能 可扩展 固定
复杂度
性能 中(JDK6+ 提升)
8. 常见问题及解决方案
  • 问题:AQS 队列过长导致性能下降
    • 解决:考虑使用公平/非公平锁、调整超时时间
9. 版本差异与实现边界
  • Java 5 引入 AQS(JUC 包)
  • AQS 是 JUC 包的核心,几乎所有并发工具都基于它
  • JDK 21 中虚拟线程的实现与 AQS 有关联
10. 常见追问
  • 追问:AQS 和 synchronized 的关系?
    • 回答方向:synchronized 基于对象监视器实现,AQS 是 JUC 包的基础,两者是不同的同步实现。
11. 易错点
  • ❌ 错误:AQS 是一种锁

  • ✅ 正确:AQS 是同步框架,是锁的底层实现

  • ❌ 错误:AQS 中的 state 含义固定

  • ✅ 正确:state 的含义由子类决定(锁计数、剩余许可等)

一句话总结

AQS 是 JUC 包的核心同步框架,通过 state 变量、CLH 队列和 CAS 实现线程同步,是理解 Java 并发工具的关键。

3.4 并发工具类


CountDownLatch、CyclicBarrier、Semaphore 的区别和使用场景?

原始问法:

  • CountDownLatch、CyclicBarrier、Semaphore的区别和使用场景?

来源题目:SRC-03-34-139

面试先答

三者都是 JUC 包的并发工具类,基于 AQS 实现:CountDownLatch 是一次性倒计时门闩,一个或多个线程等待其他线程完成;CyclicBarrier 是可循环使用的屏障,多个线程相互等待全部到达后同时执行;Semaphore 是信号量,控制同时访问的线程数量。核心区别:Latch 是一次性的、一个线程等多个;Barrier 是循环的、多个线程互相等;Semaphore 控制并发数量。分别适用于:并行计算等待所有任务完成、多人同时开始某项任务、限流场景。

核心结论
  • CountDownLatch:一次性倒计时,一个等多个
  • CyclicBarrier:循环屏障,多个互相等
  • Semaphore:信号量,控制并发数
  • 底层都基于 AQS 实现
1. 是什么
工具类 核心功能 复用性 典型场景
CountDownLatch 倒计时门闩 一次性 并行计算汇总
CyclicBarrier 循环屏障 可循环 多阶段并行计算
Semaphore 信号量 可复用 限流、资源池
2. 为什么需要它
  • 提供高层同步原语,无需手动实现 wait/notify
  • 代码更清晰、更安全
  • 覆盖不同的并发协作场景
3. 底层原理与完整流程

CountDownLatch

CountDownLatch latch = new CountDownLatch(3); // 计数=3
// 每个任务完成后 countDown()
latch.countDown(); // -1
// 等待计数归零
latch.await(); // 阻塞直到 0

CyclicBarrier

CyclicBarrier barrier = new CyclicBarrier(4, () -> {
    // 所有参与者到达后执行
});
// 每个线程到达屏障
barrier.await(); // 等待所有参与者

Semaphore

Semaphore sem = new Semaphore(3); // 3 个许可
sem.acquire(); // 获取许可(阻塞)
// 临界区
sem.release(); // 释放许可
4. 怎么使用
// CountDownLatch:并行计算汇总
CountDownLatch latch = new CountDownLatch(4);
List<Result> results = Collections.synchronizedList(new ArrayList<>());

for (int i = 0; i < 4; i++) {
    final int taskId = i;
    new Thread(() -> {
        results.add(compute(taskId));
        latch.countDown();
    }).start();
}
latch.await(); // 等待所有任务完成
// 汇总结果
processResults(results);

// CyclicBarrier:多阶段并行
CyclicBarrier barrier = new CyclicBarrier(3, () -> {
    System.out.println("所有阶段完成,进入下一阶段");
});

for (int i = 0; i < 3; i++) {
    new Thread(() -> {
        for (int stage = 0; stage < 5; stage++) {
            computeStage(stage);
            barrier.await(); // 等待所有线程完成当前阶段
        }
    }).start();
}

// Semaphore:限流
Semaphore semaphore = new Semaphore(10); // 最多 10 个并发
ExecutorService executor = Executors.newCachedThreadPool();

for (int i = 0; i < 100; i++) {
    executor.execute(() -> {
        try {
            semaphore.acquire();
            apiCall(); // 限流的 API 调用
        } finally {
            semaphore.release();
        }
    });
}
5. 适用场景
  • CountDownLatch:并行初始化、多任务汇总、服务启动等待
  • CyclicBarrier:多阶段并行计算、多人游戏开始、数据流水线
  • Semaphore:接口限流、数据库连接池、资源访问控制
6. 不适用场景与替代方案
  • 不要用 CountDownLatch 做循环同步,改用 CyclicBarrier
  • 不要用 Semaphore 做互斥锁(功能虽有但语义不对)
  • 替代方案:CompletableFuture(更强的编排能力)
7. 优缺点与技术取舍
工具类 优点 缺点
CountDownLatch 简单直接 一次性
CyclicBarrier 可循环 所有参与者必须到位
Semaphore 控制并发数 不支持公平性以外的策略
8. 常见问题及解决方案
  • 问题CountDownLatchcountDown() 未调用
    • 解决:在 finally 块中调用,或使用超时 await(timeout)
9. 版本差异与实现边界
  • 三者均基于 AQS 实现
  • Semaphore 支持公平/非公平模式
  • CyclicBarrier 支持 reset() 重置
10. 常见追问
  • 追问CountDownLatchCompletableFuture.allOf() 怎么选?
    • 回答方向:简单等待用 CountDownLatch,需要链式组合用 CompletableFuture
11. 易错点
  • ❌ 错误:CountDownLatch 可以重用
  • ✅ 正确:是一次性的,重用需要重新创建
一句话总结

CountDownLatch 一次性倒计时、CyclicBarrier 循环屏障、Semaphore 信号量限流,三者从不同维度解决线程协作问题。


ThreadLocal 是干嘛的?原理是什么?

原始问法:

  • ThreadLocal是干嘛的?原理是什么?

来源题目:SRC-03-34-140

面试先答

ThreadLocal 是线程本地存储,为每个线程提供独立的变量副本,实现线程间的数据隔离。核心原理是:每个 Thread 对象内部维护一个 ThreadLocalMap(类似 HashMap),以 ThreadLocal 对象为 key,存储线程独有的值。调用 threadLocal.set(value) 时,获取当前线程的 ThreadLocalMap,将 ThreadLocal 本身作为 key 存入值;threadLocal.get() 时,从当前线程的 Map 中取出对应的值。常用于:传递上下文信息(如用户 ID、事务 ID)、线程安全的 SimpleDateFormat、避免参数传递。

核心结论
  • ThreadLocal 实现线程间数据隔离
  • 原理:每个 Thread 持有一个 ThreadLocalMap
  • key 是 ThreadLocal 对象本身
  • 用于上下文传递、线程安全、避免参数传递
1. 是什么
public class ThreadLocal<T> {
    public void set(T value) { ... }
    public T get() { ... }
    public void remove() { ... }
}
2. 为什么需要它
  • 线程间数据隔离,无需同步
  • 传递上下文信息(如 Trace ID)
  • 避免方法间层层传参
3. 底层原理与完整流程

ThreadLocalMap 结构

Thread {
    ThreadLocal.ThreadLocalMap threadLocals;
}

ThreadLocalMap {
    Entry[] table; // 数组
    // Entry 继承 WeakReference<ThreadLocal>
    static class Entry extends WeakReference<ThreadLocal> {
        Object value;
    }
}

set 流程

  1. 获取当前线程 Thread.currentThread()
  2. 获取线程的 ThreadLocalMap
  3. 如果 Map 为空,创建一个
  4. ThreadLocal 对象作为 key,value 存入 Entry

get 流程

  1. 获取当前线程的 ThreadLocalMap
  2. 遍历数组找到 key 为当前 ThreadLocal 的 Entry
  3. 返回 Entry 的 value
4. 怎么使用
public class UserContext {
    private static final ThreadLocal<Long> USER_ID =
        new ThreadLocal<>();

    public static void setUserId(Long userId) {
        USER_ID.set(userId);
    }

    public static Long getUserId() {
        return USER_ID.get();
    }

    public static void clear() {
        USER_ID.remove(); // 必须清理!
    }
}

// 在拦截器中设置
public class AuthInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(...) {
        Long userId = parseUserId(request);
        UserContext.setUserId(userId);
        return true;
    }

    @Override
    public void afterCompletion(...) {
        UserContext.clear(); // 请求完成后清理
    }
}
5. 适用场景
  • 上下文传递(用户 ID、Trace ID、租户 ID)
  • 线程安全的共享对象(如 SimpleDateFormat
  • 避免方法参数传递
6. 不适用场景与替代方案
  • 不要在使用后忘记 remove(),会导致内存泄漏
  • 不要在线程池场景下直接使用(线程复用导致数据污染)
  • 替代方案:Spring RequestContextHolderMDC(SLF4J)
7. 优缺点与技术取舍
维度 ThreadLocal 方法传参
侵入性
类型安全
可追踪性
内存风险 有泄漏风险
8. 常见问题及解决方案
  • 问题:内存泄漏(详见 WRITE-03-0020 第 5 题)
  • 问题:线程池中数据串号
    • 解决:使用 TransmittableThreadLocal 或在任务开始时设置
9. 版本差异与实现边界
  • ThreadLocal 从 Java 2 就存在
  • ThreadLocalMap 使用线性探测法解决哈希冲突
  • key 使用弱引用,value 使用强引用(导致泄漏问题)
10. 常见追问
  • 追问ThreadLocal 的 key 为什么用弱引用?
    • 回答方向:当 ThreadLocal 变量被回收后,Entry 的 key 自动变为 null,便于清理。
11. 易错点
  • ❌ 错误:ThreadLocal 会自动清理
  • ✅ 正确:必须手动调用 remove() 清理
一句话总结

ThreadLocal 通过每个线程独立的 ThreadLocalMap 实现数据隔离,key 是 ThreadLocal 本身,用于上下文传递和线程安全。


ThreadLocal 如何实现线程隔离?

原始问法:

  • ThreadLocal如何实现线程隔离?

来源题目:SRC-03-34-141

面试先答

ThreadLocal 实现线程隔离的核心机制是:每个 Thread 对象内部有一个 ThreadLocalMap 数据结构(类似哈希表),存储以 ThreadLocal 对象为 key、线程私有值为 value 的键值对。当调用 threadLocal.set(value) 时,JVM 获取当前线程的 ThreadLocalMap,以 ThreadLocal 自身为 key 存入 value;调用 threadLocal.get() 时,从当前线程的 Map 中取值。由于每个线程的 Map 是独立的,所以同一 ThreadLocal 在不同线程中存储的值互不影响。

核心结论
  • 每个 Thread 独立持有 ThreadLocalMap
  • ThreadLocal 对象为 key 存储值
  • 天然隔离,无需同步
  • 线程销毁时 Map 被回收
1. 是什么

隔离的关键是数据绑定在线程对象上,而非 ThreadLocal 对象上。

线程 A: ThreadLocalMap → { ThreadLocal1 → valueA, ThreadLocal2 → valueA2 }
线程 B: ThreadLocalMap → { ThreadLocal1 → valueB, ThreadLocal2 → valueB2 }
2. 为什么需要它
  • 实现线程间数据隔离,避免竞争
  • 无需加锁即可实现线程安全
3. 底层原理与完整流程

set 实现(JDK 源码简化):

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t); // 获取线程的 Map
    if (map != null)
        map.set(this, value); // 以 ThreadLocal 为 key
    else
        createMap(t, value); // 创建 Map
}

get 实现

public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null)
            return (T)e.value; // 返回线程绑定的值
    }
    return setInitialValue(); // 返回初始值
}

隔离关键点

  • 数据存储在 Thread 对象的 threadLocals 字段中
  • 不同线程的 threadLocals 是不同的对象
  • 即使使用同一个 ThreadLocal 实例,各线程的值也互不影响
4. 怎么使用
public class ThreadLocalIsolationDemo {
    private static final ThreadLocal<String> USER_NAME =
        ThreadLocal.withInitial(() -> "unknown");

    public static void main(String[] args) {
        // 主线程设置值
        USER_NAME.set("MainThread");
        System.out.println(USER_NAME.get()); // MainThread

        // 子线程读取(独立的值)
        new Thread(() -> {
            System.out.println(USER_NAME.get()); // unknown
            USER_NAME.set("ChildThread");
            System.out.println(USER_NAME.get()); // ChildThread
        }).start();

        // 主线程值不变
        System.out.println(USER_NAME.get()); // MainThread
    }
}
5. 适用场景
  • 所有需要线程隔离的场景
6. 不适用场景与替代方案
  • 不要在忘记清理的情况下使用
  • 替代方案:ScopedValue(JDK 21,更安全)
7. 优缺点与技术取舍

天然隔离、无需同步,但有内存泄漏风险。

8. 常见问题及解决方案
  • 问题ThreadLocal 在子线程中获取不到值
    • 解决:使用 InheritableThreadLocal(但在线程池中有限制)
9. 版本差异与实现边界
  • JDK 21 引入 ScopedValueThreadLocal 的替代品,自动清理)
10. 常见追问
  • 追问ThreadLocalInheritableThreadLocal 的区别?
    • 回答方向:InheritableThreadLocal 会在子线程创建时继承父线程的值。
11. 易错点
  • ❌ 错误:ThreadLocal 的值存在 ThreadLocal 对象中
  • ✅ 正确:值存在 ThreadThreadLocalMap
一句话总结

ThreadLocal 通过将值绑定到每个线程独立的 ThreadLocalMap 上实现隔离,key 是 ThreadLocal 本身。


ThreadLocal 内存泄漏是什么场景触发的?怎么解决?

原始问法:

  • ThreadLocal内存泄漏是什么场景触发的?怎么解决?

来源题目:SRC-03-34-142

面试先答

ThreadLocal 内存泄漏的原因是:ThreadLocalMap 的 key 是弱引用(指向 ThreadLocal 对象),value 是强引用。当 ThreadLocal 变量被回收后,key 变为 null,但 value 仍然被 Entry 的强引用持有,无法被 GC 回收。泄漏场景:线程池中的线程长期存活、ThreadLocal 变量被回收但未调用 remove()。解决方案:使用完 ThreadLocal 后必须调用 remove() 方法清理。JDK 21 的 ScopedValue 自动管理生命周期。

核心结论
  • 泄漏原因:key 弱引用被回收后 value 仍被强引用持有
  • 泄漏场景:线程池、长生命周期线程
  • 解决:使用后必须 remove()
  • JDK 21 的 ScopedValue 自动清理
1. 是什么

内存泄漏场景

1. ThreadLocal 变量 tl 被置为 null
2. GC 回收 tl 对象
3. ThreadLocalMap 中 Entry 的 key 变为 null(弱引用)
4. 但 value 仍被 Entry 强引用 → 无法回收 → 泄漏
5. 线程池中的线程长期存在 → value 累积
2. 为什么需要它

理解泄漏原因才能正确使用 ThreadLocal

3. 底层原理与完整流程

ThreadLocalMap.Entry 结构

static class Entry extends WeakReference<ThreadLocal<?>> {
    Object value; // 强引用
    Entry(ThreadLocal<?> k, Object v) {
        super(k); // 弱引用 key
        value = v; // 强引用 value
    }
}

泄漏过程

Thread → ThreadLocalMap → Entry[] → Entry → value(强引用,无法回收)
                                                    ↓
                                              key(弱引用,已回收为 null)
4. 怎么使用
// 正确使用:try-finally
public void processRequest() {
    UserContext.setUserId(userId);
    try {
        // 业务逻辑
    } finally {
        UserContext.clear(); // 必须清理!
    }
}

// 使用 JDK 21 ScopedValue(自动清理)
public class ScopedValueDemo {
    private static final ScopedValue<String> USER =
        ScopedValue.newInstance();

    public void processRequest() {
        ScopedValue.where(USER, "alice").run(() -> {
            // 业务逻辑,作用域结束自动清理
        });
    }
}
5. 适用场景
  • 所有使用 ThreadLocal 的场景都需要注意清理
6. 不适用场景与替代方案
  • 不要在线程池中使用 ThreadLocal 而不清理
  • 替代方案:JDK 21 ScopedValue、Slf4J MDC
7. 优缺点与技术取舍
方案 泄漏风险 清理方式
ThreadLocal 手动 remove()
ScopedValue 自动清理
InheritableThreadLocal 手动 remove()
8. 常见问题及解决方案
  • 问题:Tomcat 中 ThreadLocal 导致内存泄漏
    • 解决:在 Filter 的 finally 中清理
9. 版本差异与实现边界
  • JDK 21:ScopedValue 自动管理,无泄漏风险
  • Spring RequestContextHolder 在请求结束后自动清理
10. 常见追问
  • 追问:为什么 JDK 不自动清理 value?
    • 回答方向:因为 key 弱引用回收时,JDK 无法确定 value 是否还在使用。
11. 易错点
  • ❌ 错误:ThreadLocal 使用后不会泄漏
  • ✅ 正确:如果不调用 remove() 会泄漏
一句话总结

ThreadLocal 内存泄漏是因为 key 弱引用回收后 value 仍被强引用持有,解决方法是使用后必须 remove(),JDK 21 用 ScopedValue 替代。


线程池场景下使用 ThreadLocal 会导致什么问题?如何解决?

原始问法:

  • 线程池场景下使用ThreadLocal会导致什么问题?如何解决?

来源题目:SRC-03-34-143

面试先答

线程池场景下使用 ThreadLocal 会导致两个问题:第一,数据串号——线程池中的线程被复用,如果上一个任务设置了 ThreadLocal 值但未清理,下一个任务可能读到脏数据;第二,内存泄漏——线程池中的线程长期存活,ThreadLocalMap 中的过期 Entry 无法回收。解决方案有三:第一,在任务执行的 finally 块中调用 remove() 清理;第二,使用阿里的 TransmittableThreadLocal 自动传递和清理;第三,使用 TaskDecorator 在任务提交时自动清理。

核心结论
  • 线程池复用线程导致数据串号和内存泄漏
  • 解决:finally remove()TransmittableThreadLocalTaskDecorator
  • 必须在任务边界清理 ThreadLocal
1. 是什么

数据串号场景

线程 A 执行任务 1:setUserId(100) → 未清理
线程 A 执行任务 2:getUserId() → 返回 100(脏数据!)
2. 为什么需要它

线程池复用线程,打破了"一个线程对应一个请求"的假设。

3. 底层原理与完整流程

问题根源:

  • 线程池中的线程长期存活
  • ThreadLocalMap 随线程存在
  • 如果上一个任务未清理,下一个任务会看到旧值
4. 怎么使用
// 方案一:finally 清理
public void executeTask(Runnable task) {
    try {
        before(); // 设置 ThreadLocal
        task.run();
    } finally {
        ThreadLocalUtils.clearAll(); // 清理
    }
}

// 方案二:TaskDecorator(Spring)
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setTaskDecorator(new TaskDecorator() {
    @Override
    public Runnable decorate(Runnable runnable) {
        // 捕获当前线程的 ThreadLocal 值
        Map<ThreadLocal<?>, Object> context = captureContext();
        return () -> {
            // 恢复到新线程
            restoreContext(context);
            try {
                runnable.run();
            } finally {
                clearContext(); // 清理
            }
        };
    }
});

// 方案三:TransmittableThreadLocal(阿里开源)
TransmittableThreadLocal<String> context = new TransmittableThreadLocal<>();
// 在线程池中自动传递和清理
5. 适用场景
  • 所有使用线程池的 ThreadLocal 场景
  • Spring 应用中的上下文传递
6. 不适用场景与替代方案
  • 不要在线程池中直接使用 ThreadLocal 而不清理
  • 替代方案:MDC、Spring RequestContextHolder
7. 优缺点与技术取舍
方案 侵入性 自动清理 自动传递
finally remove
TaskDecorator
TransmittableThreadLocal
8. 常见问题及解决方案
  • 问题TransmittableThreadLocal 仍有泄漏
    • 解决:使用 TtlExecutors.getTtlExecutorService() 包装线程池
9. 版本差异与实现边界
  • JDK 本身不解决线程池中 ThreadLocal 的传递问题
  • Spring TaskDecorator 仅在 Spring 线程池中生效
  • TransmittableThreadLocal 是阿里开源库,独立于 JDK
10. 常见追问
  • 追问InheritableThreadLocal 为什么在线程池中效果不好?
    • 回答方向:线程池复用线程时不会重新创建,不会触发继承。
11. 易错点
  • ❌ 错误:线程池中 ThreadLocal 是线程安全的
  • ✅ 正确:复用导致数据串号,必须清理
一句话总结

线程池中 ThreadLocal 会导致数据串号和泄漏,通过 finally remove()TransmittableThreadLocalTaskDecorator 解决。


不同 ThreadLocal 之间如何进行信息传递?

原始问法:

  • 不同ThreadLocal之间如何进行信息传递?

来源题目:SRC-03-34-144

面试先答

不同 ThreadLocal 之间本身不能直接传递信息,因为它们是独立的变量。常见的做法有三种:第一,使用一个 ThreadLocal 存储上下文对象,将所有需要传递的数据放在这个对象中;第二,使用 ThreadLocalget() 获取同一个线程中其他 ThreadLocal 的值;第三,使用 InheritableThreadLocalTransmittableThreadLocal 在线程间传递。最推荐的是使用一个包装类统一管理上下文,如 Spring 的 RequestContextHolder、SLF4J 的 MDC

核心结论
  • 不同 ThreadLocal 无法直接传递
  • 通过上下文包装类统一管理
  • 使用 InheritableThreadLocal 在线程间传递
  • 推荐使用 MDC 或自定义上下文管理器
1. 是什么
// 方式一:上下文对象
public class ThreadContext {
    private static final ThreadLocal<Map<String, Object>> CONTEXT =
        ThreadLocal.withInitial(HashMap::new);

    public static void put(String key, Object value) {
        CONTEXT.get().put(key, value);
    }

    public static Object get(String key) {
        return CONTEXT.get().get(key);
    }

    public static void clear() {
        CONTEXT.remove();
    }
}
2. 为什么需要它
  • 多个 ThreadLocal 之间共享数据
  • 避免重复存储相同信息
  • 统一管理上下文生命周期
3. 底层原理与完整流程

通过一个统一的 ThreadLocal<Map> 存储所有上下文数据,实现间接传递。

4. 怎么使用
// 设置上下文
ThreadContext.put("userId", 1001);
ThreadContext.put("tenantId", "tenant_01");
ThreadContext.put("traceId", UUID.randomUUID().toString());

// 在任何地方获取
Long userId = (Long) ThreadContext.get("userId");
String traceId = (String) ThreadContext.get("traceId");
5. 适用场景
  • 多租户系统、链路追踪、权限校验
6. 不适用场景与替代方案
  • 不要滥用上下文管理器存储大量数据
  • 替代方案:使用 MDC(SLF4J)、RequestContextHolder(Spring)
7. 优缺点与技术取舍

通过统一的上下文管理器简化了 ThreadLocal 之间的协作,但需要注意数据生命周期管理。

8. 常见问题及解决方案
  • 问题:上下文数据不一致
    • 解决:统一入口设置、出口清理
9. 版本差异与实现边界
  • 无版本差异
  • ScopedValue(JDK 21)提供了更安全的上下文传递
10. 常见追问
  • 追问MDC 是如何实现的?
    • 回答方向:MDC 内部使用 ThreadLocal<Map> 存储日志上下文信息。
11. 易错点
  • ❌ 错误:不同 ThreadLocal 可以直接通信
  • ✅ 正确:通过共享的上下文对象间接传递
一句话总结

不同 ThreadLocal 通过统一的上下文包装类(ThreadLocal<Map>)间接传递信息,推荐使用 MDC 或自定义管理器。


TransmittableThreadLocal 相比 InheritableThreadLocal 核心解决了什么问题?

原始问法:

  • TransmittableThreadLocal相比InheritableThreadLocal核心解决了什么问题?

来源题目:SRC-03-34-145

面试先答

TransmittableThreadLocal(TTL,阿里开源)相比 InheritableThreadLocal 核心解决了线程池复用线程时数据传递失效的问题。InheritableThreadLocal 仅在子线程创建时从父线程复制数据,线程池中的线程被复用后不会重新创建,因此无法获取新任务的上下文。TransmittableThreadLocal 在任务提交时捕获当前线程的上下文,在任务执行时恢复到子线程,无论线程是否被复用都能正确传递。

核心结论
  • InheritableThreadLocal:子线程创建时复制一次,线程池中失效
  • TransmittableThreadLocal:任务提交时捕获、执行时恢复,线程池中有效
  • 解决了线程池场景下的上下文传递问题
  • 阿里开源,广泛应用于生产环境
1. 是什么

问题场景

// InheritableThreadLocal 在线程池中失效
InheritableThreadLocal<String> context = new InheritableThreadLocal<>();
ExecutorService pool = Executors.newFixedThreadPool(1);

context.set("value-A");
pool.execute(() -> {
    System.out.println(context.get()); // value-A(第一次,线程创建时复制)
});

context.set("value-B");
pool.execute(() -> {
    System.out.println(context.get()); // value-A(不是 value-B!线程被复用)
});
2. 为什么需要它
  • InheritableThreadLocal 在线程池中无法正确传递上下文
  • 这是生产环境中非常常见的痛点
  • TTL 解决了这个核心问题
3. 底层原理与完整流程

TTL 工作机制

  1. 任务提交时,TtlRunnable 捕获当前线程的 TTL 上下文快照
  2. 子线程执行任务前,将快照恢复到子线程
  3. 任务执行后,清理子线程的 TTL 上下文
提交线程: capture → {key1=val1, key2=val2}
    ↓
TtlRunnable: 保存快照
    ↓
子线程: load(snapshot) → 执行 → remove()
4. 怎么使用
// 1. 创建 TTL
TransmittableThreadLocal<String> context = new TransmittableThreadLocal<>();

// 2. 包装线程池
ExecutorService ttlExecutor = TtlExecutors.getTtlExecutorService(
    Executors.newFixedThreadPool(4)
);

// 3. 使用
context.set("trace-id-123");
ttlExecutor.execute(() -> {
    System.out.println(context.get()); // trace-id-123(正确传递)
});

// 4. 或使用 TtlRunnable / TtlCallable
Runnable ttlTask = TtlRunnable.get(() -> {
    System.out.println(context.get());
});
executor.execute(ttlTask);
5. 适用场景
  • 线程池中的上下文传递(用户 ID、Trace ID、租户 ID)
  • 异步任务中的 ThreadLocal 传递
  • 全链路追踪
6. 不适用场景与替代方案
  • 不要在非线程池场景中使用(没必要)
  • 替代方案:TaskDecorator(Spring)、手动传递
7. 优缺点与技术取舍
维度 InheritableThreadLocal TransmittableThreadLocal
线程池支持 不支持 支持
传递时机 线程创建时 任务提交时
自动清理 不支持 支持
实现复杂度
8. 常见问题及解决方案
  • 问题:TTL 与 Spring TaskDecorator 冲突
    • 解决:选择其中一种方案即可
9. 版本差异与实现边界
  • TransmittableThreadLocal 是阿里开源项目
  • 最新版本 2.x,支持 JDK 1.6+
  • 广泛应用于阿里巴巴、美团等公司
10. 常见追问
  • 追问:TTL 如何与 CompletableFuture 配合?
    • 回答方向:使用 TtlExecutors.getTtlExecutorService() 包装 CompletableFuture 的线程池。
11. 易错点
  • ❌ 错误:InheritableThreadLocal 在线程池中也能工作
  • ✅ 正确:线程复用导致传递失效,必须用 TransmittableThreadLocal
一句话总结

TransmittableThreadLocal 解决了 InheritableThreadLocal 在线程池中复用线程时数据传递失效的核心问题,通过任务快照机制实现可靠的上下文传递。


Atomic 原子类的原理是什么?

原始问法:

  • Atomic原子类的原理是什么?

来源题目:SRC-03-34-146

面试先答

Java 的 Atomic 原子类基于 CAS(Compare-And-Swap) 实现。每个原子类内部维护一个 volatile 变量作为存储,通过 Unsafe 类的 native 方法直接调用 CPU 级别的 CAS 指令实现原子更新。以 AtomicInteger 为例:内部有一个 volatile int valueincrementAndGet() 方法在循环中读取旧值、计算新值、通过 CAS 比较并交换,成功则返回,失败则重试。Java 8 的 LongAdder 进一步优化,将单一 value 分散到多个 cell,减少 CAS 竞争。

核心结论
  • 原子类基于 CAS + volatile 实现
  • 通过 Unsafe 的 native 方法调用 CPU CAS 指令
  • 基本类型原子类:AtomicIntegerAtomicLongAtomicBoolean
  • 引用类型:AtomicReferenceAtomicStampedReference
  • JDK 8 优化:LongAdderDoubleAdder
1. 是什么

原子类家族

分类 功能
基本类型 AtomicIntegerAtomicLongAtomicBoolean 原子操作基本类型
引用类型 AtomicReference<T>AtomicStampedReference<T> 原子操作引用类型
数组类型 AtomicIntegerArrayAtomicLongArrayAtomicReferenceArray 原子操作数组
属性类型 AtomicIntegerFieldUpdaterAtomicLongFieldUpdater 原子操作对象的 volatile 字段
累加器 LongAdderDoubleAdderLongAccumulator 高并发累加优化
2. 为什么需要它
  • 提供无锁的线程安全操作
  • synchronized 更轻量(无阻塞、无上下文切换)
  • 适合计数器、状态标志等简单原子操作
3. 底层原理与完整流程

AtomicInteger 源码

public class AtomicInteger extends Number {
    private static final Unsafe U = Unsafe.getUnsafe();
    private static final long VALUE;

    static {
        VALUE = U.objectFieldOffset(AtomicInteger.class.getDeclaredField("value"));
    }

    private volatile int value; // volatile 保证可见性

    public final int incrementAndGet() {
        int prev, next;
        do {
            prev = value; // 读
            next = prev + 1; // 计算
        } while (!U.compareAndSetInt(this, VALUE, prev, next)); // CAS
        return next;
    }
}

LongAdder 优化

LongAdder:
┌───────────────────────────────┐
│  base (long)                   │ ← 低并发使用
├───────────────────────────────┤
│  cells[] (Cell 数组)           │ ← 高并发分散
│  ┌───┬───┬───┬───┐            │
│  │ 0 │ 0 │ 1 │ 0 │            │
│  └───┴───┴───┴───┘            │
└───────────────────────────────┘
4. 怎么使用
// AtomicInteger
AtomicInteger counter = new AtomicInteger(0);
counter.incrementAndGet(); // 原子 +1
counter.getAndAdd(10); // 原子 +10

// LongAdder(高并发推荐)
LongAdder adder = new LongAdder();
adder.increment(); // 分拆累加,减少 CAS 竞争
long result = adder.sum(); // 汇总结果

// AtomicReference
AtomicReference<String> ref = new AtomicReference<>("init");
ref.compareAndSet("init", "updated");

// AtomicStampedReference(解决 ABA)
AtomicStampedReference<String> stampedRef =
    new AtomicStampedReference<>("A", 0);
stampedRef.compareAndSet("A", "B", 0, 1);
5. 适用场景
  • 计数器、累加器(推荐 LongAdder
  • 状态标志位
  • 无锁数据结构(ConcurrentHashMapConcurrentLinkedQueue
6. 不适用场景与替代方案
  • 不要用原子类做多变量复合操作
  • 不要在高并发下使用单一 AtomicInteger(用 LongAdder
  • 替代方案:synchronizedReentrantLock
7. 优缺点与技术取舍
原子类 优点 缺点
AtomicInteger 简单、轻量 高并发下 CAS 自旋
LongAdder 高并发高性能 非精确操作(sum 非原子)
8. 常见问题及解决方案
  • 问题AtomicInteger 高并发下 CPU 高
    • 解决:改用 LongAdder
9. 版本差异与实现边界
  • Java 5:引入原子类(AtomicInteger 等)
  • Java 8:引入 LongAdderDoubleAdder
  • Java 9:VarHandle 提供更灵活的原子操作
10. 常见追问
  • 追问LongAdder.sum() 是原子的吗?
    • 回答方向:不是,sum() 只是简单求和,不保证原子性,适合统计场景。
11. 易错点
  • ❌ 错误:原子类可以替代所有锁
  • ✅ 正确:原子类只适合单变量操作
一句话总结

原子类基于 CAS + volatile 实现无锁原子操作,LongAdder 在高并发下进一步优化,是实现线程安全计数器的首选。

3.5 Java 内存模型


什么是 JMM 模型?三大特性是什么?如何保证?

原始问法:

  • 什么是JMM模型?三大特性是什么?如何保证?

来源题目:SRC-03-35-147

面试先答

JMM(Java Memory Model,Java 内存模型)是 Java 虚拟机规范中定义的内存访问模型,规定了多线程读写共享变量时的内存可见性和有序性规则。JMM 的三大特性是:原子性——操作要么全部执行要么不执行;可见性——一个线程修改的值对其他线程立即可见;有序性——指令按代码顺序执行,禁止重排序。保证方式:原子性通过 synchronizedLock、原子类(CAS)保证;可见性通过 volatilesynchronizedfinal 保证;有序性通过 volatile(内存屏障)、synchronizedfinal 保证。理解 JMM 是掌握 Java 并发编程的核心。

核心结论
  • JMM 定义了多线程内存访问的规则
  • 三大特性:原子性、可见性、有序性
  • 原子性:synchronizedLock、CAS
  • 可见性:volatilesynchronizedfinal
  • 有序性:volatile(内存屏障)、synchronizedfinal
  • JMM 基于 happens-before 原则建立
1. 是什么

JMM(Java Memory Model):Java 内存模型,定义了线程与主内存之间的抽象关系。

JMM 内存结构

线程 A                    主内存(Main Memory)
┌──────────────┐         ┌──────────────────┐
│ 工作内存 A    │         │                  │
│ (CPU Cache)   │  读写   │  共享变量       │
│              │ ←─────→ │                  │
│ 变量副本     │         │                  │
└──────────────┘         └──────────────────┘

线程 B                    主内存(Main Memory)
┌──────────────┐         ┌──────────────────┐
│ 工作内存 B    │         │                  │
│ (CPU Cache)   │  读写   │  共享变量       │
│              │ ←─────→ │                  │
└──────────────┘         └──────────────────┘

三大特性

特性 含义 保证机制
原子性 操作不可分割 synchronizedLock、CAS
可见性 修改立即可见 volatilesynchronizedfinal
有序性 禁止重排序 volatilesynchronizedfinal
2. 为什么需要它
  • 不同 CPU 架构内存模型不同(x86 强、ARM 弱)
  • 编译器会优化指令顺序
  • JMM 提供统一的内存可见性和有序性保证
3. 底层原理与完整流程

happens-before 原则: JMM 通过 happens-before 规则保证有序性,如果两个操作之间存在 happens-before 关系,那么前一个操作的结果对后一个操作可见。

八大 happens-before 规则

  1. 程序顺序规则:同一线程中,前面的操作 happens-before 后面的
  2. 监视器锁规则:unlock happens-before 后续对同一锁的 lock
  3. volatile 变量规则:volatile 写 happens-before 后续对该变量的读
  4. 线程启动规则:Thread.start() happens-before 线程内的所有操作
  5. 线程终止规则:线程内的所有操作 happens-before Thread.join() 返回
  6. 传递性:如果 A happens-before B,B happens-before C,则 A happens-before C
  7. final 字段规则:构造函数中对 final 的写 happens-before 其他线程读到该 final 字段
  8. 中断规则:对线程 interrupt() 的调用 happens-before 被中断线程检测到中断事件

可见性保证

  • volatile 写:强制刷新主内存
  • volatile 读:强制从主内存读取
  • synchronized:锁释放前刷新主内存,锁获取后从主内存读取

有序性保证

  • volatile:禁止重排序(内存屏障)
  • synchronized:锁内代码保持顺序
  • final:初始化安全性
4. 怎么使用
// volatile 保证可见性和有序性
public class JMMDemo {
    private volatile boolean ready = false;
    private int data = 0;

    public void writer() {
        data = 1;          // 写
        ready = true;      // volatile 写,保证 data 的修改对 reader 可见
    }

    public void reader() {
        if (ready) {       // volatile 读,保证能看到 data = 1
            System.out.println(data); // 可能输出 0(重排序时)
        }
    }
}

// DCL 单例 - volatile 保证有序性
public class Singleton {
    private static volatile Singleton instance;

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton(); // volatile 防止重排序
                }
            }
        }
        return instance;
    }
}
5. 适用场景
  • 所有 Java 并发编程都依赖 JMM 的规则
  • 理解 JMM 是正确使用并发工具的基础
6. 不适用场景与替代方案
  • 不要依赖 JMM 的默认行为(无同步时无保证)
  • 替代方案:使用 JUC 包的高级工具
7. 优缺点与技术取舍

JMM 提供了统一的内存可见性保证,但需要开发者正确使用 volatilesynchronized 等同步机制。

8. 常见问题及解决方案
  • 问题:没有使用同步时数据不一致
    • 解决:使用 volatilesynchronized 等建立 happens-before 关系
9. 版本差异与实现边界
  • JMM 规范从 Java 1.0 就存在,但在 Java 5 中被重新定义
  • Java 5 后的 JMM 建立在 happens-before 原则上
  • volatile 语义在 Java 5 后被加强
  • JVM 实现差异:HotSpot 的内存屏障实现
10. 常见追问
  • 追问:happens-before 和内存屏障的关系?
    • 回答方向:happens-before 是 JMM 的抽象规则,内存屏障是 CPU 层面的实现手段。
11. 易错点
  • ❌ 错误:JMM 保证所有操作的原子性
  • ✅ 正确:只有 synchronizedLock、CAS 保证原子性
一句话总结

JMM 是 Java 内存模型,通过 happens-before 规则保证原子性、可见性和有序性,是 Java 并发编程的基础规范。


什么是指令重排序?如何禁止指令重排序?

原始问法:

  • 什么是指令重排序?如何禁止指令重排序?

来源题目:SRC-03-35-148

面试先答

指令重排序是指编译器或 CPU 在执行指令时,为了提高性能而改变指令的执行顺序,但这种改变不改变程序的正确结果(在单线程中)。重排序分为两类:编译器重排序和 CPU 重排序。在多线程环境下,重排序可能导致程序错误。禁止重排序的方法:第一,使用 volatile 关键字——通过内存屏障禁止编译器和 CPU 的重排序;第二,使用 synchronized——同步块内的指令不会被重排到块外;第三,使用 final——保证初始化安全性。典型的例子是 DCL 单例中必须使用 volatile 防止重排序。

核心结论
  • 指令重排序是编译器/CPU 为优化性能而调整指令顺序
  • 单线程重排序不影响结果,多线程可能出错
  • 禁止方法:volatile(内存屏障)、synchronizedfinal
  • 典型场景:DCL 单例、双重检查锁
1. 是什么

指令重排序:编译器或 CPU 改变程序中指令的执行顺序。

重排序类型

  1. 编译器重排序:编译器优化时调整指令顺序
  2. CPU 重排序:CPU 执行时动态调整指令顺序(乱序执行)

重排序示例

// 原始代码
data = 1;       // 指令 1
flag = true;    // 指令 2

// 可能被重排为
flag = true;    // 指令 2(先执行)
data = 1;       // 指令 1(后执行)

单线程中无影响,多线程中另一个线程可能看到 flag = truedata = 0

2. 为什么需要它
  • 编译器和 CPU 为了提高性能进行优化
  • 但在多线程中可能导致程序错误
3. 底层原理与完整流程

DCL 单例重排序问题

instance = new Singleton(); // 分为三步:
// 1. 分配内存
// 2. 初始化对象
// 3. 将引用指向内存地址

// 可能被重排为:
// 1. 分配内存
// 3. 将引用指向内存地址 ← 重排!
// 2. 初始化对象

// 后果:
// 线程 A: 执行到步骤 3,instance 不为 null
// 线程 B: 检查 instance != null,直接使用 → 未初始化!

volatile 禁止重排序的实现

  • 写之前:StoreStore 屏障(禁止写-写重排)
  • 写之后:StoreLoad 屏障(禁止写-读重排)
  • 读之前:LoadLoad 屏障(禁止读-读重排)
  • 读之后:LoadStore 屏障(禁止读-写重排)
4. 怎么使用
// 使用 volatile 禁止重排序
public class ProperSingleton {
    private static volatile ProperSingleton instance; // 必须 volatile!

    private ProperSingleton() {}

    public static ProperSingleton getInstance() {
        if (instance == null) {                    // 第一次检查
            synchronized (ProperSingleton.class) {
                if (instance == null) {            // 第二次检查
                    instance = new ProperSingleton(); // volatile 写
                }
            }
        }
        return instance;
    }
}

// 使用 final 保证初始化安全性
public class FinalSingleton {
    private final int value;

    public FinalSingleton() {
        this.value = 42; // final 写,保证其他线程能看到
    }
}
5. 适用场景
  • DCL 单例(必须用 volatile
  • 发布不可变对象(final 保证初始化安全)
  • 双重检查锁场景
6. 不适用场景与替代方案
  • 不要在没有同步的情况下依赖程序顺序
  • 替代方案:使用 synchronizedLock、枚举单例(天然防止重排序)
7. 优缺点与技术取舍

重排序提高了单线程性能,但增加了多线程编程的复杂度。通过 volatile 等机制可以禁止重排序,但会带来一定的性能开销。

8. 常见问题及解决方案
  • 问题:DCL 单例不加 volatile 偶尔出错
    • 解决:使用 volatile 或改用枚举单例
9. 版本差异与实现边界
  • Java 5+ 中 volatile 的语义被加强,完全建立在 happens-before 上
  • Java 5 之前的 volatile 不保证原子性(现在也不保证,但可见性和有序性已保证)
  • x86 架构的 TSO(Total Store Order)模型比 ARM 弱模型更少重排序
10. 常见追问
  • 追问:为什么枚举单例不需要 volatile
    • 回答方向:枚举的创建由 JVM 保证线程安全,类加载机制天然防止重排序。
11. 易错点
  • ❌ 错误:重排序一定会发生

  • ✅ 正确:重排序是可能发生,不是必然发生,所以是偶发 bug

  • ❌ 错误:synchronized 不能防止重排序

  • ✅ 正确:synchronized 锁内代码不会被重排到锁外

一句话总结

指令重排序是编译器/CPU 的性能优化,多线程中可能导致错误,通过 volatile 的内存屏障、synchronizedfinal 禁止。


什么是内存屏障?有什么作用?

原始问法:

  • 什么是内存屏障?有什么作用?

来源题目:SRC-03-35-149

面试先答

内存屏障(Memory Barrier)是 CPU 级别的同步指令,用于保证内存操作的顺序性和可见性。它像一道屏障,阻止屏障前后的指令被重排序,并强制将缓存数据刷新到主内存或从主内存读取。内存屏障有四种类型:LoadLoad、StoreStore、LoadStore、StoreLoad。在 Java 中,volatilesynchronizedfinal 等关键字的可见性和有序性保证都是通过内存屏障实现的。内存屏障是理解 JMM 和底层同步机制的关键。

核心结论
  • 内存屏障是 CPU 同步指令,保证有序性和可见性
  • 四种类型:LoadLoad、StoreStore、LoadStore、StoreLoad
  • 作用:禁止重排序 + 强制缓存刷新
  • Java 中 volatile/synchronized 通过内存屏障实现同步
1. 是什么

内存屏障(Memory Barrier):CPU 级别的指令,保证屏障前后的内存操作顺序不被重排序,并强制缓存与主内存同步。

四种内存屏障

屏障类型 作用 说明
LoadLoad 禁止 读-读 重排 保证前一个读在后一个读之前完成
StoreStore 禁止 写-写 重排 保证前一个写在后一个写之前完成
LoadStore 禁止 读-写 重排 保证前一个读在后一个写之前完成
StoreLoad 禁止 写-读 重排 保证前一个写在后一个读之前完成(最重量级)
2. 为什么需要它
  • CPU 执行指令时可能乱序执行以提高性能
  • CPU 缓存导致不同核心看到的数据不一致
  • 内存屏障为并发编程提供有序性和可见性保证
3. 底层原理与完整流程

CPU 级实现

  • x86:MFENCE(全屏障)、SFENCE(Store 屏障)、LFENCE(Load 屏障)
  • ARM:DMB(Data Memory Barrier)、DSB(Data Synchronization Barrier)、ISB(Instruction Synchronization Barrier)

Java 中的使用

volatile 写:
    ... 写 1 → StoreStore → volatile 写 → StoreLoad → ... 读 2
    (屏障保证写 1 不被重排到 volatile 写之后)
    (屏障保证 volatile 写不被重排到读 2 之后)
    (同时强制刷新缓存到主内存)

volatile 读:
    ... 写 1 → LoadLoad → volatile 读 → LoadStore → ... 写 2
    (屏障保证写 1 不被重排到 volatile 读之后)
    (屏障保证 volatile 读不被重排到写 2 之后)
    (同时强制从主内存读取)

synchronized 中的内存屏障

  • 进入同步块:在 lock 后插入 Load 屏障(强制读)
  • 退出同步块:在 unlock 前插入 Store 屏障(强制写)
4. 怎么使用
// 开发者无需手动使用内存屏障
// JMM 会在以下情况自动插入:

// 1. volatile 读写
volatileFlag = true; // 隐含 StoreStore + StoreLoad
if (volatileFlag) { } // 隐含 LoadLoad + LoadStore

// 2. synchronized
synchronized (lock) {
    // 进入:Load 屏障
    // 退出:Store 屏障
}

// 3. final 字段的初始化
public class SafePublication {
    final int value;
    public SafePublication(int v) {
        this.value = v; // final 写,保证初始化安全
    }
}

// 4. VarHandle(JDK 9+)
VarHandle handle = MethodHandles.lookup()
    .in(MyClass.class)
    .findVarHandle(MyClass.class, "counter", int.class)
    .getVarHandle();
handle.setVolatile(this, 10); // volatile 写(含屏障)
int val = handle.getVolatile(this); // volatile 读(含屏障)
5. 适用场景
  • 所有使用 volatilesynchronizedfinal 的场景
  • 理解 JMM 和并发编程的底层机制
6. 不适用场景与替代方案
  • 不要在 Java 代码中手动使用内存屏障(JMM 自动处理)
  • 替代方案:使用 volatilesynchronized 等高层机制
7. 优缺点与技术取舍

内存屏障提供了强同步保证,但会带来性能开销(阻止 CPU 优化、强制缓存刷新)。在 x86 架构上,由于 TSO 内存模型,StoreLoad 屏障默认存在,性能影响较小;在 ARM 架构上影响更大。

8. 常见问题及解决方案
  • 问题volatile 变量性能比普通变量差
    • 解决:这是内存屏障的必要开销,换取可见性和有序性保证
9. 版本差异与实现边界
  • 内存屏障是 CPU 指令,与 JDK 版本无关
  • x86(TSO 模型):部分屏障默认存在,性能好
  • ARM(弱内存模型):需要更多屏障,性能差
  • JDK 9 的 VarHandle 提供了更灵活的屏障控制
10. 常见追问
  • 追问synchronizedvolatile 都用了内存屏障,有什么区别?
    • 回答方向:synchronized 通过 monitor 实现,除了屏障还有锁竞争和阻塞;volatile 只有屏障,无锁开销。
11. 易错点
  • ❌ 错误:内存屏障只在 volatile 中使用

  • ✅ 正确:synchronizedfinal 也使用内存屏障

  • ❌ 错误:内存屏障只保证可见性

  • ✅ 正确:内存屏障同时保证可见性和有序性

一句话总结

内存屏障是 CPU 级同步指令,禁止重排序并强制缓存刷新,是 volatilesynchronizedfinal 实现可见性和有序性的底层机制,是 JMM 的核心基础。


推荐文章

14-系统设计
12-设计模式
上一篇 02-Java集合
下一篇 04-JVM