文章目录
- 一、多线程概述
-
- [1.1 并发与并行](#1.1 并发与并行)
- [1.2 线程与进程](#1.2 线程与进程)
- [1.3 多线程的核心优势](#1.3 多线程的核心优势)
- 二、多线程的创建
-
- [2.1 方式 1:继承 Thread 类](#2.1 方式 1:继承 Thread 类)
- [2.2 方式 2:实现 Runnable 接口](#2.2 方式 2:实现 Runnable 接口)
- [2.3 方式 3:实现 Callable 接口(有返回值 + 可抛异常)](#2.3 方式 3:实现 Callable 接口(有返回值 + 可抛异常))
- [2.4 Thread 与 Runnable 的核心区别及使用场景](#2.4 Thread 与 Runnable 的核心区别及使用场景)
- [2.5 方式 4:线程池创建](#2.5 方式 4:线程池创建)
- [三、Thread 类的核心 API](#三、Thread 类的核心 API)
-
- [3.1 构造方法](#3.1 构造方法)
- [3.2 核心成员方法](#3.2 核心成员方法)
- [3.3 线程调度相关方法](#3.3 线程调度相关方法)
- 四、线程的生命周期
-
- [4.1 生命周期概述](#4.1 生命周期概述)
- [4.2 各状态核心说明](#4.2 各状态核心说明)
- [4.3 线程状态转换](#4.3 线程状态转换)
- 五、线程安全
-
- [5.1 线程安全的定义](#5.1 线程安全的定义)
- [5.2 线程安全问题的产生条件与解决策略](#5.2 线程安全问题的产生条件与解决策略)
- [5.3 同步代码块](#5.3 同步代码块)
- [5.4 同步方法](#5.4 同步方法)
- [5.5 Lock 锁(显式锁)](#5.5 Lock 锁(显式锁))
- 六、死锁
-
- [6.1 死锁的定义](#6.1 死锁的定义)
- [6.2 死锁的产生条件(四大必要条件)](#6.2 死锁的产生条件(四大必要条件))
- [6.3 死锁的代码演示](#6.3 死锁的代码演示)
- [6.4 死锁的解决策略](#6.4 死锁的解决策略)
- [6.5 死锁的排查与避免](#6.5 死锁的排查与避免)
- 七、线程间通信
-
- [7.1 线程间通信的定义](#7.1 线程间通信的定义)
- [7.2 线程间通信的核心机制:等待唤醒机制](#7.2 线程间通信的核心机制:等待唤醒机制)
- [7.3 生产者 - 消费者案例](#7.3 生产者 - 消费者案例)

一、多线程概述
1.1 并发与并行
核心定义
- 并发:指两个或多个事件在同一个时间段内发生,宏观上看似同时执行,微观上实际是分时交替执行。
- 并行:指两个或多个事件在同一时刻发生,多个任务真正意义上的同时执行,需要多核 CPU 支持。
底层执行逻辑
- 单核 CPU 系统:仅支持并发,CPU 通过时间片轮转的方式为多个任务分配执行时间,每个任务执行极短的时间后切换到下一个任务,由于切换速度极快,给用户 "同时执行" 的视觉感受。
- 多核 CPU 系统:同时支持并发与并行,多个 CPU 核心可同时处理不同的任务,实现真正的并行;同时单个核心仍可通过时间片轮转处理多个任务,实现并发。
注意事项
单核处理器无法实现真正的并行,多线程在单核下本质是并发执行。
无论多核还是单核,Java 中线程的调度均由 JVM 控制,宏观上的并行不代表微观上的同时执行。
1.2 线程与进程
-
进程(Process)
定义:是内存中运行的应用程序,拥有独立的内存空间(堆、方法区等),是操作系统运行程序的基本单位。
特征:一个应用程序可同时运行多个进程;进程的生命周期包含创建、运行、阻塞、消亡,系统运行一个程序即对应一个进程的完整生命周期。
示例:打开电脑上的微信、IDEA、浏览器,每一个应用对应一个独立的进程。
-
线程(Thread)
定义:是进程中的一个执行单元,负责执行进程中的程序逻辑,是操作系统调度的最小单位。
特征:一个进程中至少包含一个主线程(如 Java 程序的 main 方法对应主线程),也可创建多个子线程,拥有多线程的程序称为多线程程序;线程共享所属进程的内存空间(堆、方法区),但拥有自己独立的栈空间。
进程与线程的核心区别
进程与线程的核心区别
| 对比维度 | 进程 | 线程 |
|---|---|---|
| 内存空间 | 拥有独立的内存空间,进程间相互隔离 | 共享所属进程的内存空间,线程间数据可直接交互 |
| 资源消耗 | 创建、销毁、切换的资源消耗大 | 创建、销毁、切换的资源消耗小,轻量级进程 |
| 独立性 | 进程间相互独立,一个进程崩溃不影响其他进程 | 线程间依赖进程,一个线程崩溃可能导致整个进程崩溃 |
| 调度单位 | 操作系统的基本运行单位 | 操作系统的最小调度单位 |
线程调度机制
JVM 采用抢占式调度(Java 默认),而非分时调度,核心规则:
优先级高的线程优先获取 CPU 执行权;
线程优先级相同时,JVM 随机选择一个线程执行(存在线程随机性);
一个线程获取 CPU 执行权后,会一直执行直到执行完毕、主动放弃、被高优先级线程抢占。
1.3 多线程的核心优势
- 提高CPU 的利用率:让 CPU 在等待某个任务(如 IO 操作、网络请求)时,可切换执行其他任务,避免 CPU 空闲;
- 提升程序的响应速度:如桌面应用中,主线程处理界面交互,子线程处理耗时操作(如文件下载、数据计算),避免界面卡死;
- 实现任务的并行处理:如多线程下载、多线程处理数据,提升任务处理效率。
注意:多线程不会提高单个任务的执行速度,仅能提升多任务场景下的整体执行效率。
二、多线程的创建
Java 中所有线程对象均为java.lang.Thread类或其子类的实例,线程的执行逻辑封装在线程执行体(run()方法)中。核心创建方式有 3 种,拓展方式为线程池创建,共 4 种实现方案。
2.1 方式 1:继承 Thread 类
实现步骤
- 定义Thread类的子类,重写run()方法:run()方法的方法体为线程的执行逻辑,即线程执行体;
- 创建 Thread 子类的实例,即创建线程对象;
- 调用线程对象的 **start()方法 **:启动线程,JVM 会自动调用该线程的run()方法。
代码实现
java
// 定义Thread子类,重写run方法
public class MyThread extends Thread {
@Override
public void run() {
// 线程执行逻辑:打印10次HelloWorld
for (int i = 0; i < 10; i++) {
System.out.println("子线程:HelloWorld! " + i);
}
}
}
// 测试类
public class ThreadTest {
public static void main(String[] args) {
// 1. 创建自定义线程对象
MyThread mt = new MyThread();
// 2. 启动线程:必须调用start(),而非直接调用run()
mt.start();
// 主线程执行逻辑:打印10次main线程信息
for (int i = 0; i < 10; i++) {
System.out.println("main线程:执行中 " + i);
}
}
}
关键注意事项
- 启动线程必须调用start()方法,而非直接调用run()方法:
- 调用start():会创建新的线程,将线程状态从新建态转为就绪态,等待 JVM 调度后执行run();
- 直接调用run():不会创建新线程,仅在当前线程(如主线程)中执行run()方法的代码,属于普通方法调用。
- 一个线程对象的start()方法仅能调用一次,多次调用会抛出IllegalThreadStateException。
2.2 方式 2:实现 Runnable 接口
实现步骤
- 定义Runnable接口的实现类,重写run()方法:封装线程执行逻辑;
- 创建Runnable实现类的实例:该实例为线程任务对象,仅封装执行逻辑,并非线程对象;
- 创建Thread对象,将 Runnable 实例作为构造方法参数:Thread 对象才是真正的线程对象,关联线程任务;
- 调用 Thread 对象的start()方法,启动线程。
代码实现
java
// 定义Runnable接口实现类,重写run方法
public class MyRunnable implements Runnable {
@Override
public void run() {
// 线程执行逻辑:打印20次HelloWorld
for (int i = 0; i < 20; i++) {
System.out.println("子线程:HelloWorld! " + i);
}
}
}
// 测试类
public class RunnableTest {
public static void main(String[] args) {
// 1. 创建线程任务对象
MyRunnable mr = new MyRunnable();
// 2. 创建线程对象,关联任务对象
Thread t = new Thread(mr);
// 3. 启动线程
t.start();
// 主线程执行逻辑
for (int i = 0; i < 20; i++) {
System.out.println("main线程:执行中 " + i);
}
}
}
简化写法:匿名内部类
java
public class RunnableAnonymousTest {
public static void main(String[] args) {
// 匿名内部类实现Runnable,直接创建线程对象
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("匿名子线程:执行中 " + i);
}
}
}).start();
// 主线程逻辑
for (int i = 0; i < 10; i++) {
System.out.println("main线程:执行中 " + i);
}
}
}
2.3 方式 3:实现 Callable 接口(有返回值 + 可抛异常)
核心优势
- 相比Runnable,Callable接口的call()方法有返回值,可获取线程执行结果;
- call()方法可抛出受检异常,无需在方法内部捕获,便于异常处理。
实现步骤
- 定义Callable接口的实现类,重写call()方法:V为返回值类型,方法体为线程执行逻辑;
- 创建Callable实现类的实例;
- 创建FutureTask对象,将 Callable 实例作为构造方法参数:FutureTask是RunnableFuture的实现类,既实现了Runnable(可作为 Thread 构造参数),又实现了Future(可获取返回值);
- 创建Thread对象,将FutureTask对象作为构造方法参数;
- 调用 Thread 对象的start()方法,启动线程;
- 调用FutureTask的 **get()方法 **:获取线程执行的返回值,该方法为阻塞方法,会等待线程执行完毕后返回结果。
代码实现
java
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
// 定义Callable实现类,指定返回值类型为Integer
public class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
// 线程执行逻辑:计算1-100的和
int sum = 0;
for (int i = 1; i <= 100; i++) {
sum += i;
}
return sum; // 返回执行结果
}
}
// 测试类
public class CallableTest {
public static void main(String[] args) throws Exception {
// 1. 创建Callable实例
MyCallable mc = new MyCallable();
// 2. 创建FutureTask对象,关联Callable
FutureTask<Integer> ft = new FutureTask<>(mc);
// 3. 创建Thread对象,关联FutureTask
Thread t = new Thread(ft);
// 4. 启动线程
t.start();
// 5. 获取返回值:get()为阻塞方法,等待线程执行完毕
Integer result = ft.get();
System.out.println("子线程执行结果:1-100的和为 " + result);
}
}
2.4 Thread 与 Runnable 的核心区别及使用场景
本质区别
- 继承 Thread:线程对象与线程任务耦合在一起,线程对象本身就是任务对象;
- 实现 Runnable:线程对象与线程任务解耦,Runnable 实例封装任务,Thread 实例封装线程,一个任务可被多个线程执行。
实现 Runnable 接口的优势
- 避免 Java 单继承的局限性:一个类继承 Thread 后,无法再继承其他类;实现 Runnable 接口后,仍可继承其他类;
- 便于实现资源共享:多个线程可共享同一个 Runnable 任务对象,适合多线程操作同一资源的场景(如卖票、抢票);
- 提高程序健壮性:任务与线程分离,代码可被多个线程共享,便于维护和扩展;
- 适配线程池:线程池仅能接收Runnable或Callable类型的任务,无法直接接收继承 Thread 的类。
使用场景选择
- 继承 Thread:适合简单的多线程场景,线程任务与线程对象一一对应,无需资源共享;
- 实现 Runnable:适合多线程操作同一资源的场景,或需要继承其他类的场景,是实际开发中的首选方式;
- 实现 Callable:适合需要获取线程执行结果或处理受检异常的场景。
2.5 方式 4:线程池创建
线程池是 Java 中管理线程的高级方式,可复用线程、减少资源消耗,是实际开发中最常用的多线程创建方式,后续单独讲解。也可以查看我另一篇文章Java多线程自定义线程池------线程池的七大参数和四大拒绝策略
三、Thread 类的核心 API
3.1 构造方法
Thread 类提供了多个重载的构造方法,核心常用构造方法如下:
| 构造方法 | 说明 |
|---|---|
| public Thread() | 创建一个无名称的线程对象 |
| public Thread(String name) | 创建一个指定名称的线程对象 |
| public Thread(Runnable target) | 创建一个关联 Runnable 任务的线程对象,使用默认名称 |
| public Thread(Runnable target, String name) | 创建一个关联 Runnable 任务且指定名称的线程对象 |
代码实现
java
public class ThreadConstructorTest {
public static void main(String[] args) {
// 1. 无参构造
Thread t1 = new Thread();
// 2. 指定名称构造
Thread t2 = new Thread("我的线程2");
// 3. 关联Runnable任务
Runnable r = new Runnable() {
@Override
public void run() {
System.out.println("线程任务执行");
}
};
Thread t3 = new Thread(r);
// 4. 关联Runnable任务并指定名称
Thread t4 = new Thread(r, "任务线程4");
// 启动线程
t1.start();
t2.start();
t3.start();
t4.start();
}
}
3.2 核心成员方法
线程名称相关方法
- public String getName():获取当前线程的名称;
- public void setName(String name):设置当前线程的名称;
- 注意:主线程的默认名称为main,子线程的默认名称为Thread-0、Thread-1、Thread-2...
线程启动与执行方法
- public void start():启动线程,将线程从新建态转为就绪态,JVM 自动调用run()方法;
- public void run():线程执行体,封装线程的核心执行逻辑,由 JVM 调用,无需手动调用。
获取当前线程对象
- public static Thread currentThread():静态方法,返回当前正在执行的线程对象的引用;
- 适用场景:在任意代码位置获取当前执行的线程,如在 Runnable 的run()方法中获取线程对象。
代码演示
java
public class ThreadMethodTest {
public static void main(String[] args) {
// 创建线程对象,未指定名称
Thread t = new Thread(new Runnable() {
@Override
public void run() {
// 获取当前线程对象(子线程)
Thread current = Thread.currentThread();
// 获取线程名称
System.out.println("子线程原名称:" + current.getName());
// 设置线程名称
current.setName("业务处理线程");
System.out.println("子线程新名称:" + current.getName());
}
});
// 启动线程
t.start();
// 获取主线程对象并设置名称
Thread mainThread = Thread.currentThread();
mainThread.setName("主业务线程");
System.out.println("主线程名称:" + mainThread.getName());
}
}
start () 与 run () 方法的核心区别
| 对比维度 | start () 方法 | run () 方法 |
|---|---|---|
| 调用者 | 由开发人员手动调用 | 由 JVM 自动调用,无需手动调用 |
| 线程创建 | 会创建新的线程,分配独立的栈空间 | 不会创建新线程,仅在当前线程中执行代码 |
| 状态转换 | 将线程从新建态转为就绪态 | 将线程从就绪态转为运行态 |
| 执行次数 | 一个线程对象仅能调用一次, | 多次调用抛异常 可被多次调用,属于普通方法 |
| 返回值 | void,无返回值 | void,无返回值(Callable 的 call () 有返回值) |
3.3 线程调度相关方法
Thread 类提供了一系列用于线程调度的方法,用于控制线程的执行顺序、状态,核心方法如下:
| 方法名 | 修饰符 | 说明 |
|---|---|---|
| public static void sleep(long millis) | 静态 | 让当前线程休眠指定毫秒数(millis),休眠期间线程进入阻塞态,释放 CPU 执行权,不释放锁资源;休眠结束后转为就绪态,等待 JVM 调度;可抛出InterruptedException |
| public final void join() | 非静态 | 等待该线程终止后,其他线程才能继续执行;调用该方法的线程进入阻塞态,直到目标线程执行完毕;可抛出InterruptedException |
| public final void join(long millis) | 非静态 | 等待该线程终止,最多等待指定毫秒数,超时后不再等待 |
| public static void yield() | 静态 | 线程礼让,让当前正在执行的线程主动放弃 CPU 执行权,转为就绪态,与其他就绪态线程重新争夺 CPU;礼让是自愿的,JVM 可能忽略该请求 |
| public final void setDaemon(boolean on) | 非静态 | 将该线程标记为守护线程(on=true)或用户线程(on=false);必须在start()方法前调用;当 JVM 中所有运行的线程都是守护线程时,JVM 会自动退出 |
| public final void setPriority(int newPriority) | 非静态 | 设置线程的优先级,取值范围 1-10,默认 5;优先级越高,获取 CPU 的概率越大 |
| public final int getPriority() | 非静态 | 获取线程的优先级 |
| public void interrupt() | 非静态 | 中断线程,将线程的中断状态置为 true;若线程处于 sleep/join/wait 状态,会抛出InterruptedException并清除中断状态 |
| public static boolean interrupted() | 静态 | 判断当前线程是否被中断,会清除中断状态(连续调用两次,第二次返回 false) |
| public boolean isInterrupted() | 非静态 | 判断当前线程是否被中断,不会清除中断状态 |
关键方法代码演示
- 线程休眠(sleep ())
java
import java.util.Date;
public class ThreadSleepTest {
public static void main(String[] args) {
Thread t = new Thread("休眠线程") {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(getName() + ":" + i + ",当前时间:" + new Date());
try {
// 休眠1秒(1000毫秒)
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
t.start();
}
}
- 线程加入(join ())
java
public class ThreadJoinTest {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread("子线程1") {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(getName() + ":" + i);
}
}
};
t1.start();
// 让主线程等待t1执行完毕后再执行
t1.join();
// 主线程逻辑
for (int i = 0; i < 5; i++) {
System.out.println("主线程:" + i);
}
}
}
- 线程礼让(yield ())
java
public class ThreadYieldTest {
public static void main(String[] args) {
Thread t1 = new Thread("礼让线程1") {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(getName() + ":" + i);
// 线程礼让
Thread.yield();
}
}
};
Thread t2 = new Thread("线程2") {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(getName() + ":" + i);
}
}
};
t1.start();
t2.start();
}
}
- 守护线程(setDaemon ())
java
public class ThreadDaemonTest {
public static void main(String[] args) {
Thread t1 = new Thread("守护线程1") {
@Override
public void run() {
// 死循环,若为守护线程,主线程结束后该线程会自动退出
while (true) {
System.out.println(getName() + ":运行中");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
// 设置为守护线程,必须在start()前
t1.setDaemon(true);
t1.start();
// 主线程执行5次后结束
for (int i = 0; i < 5; i++) {
System.out.println("主线程:" + i);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("主线程执行完毕,JVM即将退出");
}
}
- 线程停止
stop():让线程停止,过时了,但是还可以使用。
interrupt():中断线程。 把线程的状态终止,并抛出一个InterruptedException。
java
public class ThreadStopTst {
public static void main(String[] args) {
// 匿名内部类实现Runnable,直接创建线程对象
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("开始执行:"+new Date());
try {
Thread.sleep(10000);//睡了10秒
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("结果执行:"+new Date());
}
}).start();
try {
//如果你睡3秒还不睡醒,就干掉你
Thread.sleep(3000);
// t1.stop();//已过时,但可以用。如果后面还有代码就不没去运行
t1.interrupt();//判断当前线程中断,不影响后面的执行
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
四、线程的生命周期
4.1 生命周期概述
Java 中线程的生命周期包含6 种核心状态,状态之间可通过 Thread 类的方法相互转换
JVM 通过java.lang.Thread.State枚举定义了这 6 种状态:
- NEW(新建态)
- RUNNABLE(就绪态 / 运行态)
- BLOCKED(阻塞态)
- WAITING(等待态)
- TIMED_WAITING(计时等待态)
- TERMINATED(消亡态)
4.2 各状态核心说明
- NEW(新建态)
状态描述:创建了线程对象,但未调用start()方法;
示例:Thread t = new Thread();,此时 t 处于新建态;
转换条件:调用start()方法,转为RUNNABLE。 - RUNNABLE(就绪态 / 运行态)
状态描述:JVM 中将就绪态和运行态合并为 RUNNABLE,包含两种子状态:
就绪态:调用start()方法后,线程等待 JVM 分配 CPU 执行权,具备执行资格,但无执行权;
运行态:线程获取 CPU 执行权,正在执行run()方法的代码,具备执行资格和执行权;
转换条件:
就绪态 → 运行态:JVM 调度,分配 CPU 执行权;
运行态 → 就绪态:线程礼让(yield ())、被高优先级线程抢占、时间片用完;
运行态 → 其他状态:调用 sleep ()/join ()/wait ()、获取锁失败、线程执行完毕。 - BLOCKED(阻塞态)
状态描述:线程因获取同步锁(synchronized)失败而进入的状态,等待其他线程释放锁;
转换条件:
进入:运行态线程尝试获取 synchronized 锁,锁已被其他线程持有;
退出:其他线程释放 synchronized 锁,当前线程获取锁成功,转为RUNNABLE。 - WAITING(等待态)
状态描述:线程进入无时间限制的等待,需依靠其他线程的显式唤醒才能退出等待;
进入方式:调用无参的wait()、join()、LockSupport.park()方法;
转换条件:
唤醒:其他线程调用notify()/notifyAll()(对应 wait ())、目标线程执行完毕(对应 join ())、LockSupport.unpark()(对应 park ()),转为BLOCKED(需获取锁)或RUNNABLE;
中断:其他线程调用interrupt(),抛出InterruptedException,转为RUNNABLE。 - TIMED_WAITING(计时等待态)
状态描述:线程进入有时间限制的等待,到达指定时间后自动唤醒,也可被其他线程显式唤醒;
进入方式:调用带参的sleep(long millis)、wait(long millis)、join(long millis)、LockSupport.parkNanos()、LockSupport.parkUntil();
转换条件:
自动唤醒:等待时间到达,转为BLOCKED或RUNNABLE;
显式唤醒:其他线程调用notify()/notifyAll()等,转为BLOCKED或RUNNABLE;
中断:其他线程调用interrupt(),抛出InterruptedException,转为RUNNABLE。 - TERMINATED(消亡态)
状态描述:线程的run()方法执行完毕,或因异常导致线程终止;
进入方式:
正常终止:run()/call()方法执行完毕;
异常终止:线程执行过程中抛出未捕获的异常,导致线程终止;
注意:消亡态的线程无法再次启动,调用start()会抛出IllegalThreadStateException。
4.3 线程状态转换

java
新建态(NEW) →[调用 start()]→ 就绪态(RUNNABLE)
就绪态(RUNNABLE) →[获得CPU时间片]→ 运行态(RUNNABLE)
运行态(RUNNABLE)
├─[yield()/时间片用完/高优先级抢占] → 回到 就绪态(RUNNABLE)
├─[获取 synchronized 锁失败] → 进入 阻塞态(BLOCKED)
├─[sleep(long)/wait(long)/join(long)] → 进入 计时等待(TIMED_WAITING)
├─[wait()/join()] → 进入 无限等待(WAITING)
└─[run()执行完毕/异常终止] → 进入 消亡态(TERMINATED)
阻塞态(BLOCKED) →[成功获取锁] → 回到 就绪态(RUNNABLE)
计时等待(TIMED_WAITING)
├─[时间到]
├─[被 notify()/notifyAll()]
└─[被 interrupt()]
→ 回到 就绪态/阻塞态
无限等待(WAITING)
├─[被 notify()/notifyAll()]
├─[join() 线程执行完毕]
└─[被 interrupt()]
→ 回到 就绪态/阻塞态
五、线程安全
5.1 线程安全的定义
当多个线程同时访问同一个共享资源,且至少有一个线程对该资源进行写操作时,若程序的执行结果与单线程执行结果一致,且共享资源的状态与预期一致,则称该程序是线程安全的;反之则为线程不安全。
线程不安全的本质
- 共享资源:多个线程共同访问的变量、对象、文件等(如堆中的对象、静态变量);
- 原子性缺失:对共享资源的写操作并非原子操作(即操作不可被分割,要么全部执行,要么全部不执行),多个线程的操作会相互干扰。
线程不安全案例:电影院卖票
电影院有 100 张票,3 个售票窗口(3 个线程)同时卖票,模拟卖票过程,观察线程安全问题。
代码实现(线程不安全版)
java
// 卖票任务类:实现Runnable,共享票源
public class TicketRunnable implements Runnable {
// 共享资源:100张票,多个线程共享同一个对象的该变量
private int ticket = 100;
@Override
public void run() {
// 死循环,持续卖票
while (true) {
// 判断票是否存在
if (ticket > 0) {
try {
// 休眠10毫秒,放大线程安全问题(模拟售票耗时)
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 卖票:打印当前售票窗口和票号,票号减1
System.out.println(Thread.currentThread().getName() + ":正在卖第" + ticket + "张票");
ticket--;
}
}
}
}
// 测试类:创建3个线程模拟3个售票窗口
public class TicketTest {
public static void main(String[] args) {
// 创建共享的卖票任务对象
TicketRunnable tr = new TicketRunnable();
// 创建3个线程,关联同一个任务对象
Thread t1 = new Thread(tr, "窗口1");
Thread t2 = new Thread(tr, "窗口2");
Thread t3 = new Thread(tr, "窗口3");
// 启动线程
t1.start();
t2.start();
t3.start();
}
}
运行结果问题分析
运行代码后,会出现以下线程不安全现象:
- 重复卖票:同一张票被多个窗口卖出(如窗口 1 和窗口 2 同时卖出第 1 张票);
- 卖出负票:票号变为 0 或负数后,仍有窗口卖票(如窗口 3 卖出第 - 1 张票)。
问题原因
对共享资源ticket的操作并非原子操作,if (ticket > 0)和ticket--是两个独立的步骤,多个线程在执行过程中会相互干扰:
例如:线程 1 执行if (ticket > 0)后,ticket=1,此时 CPU 被线程 2 抢占;
- 线程 2 也执行if (ticket > 0),ticket 仍 = 1,然后线程 2 休眠,CPU 被线程 1 抢占;
- 线程 1 继续执行,卖出第 1 张票,ticket=0;
- 线程 2 休眠结束,继续执行,卖出第 1 张票,ticket=-1,出现重复卖票和负票问题。
5.2 线程安全问题的产生条件与解决策略
线程安全问题必须同时满足以下 3 个条件,缺一不可:
- 多线程环境:存在两个或以上的线程同时执行;
- 共享资源:多个线程访问同一个共享资源(如全局变量、静态变量、堆对象);
- 写操作:至少有一个线程对共享资源进行写操作(修改、删除、新增),若仅为读操作,则不会出现线程安全问题。
线程安全的解决策略:同步机制
解决线程安全问题的核心思路是保证对共享资源的操作是原子操作,让多个线程串行执行对共享资源的写操作,避免相互干扰。Java 中提供了 3 种核心的同步机制:
- 同步代码块(synchronized);
- 同步方法(synchronized);
- Lock 锁(java.util.concurrent.locks.Lock)。
核心原理
通过加锁的方式,让多个线程在访问共享资源时,必须先获取锁,只有获取锁的线程才能执行操作,其他线程需等待锁释放后再争夺,从而保证操作的原子性。
5.3 同步代码块
语法格式
java
synchronized(同步锁对象) {
// 需保证线程安全的代码(对共享资源的写操作)
// 称为同步代码块
}
同步锁对象的要求
- 锁对象必须是引用类型:可以是 Object、自定义对象、类的 Class 对象等,不能是基本数据类型(int、long、boolean 等);
- 多个线程必须使用同一个锁对象:若多个线程的锁对象不同,则无法实现同步,仍会出现线程安全问题;
- 锁对象的选择:
- 非静态方法 / 代码块:可使用this(当前对象)或自定义的共享对象;
- 静态方法 / 代码块:必须使用类的 Class 对象(如TicketRunnable.class),因为静态方法属于类,不依赖对象。
代码实现(同步代码块版,解决卖票问题)
java
public class TicketRunnable implements Runnable {
private int ticket = 100;
// 定义共享的锁对象:多个线程使用同一个对象
private Object lock = new Object();
@Override
public void run() {
while (true) {
// 同步代码块:锁对象为lock
synchronized (lock) {
if (ticket > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":正在卖第" + ticket + "张票");
ticket--;
}
}
}
}
}
// 测试类不变,同6.2
原理说明
- 当线程 1 进入同步代码块时,会获取lock对象的锁,其他线程(线程 2、线程 3)尝试进入同步代码块时,因获取不到锁而进入BLOCKED(阻塞态);
- 线程 1 执行完同步代码块后,会自动释放锁,此时线程 2、线程 3 争夺锁,获取锁的线程进入执行,其余线程继续阻塞;
- 保证了对ticket的操作(if 判断 + ticket--)是原子操作,避免了多线程干扰。
5.4 同步方法
语法格式
将synchronized关键字修饰在方法上,该方法即为同步方法,整个方法体的代码均为同步代码,保证原子性。
java
// 非静态同步方法
public synchronized 返回值类型 方法名(参数列表) {
// 需保证线程安全的代码
}
// 静态同步方法
public static synchronized 返回值类型 方法名(参数列表) {
// 需保证线程安全的代码
}
同步方法的锁对象
同步方法的锁对象由 JVM 自动指定,无需手动定义,规则如下:
- 非静态同步方法:锁对象为this(当前类的实例对象);
- 静态同步方法:锁对象为当前类的 Class 对象(如TicketRunnable.class),因为静态方法属于类,不依赖实例对象。
代码实现(同步方法版,解决卖票问题)
方式 1:非静态同步方法
java
public class TicketRunnable implements Runnable {
private int ticket = 100;
@Override
public void run() {
while (true) {
// 调用同步方法
sellTicket();
}
}
// 非静态同步方法:锁对象为this
private synchronized void sellTicket() {
if (ticket > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":正在卖第" + ticket + "张票");
ticket--;
}
}
}
方式 2:静态同步方法(票源为静态变量时)
java
public class TicketRunnable implements Runnable {
// 静态共享资源:属于类,所有实例共享
private static int ticket = 100;
@Override
public void run() {
while (true) {
sellTicket();
}
}
// 静态同步方法:锁对象为TicketRunnable.class
private static synchronized void sellTicket() {
if (ticket > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":正在卖第" + ticket + "张票");
ticket--;
}
}
}
同步代码块与同步方法的区别
| 对比维度 | 同步代码块 | 同步方法 |
|---|---|---|
| 锁对象 | 手动指定,灵活多样 | JVM 自动指定,非静态为 this,静态为 Class 对象 |
| 同步范围 | 可指定任意代码段,范围更小,效率更高 | 整个方法体,范围更大,效率相对较低 |
| 灵活性 | 高,可针对不同代码段使用不同锁对象 | 低,整个方法使用同一个锁对象 |
| 适用场景 | 仅需对方法中部分代码做同步时 | 需对整个方法的代码做同步时 |
5.5 Lock 锁(显式锁)
核心介绍
- java.util.concurrent.locks.Lock是 Java 提供的显式锁接口,相比synchronized(隐式锁,自动加锁和释放),Lock 锁需要手动加锁和手动释放,灵活性更高;
- 核心实现类:java.util.concurrent.locks.ReentrantLock(可重入锁),支持公平锁和非公平锁,默认非公平锁;
- 核心优势:支持尝试获取锁、可中断获取锁、超时获取锁,解决了synchronized锁无法释放的问题。
Lock 接口的核心方法
| 方法名 | 说明 |
|---|---|
| void lock() | 获取锁:若锁已被持有,则阻塞,直到获取锁 |
| boolean tryLock() | 尝试获取锁:成功返回 true,失败返回 false,不阻塞 |
| boolean tryLock(long time, TimeUnit unit) | 超时尝试获取锁:在指定时间内尝试获取锁,成功返回 true,超时返回 false |
| void unlock() | 释放锁:必须手动调用,建议在finally块中执行,保证锁一定被释放 |
| Condition newCondition() | 创建条件对象,用于线程间通信(替代 wait ()/notify ()) |
实现步骤
- 在成员位置创建ReentrantLock对象:保证多个线程共享同一个锁对象;
- 在需要同步的代码前调用lock()方法,手动加锁;
- 在需要同步的代码后调用unlock()方法,手动释放锁;
- 建议将unlock()方法放在finally块中,避免因异常导致锁未释放,造成死锁。
代码实现(Lock 锁版,解决卖票问题)
java
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class TicketRunnable implements Runnable {
private int ticket = 100;
// 1. 成员位置创建ReentrantLock对象,默认非公平锁
private Lock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
// 2. 手动加锁
lock.lock();
try {
// 需同步的代码
if (ticket > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":正在卖第" + ticket + "张票");
ticket--;
}
} finally {
// 3. 手动释放锁,放在finally中,保证锁一定被释放
lock.unlock();
}
}
}
}
synchronized 与 Lock 锁的核心区别
| 对比维度 | synchronized(隐式锁) | Lock(显式锁,ReentrantLock) |
|---|---|---|
| 锁的性质 | 隐式锁,自动加锁、自动释放(代码块 / 方法执行完毕或异常时释放) | 显式锁,手动加锁(lock ())、手动释放(unlock ()),需在 finally 中释放 |
| 灵活性 | 低,仅支持阻塞式获取锁 | 高,支持尝试获取锁(tryLock ())、超时获取锁、可中断获取锁 |
| 锁的类型 | 非公平锁(默认),不支持公平锁 | 非公平锁(默认),可通过构造方法指定为公平锁(new ReentrantLock (true)) |
| 线程间通信 | 通过 Object 类的 wait ()/notify ()/notifyAll () | 通过 Condition 对象的 await ()/signal ()/signalAll (),支持多条件通信 |
| 可重入性 | 支持可重入(同一线程可多次获取同一把锁) | 支持可重入 |
| 异常处理 | 异常时自动释放锁,不会造成死锁 | 异常时若未手动释放锁,会造成死锁,需在 finally 中释放 |
| 性能 | 低并发下性能较好,高并发下性能较差 | 高并发下性能优于 synchronized |
六、死锁
6.1 死锁的定义
死锁是指两个或多个线程在执行过程中,因相互持有对方所需要的锁资源,而导致所有线程都处于等待状态,无法继续执行,且这种等待永远无法自行解除的现象。
死锁的核心特征
- 多个线程相互持有对方需要的锁;
- 所有线程都处于阻塞 / 等待状态,无法释放自己持有的锁;
- 无外部干预的情况下,线程永远无法继续执行。
6.2 死锁的产生条件(四大必要条件)
死锁的产生必须同时满足以下4 个必要条件,缺一不可,只要破坏其中任意一个条件,死锁即可避免:
- 互斥条件:锁资源为独占资源,同一时间仅能被一个线程持有,其他线程无法获取;
- 请求并持有条件:一个线程已经持有一个锁资源,又尝试请求获取另一个锁资源;
- 不可剥夺条件:线程持有的锁资源无法被其他线程强制剥夺,仅能由持有线程主动释放;
- 循环等待条件:多个线程形成一个循环等待链,每个线程都在等待链中下一个线程持有的锁资源。
6.3 死锁的代码演示
场景设计
创建两个锁对象LockA和LockB,创建一个线程任务类,让线程在执行时:
- 当i%2==0时,先获取LockA,再尝试获取LockB;
- 当i%2!=0时,先获取LockB,再尝试获取LockA;
- 两个线程同时执行该任务,会形成循环等待,产生死锁。
代码实现
java
// 定义锁对象A:单例,保证全局唯一
class LockA {
private LockA() {}
public static final LockA lockA = new LockA();
}
// 定义锁对象B:单例,保证全局唯一
class LockB {
private LockB() {}
public static final LockB lockB = new LockB();
}
// 死锁任务类:实现Runnable
class DeadLockRunnable implements Runnable {
private int i = 0;
@Override
public void run() {
while (true) {
if (i % 2 == 0) {
// 线程1:先获取lockA,再尝试获取lockB
synchronized (LockA.lockA) {
System.out.println(Thread.currentThread().getName() + ":获取了LockA,尝试获取LockB");
synchronized (LockB.lockB) {
System.out.println(Thread.currentThread().getName() + ":获取了LockB,执行完毕");
}
}
} else {
// 线程2:先获取lockB,再尝试获取lockA
synchronized (LockB.lockB) {
System.out.println(Thread.currentThread().getName() + ":获取了LockB,尝试获取LockA");
synchronized (LockA.lockA) {
System.out.println(Thread.currentThread().getName() + ":获取了LockA,执行完毕");
}
}
}
i++;
}
}
}
// 测试类:创建两个线程,模拟死锁
public class DeadLockTest {
public static void main(String[] args) {
DeadLockRunnable dlr = new DeadLockRunnable();
Thread t1 = new Thread(dlr, "线程1");
Thread t2 = new Thread(dlr, "线程2");
t1.start();
t2.start();
}
}
运行结果分析
运行代码后,控制台会输出以下内容,然后程序卡死,不再执行:
bash
线程1:获取了LockA,尝试获取LockB
线程2:获取了LockB,尝试获取LockA
- 线程 1 持有LockA,等待获取LockB;
- 线程 2 持有LockB,等待获取LockA;
- 两个线程相互持有对方需要的锁,形成循环等待,满足死锁的四大必要条件,产生死锁。
6.4 死锁的解决策略
解决死锁的核心思路是破坏死锁的四大必要条件中的任意一个,实际开发中最常用、最简单的方式是破坏循环等待条件,其次是破坏请求并持有条件。
策略 1:破坏循环等待条件(最常用)
统一锁的获取顺序:让所有线程在获取多个锁时,按照固定的顺序获取,避免形成循环等待链。
java
// 锁对象A、LockB不变
class DeadLockRunnable implements Runnable {
private int i = 0;
@Override
public void run() {
while (true) {
if (i % 2 == 0) {
// 统一顺序:先获取LockA,再获取LockB
synchronized (LockA.lockA) {
System.out.println(Thread.currentThread().getName() + ":获取了LockA,尝试获取LockB");
synchronized (LockB.lockB) {
System.out.println(Thread.currentThread().getName() + ":获取了LockB,执行完毕");
}
}
} else {
// 统一顺序:先获取LockA,再获取LockB(不再先获取LockB)
synchronized (LockA.lockA) {
System.out.println(Thread.currentThread().getName() + ":获取了LockA,尝试获取LockB");
synchronized (LockB.lockB) {
System.out.println(Thread.currentThread().getName() + ":获取了LockB,执行完毕");
}
}
}
i++;
}
}
}
原理:所有线程都先获取LockA,再获取LockB,不会形成循环等待,破坏了循环等待条件,避免死锁。
策略 2:破坏请求并持有条件
一次性获取所有需要的锁:让线程在执行前,一次性获取所有需要的锁资源,若无法获取全部锁,则放弃已获取的锁,重新尝试,避免持有部分锁并请求另一部分锁。
- 实现方式:可通过Lock锁的tryLock()方法实现,尝试获取所有锁,若有一个获取失败,则释放已获取的锁。
策略 3:破坏不可剥夺条件
使用可中断的锁获取方式:让线程在获取锁的过程中,可被其他线程中断,若线程等待锁的时间过长,则中断线程,释放其持有的锁资源。
- 实现方式:Lock锁的lockInterruptibly()方法,支持可中断的锁获取。
策略 4:破坏互斥条件
使用共享锁替代独占锁:将独占的锁资源改为共享资源,允许多个线程同时持有锁,避免互斥。
- 适用场景:仅适用于对共享资源的读操作,若有写操作,则无法使用,因此实际开发中使用较少。
6.5 死锁的排查与避免
死锁的排查方式
- jps 命令:查看当前运行的 Java 进程的 PID;
- jstack PID 命令:查看该进程的线程堆栈信息,若存在死锁,会在堆栈信息中明确标注Deadlock,并显示死锁的线程和锁资源;
- IDE 工具:使用 IDEA/Eclipse 的 JVM 监控工具,查看线程状态,识别死锁线程。
死锁的避免原则
- 尽量减少锁的嵌套:避免在同步代码块 / 同步方法中嵌套另一个同步代码块 / 同步方法;
- 统一锁的获取顺序:多个线程获取多个锁时,按照固定的顺序获取;
- 控制锁的持有时间:尽量缩短线程持有锁的时间,执行完同步代码后立即释放锁;
- 使用显式锁(Lock) 替代 synchronized:Lock 锁支持尝试获取锁、超时获取锁,可有效避免死锁;
- 避免无限期等待锁:使用tryLock()设置超时时间,若超时则放弃获取锁,释放已持有的锁。
七、线程间通信
7.1 线程间通信的定义
线程间通信是指多个线程在执行过程中,通过一定的机制相互协调、相互配合,共同完成一个任务,实现对共享资源的有序访问,避免资源争夺,提高程序的执行效率。
线程间通信的核心场景
当多个线程处理同一个共享资源,但任务不同时,需要通过通信让线程有序执行,例如:
- 生产者线程生产数据,消费者线程消费数据(生产者 - 消费者模型);
- 线程 A 写入数据,线程 B 读取数据,保证先写后读。
7.2 线程间通信的核心机制:等待唤醒机制
等待唤醒机制是 Java 中线程间通信的核心机制,通过 Object 类的一组方法实现,让线程在满足特定条件时进入等待状态,在条件满足时被其他线程唤醒,实现有序执行。
核心方法(Object 类的方法,必须在同步代码块 / 同步方法中使用)
| 方法名 | 说明 |
|---|---|
| public final void wait() | 让当前线程进入WAITING(无计时等待态),释放持有的锁资源,需其他线程调用notify()/notifyAll()唤醒 |
| public final void wait(long timeout) | 让当前线程进入TIMED_WAITING(计时等待态),释放持有的锁资源,超时自动唤醒,也可被显式唤醒 |
| public final void notify() | 唤醒在此对象监视器上等待的单个线程,被唤醒的线程需重新争夺锁资源 |
| public final void notifyAll() | 唤醒在此对象监视器上等待的所有线程,被唤醒的线程需重新争夺锁资源 |
方法使用的核心注意事项
- 必须在同步代码块 / 同步方法中使用:这些方法的调用需要依赖对象的监视器(锁),若在非同步环境中使用,会抛出IllegalMonitorStateException;
- 必须由同一个锁对象调用:等待的线程和唤醒的线程必须使用同一个锁对象调用 wait ()/notify ()/notifyAll (),否则无法唤醒;
- wait () 方法会释放锁资源:与 sleep () 不同,wait () 在等待时会释放持有的锁,让其他线程可以获取锁;sleep () 在休眠时不会释放锁;
- 被唤醒的线程需重新争夺锁:notify ()/notifyAll () 仅唤醒线程,不会直接让线程执行,被唤醒的线程会进入BLOCKED 态,等待争夺锁资源,获取锁后才能继续执行。
7.3 生产者 - 消费者案例
实现一个简单的生产者 - 消费者模型:
- 包子铺(生产者线程):生产包子,包子有state状态(true:有包子,false:无包子);
- 吃货(消费者线程):消费包子,若有包子则吃,若无包子则等待;
- 核心规则:
- 无包子时,消费者线程等待,生产者线程生产包子,生产完成后唤醒消费者;
- 有包子时,生产者线程等待,消费者线程消费包子,消费完成后唤醒生产者。
java
// 包子类:共享资源,包含状态和名称
class BaoZi {
// 包子状态:true-有包子,false-无包子
public boolean state = false;
// 包子名称
public String name;
}
// 包子铺(生产者线程):实现Runnable
class BaoZiPu implements Runnable {
private BaoZi bz; // 共享的包子对象
public BaoZiPu(BaoZi bz) {
this.bz = bz;
}
@Override
public void run() {
int count = 0; // 包子计数,模拟不同包子
while (true) {
// 同步代码块:锁对象为包子对象(共享资源)
synchronized (bz) {
// 有包子时,生产者等待
if (bz.state) {
try {
bz.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 无包子时,生产包子
if (count % 2 == 0) {
bz.name = "猪肉大葱包";
} else {
bz.name = "韭菜鸡蛋包";
}
count++;
System.out.println("包子铺:生产了" + bz.name);
// 修改包子状态为有
bz.state = true;
// 唤醒消费者线程
bz.notify();
}
}
}
}
// 吃货(消费者线程):实现Runnable
class ChiHuo implements Runnable {
private BaoZi bz; // 共享的包子对象
public ChiHuo(BaoZi bz) {
this.bz = bz;
}
@Override
public void run() {
while (true) {
// 同步代码块:锁对象为包子对象(与生产者一致)
synchronized (bz) {
// 无包子时,消费者等待
if (!bz.state) {
try {
bz.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 有包子时,消费包子
System.out.println("吃货:吃了" + bz.name);
// 修改包子状态为无
bz.state = false;
// 唤醒生产者线程
bz.notify();
}
}
}
}
// 测试类:创建生产者和消费者线程
public class ProducerConsumerTest {
public static void main(String[] args) {
// 创建共享的包子对象
BaoZi bz = new BaoZi();
// 创建生产者和消费者线程
Thread pu = new Thread(new BaoZiPu(bz), "包子铺");
Thread huo = new Thread(new ChiHuo(bz), "吃货");
// 启动线程
pu.start();
huo.start();
}
}
运行结果(有序执行)
bash
包子铺:生产了猪肉大葱包
吃货:吃了猪肉大葱包
包子铺:生产了韭菜鸡蛋包
吃货:吃了韭菜鸡蛋包
包子铺:生产了猪肉大葱包
吃货:吃了猪肉大葱包
...
原理说明
- 初始时包子状态为false(无包子),消费者线程进入同步代码块后,调用bz.wait()进入等待态,释放锁;
- 生产者线程获取锁,生产包子,修改状态为true,调用bz.notify()唤醒消费者线程,然后继续循环,因状态为true,调用bz.wait()进入等待态,释放锁;
- 消费者线程被唤醒,争夺锁成功后,消费包子,修改状态为false,调用bz.notify()唤醒生产者线程,然后继续循环,因状态为false,调用bz.wait()进入等待态,释放锁;
- 如此循环,实现生产者和消费者的有序执行,完成线程间通信。
wait () 与 sleep () 的核心区别
wait () 和 sleep () 均能让线程进入等待状态,但二者存在本质区别,是面试高频考点,核心区别如下:
| 对比维度 | wait() | sleep() |
|---|---|---|
| 所属类 | Object 类的方法,所有对象均可调用 | Thread 类的静态方法,仅能通过 Thread 调用 |
| 锁资源 | 等待时释放锁资源,让其他线程可获取锁 | 休眠时不释放锁资源,其他线程无法获取锁 |
| 使用环境 | 必须在同步代码块 / 同步方法中使用 | 可在任意环境中使用,无同步要求 |
| 唤醒方式 | 需其他线程调用notify()/notifyAll()唤醒,或中断; | 无参 wait () 需显式唤醒 到达指定时间后自动唤醒,或被中断 |
| 异常处理 | 必须捕获InterruptedException,或抛出 | 必须捕获InterruptedException,或抛出 |
| 状态转换 | 运行态 → WAITING/TIMED_WAITING,释放锁 | 运行态 → TIMED_WAITING,不释放锁 |
| 用途 | 用于线程间通信,协调线程执行顺序 | 用于线程休眠,暂停执行指定时间 |
