Java 多线程面试核心知识点总结

整理了Java多线程面试中最常考的知识点,包含详细代码解释和图解


1. 进程与线程的概念

1.1 什么是进程?

进程是操作系统进行资源分配和调度的基本单位,是正在运行的程序的实例。

进程的核心特征

特征 说明
独立性 每个进程拥有独立的地址空间、文件描述符、寄存器状态
资源拥有 操作系统分配给进程:内存、文件句柄、I/O设备
生命周期 创建 → 就绪 → 运行 → 阻塞 → 终止
通信方式 进程间通信(IPC):管道、消息队列、共享内存、socket等

进程的结构

1
2
3
4
5
6
7
8
9
10
11
进程
├── PCB (进程控制块)
│ ├── 进程ID (PID)
│ ├── 进程状态
│ ├── 寄存器上下文
│ ├── 内存管理信息
│ └── 资源限制
├── 代码段 (Text)
├── 数据段 (Data)
├── 堆 (Heap)
└── 栈 (Stack)

进程与程序的区别

程序 进程
静态的磁盘文件 动态的内存实例
永久保存 有生命周期
被动实体 主动实体

1.2 什么是线程?

线程是进程内的执行单元,是CPU调度的最小单位。

线程的核心特征

特征 说明
轻量级 线程共享进程的地址空间,创建/切换成本远低于进程
共享资源 同一进程内的线程共享:代码段、数据段、堆、打开的文件
独立资源 每个线程独有:栈、寄存器、程序计数器、线程局部存储
并发执行 同一进程内多线程可并行执行在多核CPU上

2. 线程与进程的关系、区别及优缺点

2.1 关系

1
2
3
4
进程 (资源分配单位)
└── 线程1 (CPU调度单位)
└── 线程2 (CPU调度单位)
└── 线程3 (CPU调度单位)

一个进程可以包含多个线程,线程是进程内的执行流,共享进程的资源。

2.2 核心区别

维度 进程 线程
根本区别 资源分配的基本单位 CPU调度的基本单位
地址空间 独立(隔离) 共享(同一进程内)
创建/销毁 慢 (ms级) 快 (μs级)
切换开销 大(切换页表、缓存) 小(仅切换寄存器和栈)
通信 需 IPC(复杂) 直接共享内存(简单)
安全性 高(独立地址空间) 低(共享地址空间)
容错性 高(一个进程挂不影响其他) 低(线程崩溃可能波及整个进程)

2.3 进程优缺点

✅ 优点:

  • 独立地址空间,隔离性好,一个进程崩溃不影响其他
  • 适合 CPU 密集型任务,可充分利用多核
  • 编程简单(无需考虑同步)

❌ 缺点:

  • 创建/切换开销大
  • 进程间通信复杂(需要 IPC)
  • 内存占用高

2.4 线程优缺点

✅ 优点:

  • 轻量级,创建/切换快
  • 共享进程资源,通信简单(直接读写共享内存)
  • 适合 I/O 密集型任务

❌ 缺点:

  • 共享地址空间,需要同步(加锁),编程复杂
  • 一个线程崩溃可能导致整个进程崩溃
  • 受 GIL 限制(Python 中,Java无此问题)

2.5 使用场景

场景 推荐
CPU 密集型任务 多进程
I/O 密集型任务 多线程
需要高隔离性 多进程
需要频繁通信 多线程
Python CPU 密集 multiprocessing

3. 如何创建线程 (Java)

3.1 方式一:继承 Thread 类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 定义一个继承自 Thread 的类
class MyThread extends Thread {
// 重写 run() 方法,线程执行的核心逻辑就写在这里
@Override
public void run() {
// 线程启动后会执行这个方法
System.out.println("Thread is running!");
}
}

public class CreateThreadDemo {
public static void main(String[] args) {
// 创建线程对象
MyThread thread = new MyThread();

// 启动线程(不是调用 run(),是调用 start())
thread.start(); // start() 会创建新线程并执行 run()
}
}

代码解释:

  • extends Thread:让 MyThread 成为一个线程类
  • run():线程执行的主体方法,写在线程中要做的任务
  • start():启动线程的唯一正确方式,会创建新线程并调用 run()

3.2 方式二:实现 Runnable 接口(推荐)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 定义一个实现 Runnable 接口的类
class MyRunnable implements Runnable {
// 实现 run() 方法
@Override
public void run() {
System.out.println("Runnable thread is running!");
}
}

public class CreateThreadDemo2 {
public static void main(String[] args) {
// 创建 Runnable 实现类对象
MyRunnable runnable = new MyRunnable();

// 创建 Thread 对象,将 Runnable 传入
Thread thread = new Thread(runnable);

// 启动线程
thread.start();

// 方式二:使用 Lambda 表达式(更简洁)
new Thread(() -> {
System.out.println("Lambda thread is running!");
}).start();
}
}

代码解释:

  • implements Runnable:实现接口,比继承 Thread 更好(Java 不支持多继承)
  • new Thread(runnable):将任务对象传给 Thread,Thread 负责创建线程
  • () -> {}:Lambda 表达式,简化匿名内部类写法

3.3 方式三:使用线程池(推荐)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadPoolDemo {
public static void main(String[] args) {
// 创建一个固定大小的线程池(5个工作线程)
ExecutorService executor = Executors.newFixedThreadPool(5);

// 提交10个任务给线程池
for (int i = 0; i < 10; i++) {
final int taskId = i;

// 提交任务(自动分配给空闲线程执行)
executor.submit(() -> {
System.out.println("Task " + taskId + " is running");
});
}

// 关闭线程池(不再接收新任务,等待已提交任务完成)
executor.shutdown();
}
}

代码解释:

  • ExecutorService:线程池接口,管理线程的生命周期
  • Executors.newFixedThreadPool(5):创建固定5个线程的线程池
  • executor.submit():提交任务给线程池执行
  • executor.shutdown():优雅关闭线程池,不再接收新任务

3.4 方式四:实现 Callable 接口 + Future

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class CallableDemo {
public static void main(String[] args) throws Exception {
// 创建 Callable 任务(有返回值)
Callable<Integer> callable = () -> {
int sum = 0;
for (int i = 1; i <= 100; i++) {
sum += i;
}
return sum; // 返回计算结果
};

// 用 FutureTask 包装 Callable
FutureTask<Integer> futureTask = new FutureTask<>(callable);

// 创建线程执行
Thread thread = new Thread(futureTask);
thread.start();

// 获取返回值(会阻塞等待结果)
Integer result = futureTask.get();
System.out.println("Result: " + result); // 输出: Result: 5050
}
}

代码解释:

  • Callable<T>:带返回值的任务接口,泛型指定返回值类型
  • FutureTask:包装 Callable,可以像 Runnable 一样交给线程执行
  • futureTask.get():获取任务返回值,如果任务未完成会阻塞等待

4. 线程的生命周期和状态

4.1 线程的6种状态

Java 中线程有 6 种状态Thread.State 枚举):

线程状态转换图

1
2
3
NEW → RUNNABLE → BLOCKED/WAITING/TIMED_WAITING → TERMINATED

(可再次回到 RUNNABLE)
状态 说明 触发方式
NEW 线程创建但未启动 new Thread()
RUNNABLE 可运行(就绪+运行) start()
BLOCKED 阻塞(等待获取锁) synchronized 未获取锁
WAITING 无限等待 Object.wait(), Thread.join(), LockSupport.park()
TIMED_WAITING 限时等待 Thread.sleep(), wait(timeout), join(timeout)
TERMINATED 已终止 run() 执行完毕

4.2 状态转换图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
          ┌──────────────────┐
│ NEW │
└────────┬─────────┘
│ start()

┌──────────────────┐
┌────────▶│ RUNNABLE │◀──────────┐
│ └────────┬─────────┘ │
│ │ │
│ │ I/O/锁/sleep │
│ ▼ │
│ ┌──────────────────┐ │
│ │ BLOCKED │ │
│ │ /WAITING │ │
│ └────────┬─────────┘ │
│ │ notify/wimer/锁获得 │
│ │ │
└──────────────────┘ │ run() 结束


┌──────────────────┐
│ TERMINATED │
└──────────────────┘

4.3 各状态代码示例

NEW(新建)

1
2
3
4
// 创建线程对象,此时线程还未启动
Thread thread = new Thread(() -> System.out.println("Hello"));
// getState() 返回 NEW
System.out.println(thread.getState()); // NEW

代码解释:

  • new Thread():仅创建了线程对象,尚未分配系统资源
  • getState():获取线程当前状态

RUNNABLE(可运行)

1
2
3
4
5
6
7
8
9
Thread thread = new Thread(() -> {
for (int i = 0; i < 100000; i++) {
Math.random(); // 模拟计算任务
}
});

thread.start(); // 启动线程
// 此时线程就绪,等待CPU调度执行
System.out.println(thread.getState()); // RUNNABLE(可能很快变为 TERMINATED)

代码解释:

  • start():启动线程,线程状态变为 RUNNABLE
  • RUNNABLE 包含”就绪(READY)”和”运行(RUNNING)”两种子状态

BLOCKED(阻塞)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class BlockedDemo {
private static final Object lock = new Object();

public static void main(String[] args) throws InterruptedException {
// 线程1:获取锁后进入同步块
Thread thread1 = new Thread(() -> {
synchronized (lock) {
try {
Thread.sleep(5000); // 持有锁5秒
} catch (InterruptedException e) {}
}
});

// 线程2:尝试获取锁,但锁被thread1持有
Thread thread2 = new Thread(() -> {
synchronized (lock) { // 阻塞等待获取锁
System.out.println("Thread2 got the lock");
}
});

thread1.start();
Thread.sleep(100); // 确保thread1先获取锁
thread2.start();
Thread.sleep(100); // 确保thread2已尝试获取锁

// thread2 处于 BLOCKED 状态
System.out.println("Thread2 state: " + thread2.getState()); // BLOCKED
}
}

代码解释:

  • synchronized (lock):尝试获取对象的锁
  • 如果锁被其他线程持有,当前线程进入 BLOCKED 状态等待获取锁
  • BLOCKED 状态的线程不占用 CPU

WAITING(无限等待)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class WaitingDemo {
private static final Object lock = new Object();

public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
synchronized (lock) {
try {
// 调用 wait() 进入无限等待状态
// wait() 会释放持有的锁
lock.wait();
} catch (InterruptedException e) {}
}
});

thread.start();
Thread.sleep(100); // 确保thread已调用wait()

// thread 处于 WAITING 状态
System.out.println("Thread state: " + thread.getState()); // WAITING
}
}

代码解释:

  • lock.wait():让当前线程在 lock 的等待集中无限等待
  • wait() 会释放当前持有的锁
  • 必须先获取 lock 的锁才能调用 wait()

TIMED_WAITING(限时等待)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class TimedWaitingDemo {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
try {
// Thread.sleep() 使线程进入限时等待
Thread.sleep(3000); // 等待3秒后自动醒来
} catch (InterruptedException e) {}
});

thread.start();
Thread.sleep(100); // 确保thread已调用sleep()

// thread 处于 TIMED_WAITING 状态
System.out.println("Thread state: " + thread.getState()); // TIMED_WAITING
}
}

代码解释:

  • Thread.sleep(3000):让线程休眠3秒
  • 限时等待状态下,线程不占用 CPU
  • 时间到达后自动恢复到 RUNNABLE 状态

TERMINATED(终止)

1
2
3
4
5
6
7
8
9
10
11
12
13
public class TerminatedDemo {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
System.out.println("Task completed");
});

thread.start();
thread.join(); // 等待线程执行完毕

// thread 已执行完毕,进入 TERMINATED 状态
System.out.println("Thread state: " + thread.getState()); // TERMINATED
}
}

代码解释:

  • join():等待线程执行完毕
  • 当 run() 方法执行完毕后,线程状态变为 TERMINATED

5. 线程上下文切换

5.1 什么是线程上下文切换?

线程上下文切换(Context Switch)是指CPU从执行一个线程切换到执行另一个线程的过程。

CPU在同一时刻只能执行一个线程,当需要切换时:

  1. 保存当前线程的执行状态(寄存器、程序计数器、栈指针等)
  2. 恢复另一个线程的执行状态
  3. 切换到新线程执行

5.2 切换的时机

触发条件 说明
时间片用完 CPU时间片轮转调度
阻塞 线程等待I/O、锁、资源
优先级 高优先级线程抢占
主动让出 yield(), sleep(), wait()
中断 硬件中断、系统调用

5.3 上下文切换的过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
线程A运行中

│ 时间片耗尽/阻塞

┌─────────────────────┐
│ 1. 保存线程A状态 │
│ - PC (程序计数器) │
│ - Registers │
│ - Stack Pointer │
│ - CPU缓存 │
└─────────────────────┘

│ 调度器选择线程B

┌─────────────────────┐
│ 2. 恢复线程B状态 │
│ - 加载B的寄存器 │
│ - 切换栈空间 │
│ - 刷新CPU缓存 │
└─────────────────────┘


线程B开始运行

5.4 如何减少上下文切换?

  1. 减少线程数量:合理配置线程池大小
  2. 使用协程/虚拟线程:Java 21 虚拟线程
  3. 无锁化设计:CAS、Atomic、ConcurrentHashMap
  4. 减少阻塞:异步I/O、epoll
  5. 减少锁竞争:读写锁、分离锁
1
2
3
4
// 合理配置线程数
int cores = Runtime.getRuntime().availableProcessors(); // 获取CPU核心数
// I/O密集型:CPU核心数 * 2
ExecutorService pool = Executors.newFixedThreadPool(cores * 2);

代码解释:

  • Runtime.getRuntime().availableProcessors():获取CPU核心数
  • I/O密集型任务线程数设为 CPU核心数×2,可以在等待I/O时让更多线程执行

6. 为什么 wait() 方法不定义在 Thread 中?

6.1 核心原因

wait()对象 monitor(监视器) 的组成部分,不是线程的属性。

Java 的并发同步基于 Monitor 机制:

  • 每个对象有一把(monitor lock)
  • 每个对象有一个等待集(wait set)
  • wait() / notify() / notifyAll() 操作的是对象的等待集

设计思想:谁拥有资源,谁就负责等待通知,而不是线程本身。


6.2 对比:Object vs Thread

方法 定义位置 原因
wait() Object 操作对象的 monitor 等待集
notify() Object 唤醒对象的等待线程
sleep() Thread 让线程暂停执行
yield() Thread 让出 CPU 时间片
join() Thread 等待线程结束

6.3 代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class WaitDemo {
public static void main(String[] args) throws InterruptedException {
// 创建锁对象
Object lock = new Object();

// 线程1:等待在 lock 对象上
Thread t1 = new Thread(() -> {
synchronized (lock) { // 必须先获取 lock 的锁
try {
System.out.println("Thread1 waiting...");
lock.wait(); // 在 lock 的等待集中等待
System.out.println("Thread1 resumed");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});

// 线程2:唤醒 lock 对象上等待的线程
Thread t2 = new Thread(() -> {
synchronized (lock) { // 必须先获取 lock 的锁
System.out.println("Thread2 notifying...");
lock.notify(); // 唤醒 lock 对象等待集中的一个线程
}
});

t1.start();
Thread.sleep(100); // 确保t1已调用wait()
t2.start();
}
}

代码解释:

  • synchronized (lock):获取 lock 对象的监视器锁
  • lock.wait():释放锁并在此对象的等待集中等待
  • lock.notify():唤醒在此对象等待集中等待的一个线程
  • wait/notify 必须在 synchronized 内调用,这是 Java 语言规范

6.4 sleep() 和 wait() 的区别

区别 sleep() wait()
所属类 Thread Object
是否释放锁 不释放 释放
唤醒方式 自动醒来/ interrupt() notify()/notifyAll()
位置要求 任意位置 synchronized 内
状态 TIMED_WAITING WAITING / TIMED_WAITING

7. 可以直接调用 Thread 类的 run 方法吗?

7.1 答案:可以,但不推荐

可以直接调用,但它不会启动新线程,而是在当前线程中同步执行。

1
2
3
4
5
6
7
Thread t = new Thread(() -> System.out.println("Hello"));

// ❌ 错误:不会启动新线程
t.run(); // 在主线程中执行,等同于普通方法调用

// ✅ 正确:启动新线程
t.start(); // 创建新线程执行

7.2 run() vs start() 的区别

方法 run() start()
作用 执行线程逻辑 启动新线程
执行方式 同步(在当前线程) 异步(在新线程)
调用次数 可以多次调用 只能调用一次(否则抛异常)
返回值

7.3 start() 源码解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// Thread.java 源码简化
public class Thread implements Runnable {
private Runnable target; // 存储传入的Runnable任务

@Override
public void run() {
if (target != null) {
target.run(); // 直接调用传入的Runnable的run方法
}
}

public synchronized void start() {
// 1. 检查线程状态,确保线程未被启动过
if (threadStatus != 0) {
throw new IllegalThreadStateException();
}

// 2. 通知 JVM 启动线程
start0(); // 这是一个 native 方法,由JVM实现

// 3. 此处才是真正创建新线程
// run() 会在新线程中被调用
}

private native void start0(); // JVM 层面的线程创建
}

代码解释:

  • target:存储传入的 Runnable 对象
  • threadStatus:线程状态,确保 start() 只被调用一次
  • start0():native 方法,JVM 负责创建操作系统线程

7.4 执行流程对比

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
t.run() 执行流程:
主线程 ──────────────────────► run() 执行完毕

└── 直接调用,无新线程

t.start() 执行流程:
主线程 ──────────────────────► start() 返回


┌───────────────┐
│ JVM 创建新线程 │
│ 调用 start0() │
└───────────────┘


新线程 ──────────────────────► run() 执行完毕

8. 为什么要使用多线程?

8.1 核心原因

充分利用多核 CPU 资源,提升程序性能和响应速度。


8.2 单核时代 vs 多核时代

单核时代:提高 CPU 和 IO 系统整体利用率

1
2
3
4
5
6
7
8
9
10
11
单线程情况:
线程 ──────────► [IO阻塞] ──────────►
│ │
CPU空闲 CPU空闲

多线程情况:
线程1: ──────────► [IO阻塞]
线程2: ──────────► [使用CPU]
线程3: ──────────► [使用CPU]
└─────────────────────────────────►
高 CPU 利用率

核心目的:当一个线程被 IO 阻塞时,其他线程可以继续使用 CPU。


多核时代:充分利用多核 CPU 并行计算

1
2
3
4
5
6
7
8
9
10
11
12
13
单线程(只使用1个核心):
CPU核心1: ██████████████████████ ← 100% 使用
CPU核心2: ░░░░░░░░░░░░░░░░░░░░░ ← 闲置
CPU核心3: ░░░░░░░░░░░░░░░░░░░░░ ← 闲置
CPU核心4: ░░░░░░░░░░░░░░░░░░░░░ ← 闲置

多线程(使用多核):
CPU核心1: ██████████████████████
CPU核心2: ██████████████████████
CPU核心3: ██████████████████████
CPU核心4: ██████████████████████

理想加速比: T单核 → T单核/N核心

核心目的:将计算任务分配到多个 CPU 核心并行执行。


8.3 多线程的适用场景

场景 说明
I/O 密集型 网络请求、文件读写、数据库操作
并行计算 大数据处理、图像/视频处理
服务端并发 高并发 Web 服务、实时通信
后台任务 日志记录、定时任务、消息推送
UI 交互 Android/Web 前端,避免界面卡死

9. 单核 CPU 支持 Java 多线程吗?

9.1 答案:支持

Java 多线程不需要多核 CPU,单核 CPU 完全可以运行多线程程序。

9.2 单核 CPU 运行多线程的原理

单核 CPU 实际是 “伪并行”,通过 时间片轮转 实现线程切换:

1
2
3
4
5
6
7
8
9
10
11
12
时间线:
┌─────────────────────────────────────────────────────────────┐
│ CPU时间片 │
├─────────────────────────────────────────────────────────────┤
│ 0ms 10ms 20ms 30ms 40ms 50ms 60ms │
│ ├──┬───┤├──┬───┤├──┬───┤├──┬───┤├──┬───┤├──┬───┤ │
│ │T1│T2 ││T1│T2 ││T1│T2 ││T1│T2 ││T1│T2 ││T1│T2 │ │
│ └──┴───┘└──┴───┘└──┴───┘└──┴───┘└──┴───┘└──┴───┘ │
│ ↑ ↑ ↑ ↑ ↑ ↑ │
│ 切换 切换 切换 切换 切换 切换 │
└─────────────────────────────────────────────────────────────┘
T1 = 线程1 T2 = 线程2
  • 每个线程轮流使用 CPU 的时间片
  • 微观上串行,宏观上并行
  • 称为 “并发 (Concurrency)”,不是”并行 (Parallelism)”

9.3 单核 vs 多核

维度 单核 CPU 多核 CPU
同时执行线程数 1 N (核心数)
并行方式 并发 (时间片) 并行 (真正同时)
适合任务 I/O 密集型 CPU 密集型
多线程效果 避免阻塞 真正加速

10. 单核 CPU 上运行多个线程效率一定会高吗?

10.1 答案:不一定

多线程并非总是高效,在某些情况下单线程反而更好

10.2 什么时候多线程效率低?

1. CPU 密集型任务

情况 单线程 多线程
计算1+2+…+1亿 ~200ms ~250ms(含切换开销)

原因:没有 I/O 阻塞,线程切换只会增加开销,无并行收益。


2. 线程数量过多

1
2
3
4
5
线程切换开销:
时间线:
[线程1执行][切换][线程2执行][切换][线程3][切换]...
↑ ↑ ↑
实际工作时间少,切换开销大

3. 任务过于简单

1
2
3
4
5
// 任务太简单,线程创建/切换成本 > 任务本身
new Thread(() -> System.out.println("Hi")).start();
// 创建线程开销:约 1-2ms
// 任务执行:约 0.01ms
// 得不偿失!

10.3 什么时候多线程效率高?

任务类型 单核多线程效果 原因
CPU 密集型 ❌ 更慢 线程切换开销 > 并行收益
I/O 密集型 ✅ 更快 等待时切换执行其他任务
混合型 ✅ 视情况 I/O 占比高则有效
简单短任务 ❌ 更慢 创建/切换成本太高

11. 死锁

11.1 什么是死锁?

死锁是指两个或多个线程相互持有对方需要的资源,导致所有线程都无法继续执行。

1
2
3
4
线程A: 持有锁1,等待锁2
线程B: 持有锁2,等待锁1

结果: 无限等待,形成死锁

11.2 死锁示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class DeadLockDemo {
// 两把锁
private static final Object lock1 = new Object();
private static final Object lock2 = new Object();

public static void main(String[] args) {
// 线程1: 先锁1,再锁2
Thread thread1 = new Thread(() -> {
synchronized (lock1) { // 获取 lock1
System.out.println("Thread1: 持有锁1");
try {
Thread.sleep(100); // 模拟业务处理
} catch (InterruptedException e) {}
// 尝试获取 lock2
synchronized (lock2) {
System.out.println("Thread1: 持有锁2");
}
}
}, "Thread1");

// 线程2: 先锁2,再锁1
Thread thread2 = new Thread(() -> {
synchronized (lock2) { // 获取 lock2
System.out.println("Thread2: 持有锁2");
try {
Thread.sleep(100);
} catch (InterruptedException e) {}
// 尝试获取 lock1
synchronized (lock1) {
System.out.println("Thread2: 持有锁1");
}
}
}, "Thread2");

thread1.start();
thread2.start();
}
}
// 运行结果: 程序卡死,两个线程互相等待

代码解释:

  • synchronized (lock1):获取 lock1 的监视器锁
  • Thread.sleep(100):模拟业务处理,让另一个线程有足够时间获取另一把锁
  • 两个线程互相持有对方需要的锁,形成循环等待 → 死锁

11.3 死锁的四个必要条件 (Coffman 条件)

条件 说明 打破方法
1. 互斥条件 资源一次只能被一个线程持有 无法打破
2. 占有并等待 线程持有资源同时等待其他资源 一次性获取所有资源
3. 不可抢占 资源不能被强制抢夺 设置超时/可抢占
4. 循环等待 线程形成循环等待 规定加锁顺序

12. 预防和避免线程死锁

12.1 预防死锁:破坏必要条件

1. 破坏”占有并等待”:一次性获取所有资源

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// ❌ 逐个获取:可能死锁
synchronized (lockA) {
// 持有lockA
synchronized (lockB) {
// 获取lockB
}
}

// ✅ 一次性获取:使用 tryLock
Lock lockA = new ReentrantLock();
Lock lockB = new ReentrantLock();

public void safeMethod() {
// 尝试同时获取两把锁
while (true) {
boolean acquiredA = lockA.tryLock(); // 尝试获取lockA,不阻塞
boolean acquiredB = lockB.tryLock();

if (acquiredA && acquiredB) {
try {
// 两把锁都获取成功,执行逻辑
System.out.println("执行业务逻辑");
} finally {
lockA.unlock();
lockB.unlock();
}
break;
} else {
// 获取失败,释放已获取的锁
if (acquiredA) lockA.unlock();
if (acquiredB) lockB.unlock();
// 短暂等待后重试
try {
Thread.sleep(10);
} catch (InterruptedException e) {}
}
}
}

代码解释:

  • tryLock():尝试获取锁,立即返回(不阻塞)
  • 返回 true 表示获取成功,false 表示获取失败
  • 循环尝试直到获取成功,或者选择放弃

2. 破坏”不可抢占”:设置超时

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// ❌ synchronized:不可抢占
synchronized (lock) {
// 无限等待
}

// ✅ ReentrantLock:可抢占
ReentrantLock lock = new ReentrantLock();

public void tryLockMethod() {
try {
// 尝试获取锁,等待最多1秒
boolean acquired = lock.tryLock(1, TimeUnit.SECONDS);

if (acquired) {
try {
// 获取成功,执行逻辑
System.out.println("执行业务逻辑");
} finally {
lock.unlock(); // 释放锁
}
} else {
// 获取失败,回退/重试
System.out.println("获取锁失败,执行其他逻辑");
}
} catch (InterruptedException e) {
// 处理线程中断
e.printStackTrace();
}
}

代码解释:

  • tryLock(timeout, unit):尝试获取锁,最长等待指定时间
  • 超时后返回 false,线程可以继续执行其他逻辑
  • 配合 finally 确保锁一定会被释放

3. 破坏”循环等待”:固定加锁顺序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// ❌ 无顺序:可能死锁
// 线程1: lock1 → lock2
// 线程2: lock2 → lock1

// ✅ 统一顺序:按对象hash排序
public void transfer(Account from, Account to, int amount) {
// 按账户ID排序,确保顺序一致
Account first = from.getId() < to.getId() ? from : to;
Account second = from.getId() < to.getId() ? to : from;

// 统一先锁first,再锁second
synchronized (first) {
synchronized (second) {
from.withdraw(amount);
to.deposit(amount);
}
}
}

// ✅ 使用 System.identityHashCode 排序
private void safeMethod(Object lock1, Object lock2) {
int hash1 = System.identityHashCode(lock1);
int hash2 = System.identityHashCode(lock2);

if (hash1 < hash2) {
synchronized (lock1) {
synchronized (lock2) {
// 业务逻辑
}
}
} else {
synchronized (lock2) {
synchronized (lock1) {
// 业务逻辑
}
}
}
}

代码解释:

  • getId()System.identityHashCode():获取对象的唯一标识
  • 根据标识排序,确保所有线程以相同顺序获取锁
  • 打破循环等待条件

12.2 避免死锁:运行时检测

银行家算法(了解即可)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 简化版安全性检查
public class SafeLockDetector {
private ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();

// 检测死锁
public boolean detectDeadlock() {
// 查找死锁线程
long[] deadlockedThreads = threadMXBean.findDeadlockedThreads();

if (deadlockedThreads != null && deadlockedThreads.length > 0) {
System.out.println("检测到死锁!涉及线程数: " + deadlockedThreads.length);
return true;
}
return false;
}

// 定期检测
public void startMonitoring() {
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
scheduler.scheduleAtFixedRate(() -> {
if (detectDeadlock()) {
// 记录日志、发送告警
System.out.println("发生死锁,需要处理!");
}
}, 0, 5, TimeUnit.SECONDS);
}
}

代码解释:

  • ThreadMXBean:JVM 线程管理 Bean
  • findDeadlockedThreads():查找处于死锁状态的线程
  • 返回线程 ID 数组,可以进一步分析

12.3 实际开发最佳实践

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
public class BestPracticeDemo {
private final ReentrantLock lock1 = new ReentrantLock();
private final ReentrantLock lock2 = new ReentrantLock();

// 1. 尽量减少锁的使用范围
public void method1() {
// 只在需要同步的地方加锁
lock1.lock();
try {
// 最小同步范围
performTask();
} finally {
lock1.unlock();
}
// 其他不需要同步的代码
}

// 2. 使用读写锁(读多写少场景)
private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();

public void read() {
rwLock.readLock().lock();
try {
// 读操作,多个线程可以同时读
System.out.println("Reading...");
} finally {
rwLock.readLock().unlock();
}
}

public void write() {
rwLock.writeLock().lock();
try {
// 写操作,独占
System.out.println("Writing...");
} finally {
rwLock.writeLock().unlock();
}
}

// 3. 避免嵌套锁
// ❌ 嵌套锁:容易死锁
synchronized (lockA) {
synchronized (lockB) {
// ...
}
}

// ✅ 提取方法,减少嵌套
public void safeMethod() {
methodA();
methodB();
}

private void methodA() {
synchronized (lockA) {
// 逻辑A
}
}

private void methodB() {
synchronized (lockB) {
// 逻辑B
}
}
}

总结

本文整理了 Java 多线程面试的核心知识点:

序号 知识点 重要程度
1 进程与线程的概念 ⭐⭐⭐
2 线程与进程的关系、区别、优缺点 ⭐⭐⭐⭐
3 Java 创建线程的多种方式 ⭐⭐⭐⭐⭐
4 线程生命周期和6种状态 ⭐⭐⭐⭐⭐
5 线程上下文切换 ⭐⭐⭐⭐
6 wait() 为何定义在 Object 中 ⭐⭐⭐⭐
7 run() vs start() ⭐⭐⭐⭐
8 为什么要使用多线程 ⭐⭐⭐⭐⭐
9 单核 CPU 与多线程 ⭐⭐⭐⭐
10 多线程效率问题 ⭐⭐⭐⭐
11 死锁概念 ⭐⭐⭐⭐⭐
12 死锁预防与避免 ⭐⭐⭐⭐⭐

参考资料:JavaGuide - Java并发编程面试题总结
原文链接:https://javaguide.cn/java/concurrent/java-concurrent-questions-01.html