进程与线程
一、进程
1. 定义
- 程序由指令和数据组成,但需要运行就必须加载指令至 CPU、数据至内存,还要处理磁盘、网络等设备。进程就是用于加载指令、管理内存、管理 I/O 的实体。
2. 启动
- 当一个程序运行时,系统会将程序代码从磁盘加载到内存,并开启一个进程。
3. 实例化
- 进程是程序的一个运行实例。
- 大多数程序可运行多个实例进程(如:记事本、浏览器),有些程序只能运行一个实例进程(如:网易云音乐、360 安全卫士)。
二、线程
1. 定义
- 一个进程内部可以包含一个或多个线程。
2. 执行流
- 线程是一个指令流,系统将线程中的指令按顺序交给 CPU 执行。
3. 调度单位
- Java 中,线程是最小调度单位;进程是资源分配的最小单位。
- 在 Windows 中,进程作为线程的容器,线程才是实际的执行单元。
三、进程与线程关系
- 包含关系:一个进程可以包含多个线程。线程共享进程的内存、文件描述符等资源。
- 切换开销:线程间切换比进程间切换开销小,效率更高。
![[Pasted image 20250422112416.png]]
四、进程特点
- 独立性:拥有独立的地址空间,互不干扰。
- 并发性:多个进程可并发执行。
- 动态性:有生命周期(创建、运行、阻塞、终止)。
- 异步性:进程独立运行,互不等待。
五、线程特点
- 轻量级:线程创建、销毁开销小。
- 共享资源:同一进程内线程共享内存等资源。
- 并发性:可并发执行,提高程序效率。
- 通信方便:线程之间通过共享内存通信,效率高。
关于协程:
-
协程被视为比线程更轻量级的并发单元,可以在单线程中实现并发执行,由我们开发者显式调度。
-
协程是在用户态进行调度的,避免了线程切换时的内核态开销。
-
可惜 Java 自身是不支持协程,Kotlin 可以
六、Java 中的线程机制
- 线程为最小调度单位,系统按优先级和状态调度线程。
- 进程负责资源分配(如内存、文件描述符等)。
- 线程池机制:用于复用线程,减少频繁创建销毁线程的开销。
示例代码
java
public class MyThread extends Thread {
@Override
public void run() {
System.out.println("Thread is running");
}
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
}
}
线程间如何通信?
线程间通信主要有两种方式:消息传递 和 共享内存 。Java 采用的是 共享内存的并发模型。
在这个模型中,多个线程通过读写共享变量 来进行通信。但由于每个线程都有自己的本地内存(线程工作内存),因此并不是所有对共享变量的修改都能立即被其他线程看到。
这个并发模型由 Java 内存模型(Java Memory Model,JMM) 定义,它决定了:
-
共享变量存储在 主内存 中;
-
每个线程拥有自己的 本地内存(工作内存),其中保存了主内存中共享变量的一份副本;
-
线程对共享变量的读写操作必须在本地内存中进行;
-
一个线程对共享变量的修改,何时对其他线程可见,由 JMM 的规则决定。
![[Pasted image 20250422134222.png]]
七、程序实例说明
1. 什么是"程序的实例"
- 程序 :静态的
.exe
文件,存储在磁盘上。 - 实例:程序被加载并运行后成为进程,即"程序的实例"。
2. 为什么可以运行多个实例
- 每个进程独立运行,互不影响。
- 满足用户多任务需求。
3. 举例说明
- 记事本:打开两个记事本窗口,就是两个独立进程。
- 浏览器:多个窗口或标签页为独立进程或线程。
- 画图工具:多个窗口分别为不同进程。
4. 不允许多实例的程序
- 如网易云音乐、360 安全卫士等仅允许运行一个实例,防止资源冲突或同步问题。
并行与并发
一、并发(Concurrency)
- 定义:同一时间处理多任务的能力。关注任务调度与切换,用于解决 IO 密集型任务的瓶颈。
- 实现:单核 CPU 通过时间片轮转交替运行线程。
二、并行(Parallelism)
- 定义:真正地同时执行多个任务。依赖多核 CPU。
- 实现:多个核心独立执行不同线程。
三、单核 CPU 下的并发机制
CPU
core
时间片1 时间片2 时间片3 时间片4
线程1 线程2 线程3 线程4
- 微观串行:同一时间只能执行一个线程。
- 宏观并行:人类感觉多个线程同时运行。
四、多核 CPU 下的并行执行
CPU
core 1 core 2
线程1 线程2
指令1 指令1
指令2 指令2
...
- 多线程真正同时运行,提高执行效率。
五、Rob Pike 对比喻
- 并发:同一时间应对多件事。
- 并行:同一时间做多件事。
同步与异步
一、概念
- 同步:等待结果后再继续执行。
- 异步:发起任务后立即返回,继续执行后续逻辑。
二、使用多线程实现异步调用
同步调用示例
java
public class Sync {
public static void main(String[] args) {
FileReader.read(Constants.MP4_FULL_PATH); // 阻塞执行
System.out.println("do other things...");
}
}
异步调用示例
java
public class Async {
public static void main(String[] args) {
new Thread(() -> FileReader.read(Constants.MP4_FULL_PATH)).start(); // 异步执行
System.out.println("do other things...");
}
}
三、应用场景
- 视频转码、网络请求、文件处理等耗时任务建议使用异步线程执行,避免主线程阻塞。
多线程与多核效率分析
一、单核 CPU
- 多线程无法真正提升执行速度,只是切换任务避免线程长时间阻塞。
二、多核 CPU
- 可并行运行多个线程,提高执行效率。
- 任务设计合理时,通过拆分并行处理可提高性能。
三、IO 与 CPU 使用效率
- 阻塞 IO 问题:线程被挂起等待 IO,浪费 CPU。
- 优化方法:使用非阻塞 IO 和异步 IO,释放线程继续处理其他任务。
阿姆达尔定律(Amdahl's Law)
一、核心思想
- 程序加速受限于无法并行化的串行部分,需要任务拆分。
二、加速比公式:
S = 1 ( 1 − P ) + P N S=\frac{1}{(1 - P) + \frac{P}{N}} S=(1−P)+NP1
- S:整体加速比
- P:可并行部分比例
- N:处理器核心数
三、示例计算:
-
50% 可并行 + 4 核 CPU
S= 1 0.5 + 0.5 4 \frac{1}{0.5 + \frac{0.5}{4}} 0.5+40.51 = 1.6
-
90% 可并行 + 8 核 CPU
S= 1 0.1 + 0.9 8 \frac{1}{0.1 + \frac{0.9}{8}} 0.1+80.91≈ 4.7
四、实际意义
- 串行部分是性能瓶颈。
- 任务拆分需考虑依赖关系、通信成本。
- 提高效率的关键是减少串行部分占比。
创建和运行线程的方式
一、方式一:继承 Thread
类
1. 步骤
- 创建
Thread
子类或匿名类对象。 - 重写
run()
方法,定义任务。 - 调用
start()
方法启动线程。
2. 示例代码
java
Thread t = new Thread() {
@Override
public void run() {
// 执行任务
}
};
t.start();
3. 指定线程名称(推荐)
java
Thread t1 = new Thread("t1") {
@Override
public void run() {
log.debug("hello");
}
};
t1.start();
4. 核心方法解析
Thread
:Java 中代表线程的类。run()
:线程执行逻辑所在的方法。start()
:启动线程并由 JVM 调用run()
方法。- 命名线程:构造方法传入线程名,方便调试管理。
5. 注意事项
run()
与start()
区别 :- 直接调用
run()
不会开启新线程,只在当前线程执行。 - 调用
start()
才真正创建并启动一个新线程。
- 直接调用
- 线程安全问题 :
- 多线程访问共享资源需同步控制(如
synchronized
或Lock
)。
- 多线程访问共享资源需同步控制(如
二、方式二:实现 Runnable
接口
1. 步骤
- 创建
Runnable
对象,重写run()
方法。 - 使用该对象构造
Thread
。 - 调用
start()
方法启动线程。
2. 示例代码
java
Runnable runnable = new Runnable() {
@Override
public void run() {
// 执行任务
}
};
Thread t = new Thread(runnable);
t.start();
3. 指定线程名称(推荐)
java
Runnable task2 = new Runnable() {
@Override
public void run() {
log.debug("hello");
}
};
Thread t2 = new Thread(task2, "task2-thread");
t2.start();
4. 可选:Lambda 表达式简化写法
java
Runnable task3 = () -> {
log.debug("lambda run");
};
new Thread(task3, "lambda-thread").start();
5. 优点
- 线程与任务分离,更灵活。
- 更符合面向对象设计。
- 可与线程池等 API 更好地配合使用。
- 避免多重继承限制,更易复用。
三、Thread 与 Runnable 的关系
1. 本质机制
java
public class Thread implements Runnable {
private Runnable target;
public Thread(Runnable target) {
this.target = target;
}
@Override
public void run() {
if (target != null) {
target.run();
}
}
}
- 若传入
Runnable
,则执行target.run()
; - 若未传入,则执行
Thread
类自身的run()
。
2. 示例代码对比
继承 Thread 类:
java
class MyThread extends Thread {
@Override
public void run() {
System.out.println("Hello from thread!");
}
}
实现 Runnable 接口:
java
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("Hello from thread!");
}
}
四、方式三:使用 FutureTask
配合 Thread
(有返回结果)
1. 核心概念
FutureTask
同时实现了Runnable
和Future
接口。- 可作为任务对象传入线程中执行,并支持获取返回结果。
2. 示例代码(Lambda 写法)
java
FutureTask<Integer> task3 = new FutureTask<>(() -> {
log.debug("hello");
return 100;
});
Thread t3 = new Thread(task3, "t3");
t3.start();
Integer result = task3.get(); // 阻塞获取结果
log.debug("结果是:{}", result);
3. 示例代码(匿名内部类写法)
java
FutureTask<Integer> task = new FutureTask<>(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
log.debug("running...");
Thread.sleep(1000);
return 100;
}
});
Thread t = new Thread(task);
t.start();
Integer result = task.get(); // 同样可以获取返回值
4. 示例代码(Future接口)
java
import java.util.concurrent.*;
public class CallableExample {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService executor = Executors.newSingleThreadExecutor();
Callable<Integer> task = () -> {
return 123;
};
// 这里要用线程池才行,因为Callable没有实现Runnable
Future<Integer> future = executor.submit(task);
Integer result = future.get(); // 获取任务执行结果
System.out.println("Result: " + result);
executor.shutdown();
}
}
五、相关接口关系(逻辑)
RunnableFuture<V>
同时继承Runnable
和Future<V>
。FutureTask
实现了RunnableFuture
,可用于线程执行 + 返回结果。
六、关键接口解析
接口/类 | 功能 |
---|---|
Runnable |
定义任务,无返回结果 |
Callable |
定义任务,有返回结果 |
Future |
获取异步任务执行结果 |
FutureTask |
任务 + 结果二合一(执行 + 获取) |
七、总结
- 推荐方式 :
- 简单任务:
Runnable
+Thread
- 需要返回值:
Callable
+FutureTask
+Thread
- 简单任务:
- 优点对比 :
Runnable
:灵活,适合线程池等结构。FutureTask
:支持异步执行 + 返回结果。
- 线程安全问题依然存在:需要适当同步控制。
线程并发与资源管理
一、线程并发与资源占用
1. 并发执行顺序
- 不可控性:线程的执行顺序由操作系统调度器决定,开发者无法直接控制。
- 协调执行 :可通过
synchronized
、Lock
、Semaphore
等同步机制协调线程执行顺序。
2. 资源竞争
-
核心数量限制:
- 当线程数量 > CPU 核心数量时,线程间会争抢核心资源。
- 操作系统使用时间片轮转等策略调度线程执行。
-
无限循环的影响:
- 如果线程中存在无限循环且不释放 CPU(如无
sleep()
),该线程会持续占用核心资源。 - 其他线程/进程可能获取不到 CPU 时间片,系统卡顿或响应慢。
- 如果线程中存在无限循环且不释放 CPU(如无
3. 让出线程
- 使用
Thread.sleep()
可以让出 CPU 时间片,使其他线程有机会执行。
示例代码:
java
public class InfiniteLoopThread extends Thread {
@Override
public void run() {
while (true) {
System.out.println("Running...");
try {
Thread.sleep(1000); // 让出 CPU
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
二、查看和管理进程与线程
1. Windows 系统
- 任务管理器:查看线程数、进程资源占用,可终止进程。
- 命令行工具 :
-
tasklist
:查看所有进程。cmdtasklist
-
taskkill
:结束指定进程。cmdtaskkill /F /PID <PID> taskkill /F /PID 1234
-
2. Linux 系统
- 常用命令 :
-
查看所有进程:
bashps -ef
-
查看某个进程的线程:
bashps -fT -p <PID>
-
杀死进程:
bashkill <PID>
-
实时查看资源占用:
bashtop
-
显示线程:按
H
键切换线程视图。 -
查看某个进程的线程:
bashtop -H -p <PID>
-
-
3. Java 应用程序相关工具
-
jps
:查看所有 Java 进程。bashjps
-
jstack
:查看指定 Java 进程的线程状态。bashjstack <PID>
示例流程:
bash
# 获取 Java 进程 PID
jps
# 查看线程状态
jstack <PID>
三、远程线程监控:使用 jconsole
1. 启动 Java 应用程序(带远程监控参数)
bash
java -Djava.rmi.server.hostname=192.168.1.100 \
-Dcom.sun.management.jmxremote \
-Dcom.sun.management.jmxremote.port=9090 \
-Dcom.sun.management.jmxremote.ssl=false \
-Dcom.sun.management.jmxremote.authenticate=true \
-jar your-java-app.jar
各参数含义:
参数 | 说明 |
---|---|
-Djava.rmi.server.hostname |
指定主机 IP |
-Dcom.sun.management.jmxremote |
启用 JMX 远程管理 |
-Dcom.sun.management.jmxremote.port |
设置远程端口 |
-Dcom.sun.management.jmxremote.ssl |
是否使用 SSL |
-Dcom.sun.management.jmxremote.authenticate |
是否启用身份验证 |
2. 修改 /etc/hosts
(如有需要)
plaintext
127.0.0.1 your-hostname
3. 配置远程认证(可选)
-
编辑密码文件:
- 配置
jmxremote.password
和jmxremote.access
文件内容。
- 配置
-
设置文件权限:
bashchmod 600 jmxremote.password jmxremote.access
-
连接时输入认证信息:
- 示例用户:
controlRole
- 示例密码:
R&D
- 示例用户:
四、使用 jconsole 进行远程监控
步骤:
-
启动 Java 应用(带远程参数)
-
打开 jconsole
- Windows:运行
jconsole.exe
,终端输入jconsole
- Linux/Mac:终端执行
jconsole
- Windows:运行
-
连接远程进程
-
输入远程地址:
service:jmx:rmi:///jndi/rmi://<IP>:<端口>/jmxrmi
-
-
输入认证信息(如启用身份验证)
-
查看监控数据
- 线程情况
- 内存使用
- GC 状态
- 类加载信息
五、监控示例
示例参数:
- IP:
192.168.1.100
- 端口:
9090
启动命令:
bash
java -Djava.rmi.server.hostname=192.168.1.100 \
-Dcom.sun.management.jmxremote \
-Dcom.sun.management.jmxremote.port=9090 \
-Dcom.sun.management.jmxremote.ssl=false \
-Dcom.sun.management.jmxremote.authenticate=true \
-jar your-java-app.jar
jconsole 连接地址:
service:jmx:rmi:///jndi/rmi://192.168.1.100:9090/jmxrmi
用户名密码示例:
- 用户名:
controlRole
- 密码:
R&D
六、总结
- 线程并发不可控:操作系统调度线程执行。
- 资源竞争严重时需合理调度:防止某线程独占资源。
- 线程释放资源 :使用
Thread.sleep()
等方式释放 CPU。 - 进程线程查看工具丰富:Windows/Linux/Java 均有专用工具。
- 远程监控推荐使用 jconsole + JMX 参数,方便实时分析线程运行状态和资源占用。
JVM 栈与线程上下文切换
一、栈与栈帧
1. JVM 中的栈
-
作用:
- 每个线程启动后,JVM 会为其分配一块独立的栈内存。
- 用于存储方法调用过程中产生的局部变量、操作数、中间结果、返回地址等。
-
特点:
- 先进后出(LIFO):方法先调用后返回,栈帧先入后出。
- 线程独立性:每个线程拥有自己的栈,互不干扰。
2. 栈帧(Stack Frame)
- 定义 :
- 每次方法调用都会创建一个栈帧。
- 栈帧中包含方法执行所需的所有信息,如:
- 局部变量表
- 操作数栈
- 动态链接(方法引用)
- 返回地址
3. 示例代码
java
public class TestFrames {
public static void main(String[] args) {
method1(10);
}
private static void method1(int x) {
int y = x + 1;
Object m = method2();
System.out.println(m);
}
private static Object method2() {
Object n = new Object();
return n;
}
}
4. 栈帧生命周期
阶段 | 描述 |
---|---|
创建 | 每调用一个方法,JVM 在当前线程的栈中创建一个新的栈帧 |
销毁 | 方法执行完毕后,对应的栈帧被销毁 |
5. 示例分析
main()
方法调用method1(10)
→ 创建第一个栈帧。method1()
调用method2()
→ 创建第二个栈帧。method2()
执行完毕 → 栈帧销毁 → 回到method1()
。
示例图解
![[Pasted image 20250318145816.png]]
6. IDEA栈帧信息标记示例
method2:21, thread (codeforces)
method1:17, thread (codeforces)
main:12, thread (codeforces)
- 表示在代码第 X 行,执行线程为
codeforces
。
二、线程上下文切换(Thread Context Switch)
1. 定义
- 当 CPU 从一个线程切换执行另一个线程时,就发生了线程上下文切换。
2. 常见触发场景
- 当前线程的时间片用完
- 有更高优先级线程就绪
- 垃圾回收
- 线程执行了以下操作:
sleep()
/yield()
/wait()
/join()
- 获取锁:
synchronized
/Lock
park()
等阻塞方法
3. 上下文切换过程
步骤 | 描述 |
---|---|
保存状态 | 操作系统保存当前线程执行状态(包括栈帧、PC 等) |
恢复状态 | 切换到另一个线程时,恢复该线程的保存状态 |
4. 性能影响
- 代价高:每次切换都涉及 CPU 缓存失效、内存页切换等系统操作。
- 影响程序效率:频繁切换将降低整体性能。
三、总结
1. 栈与栈帧
项目 | 描述 |
---|---|
JVM 栈 | 每个线程独立持有,用于存储方法调用信息 |
栈帧 | 每次方法调用创建的执行单元,包含局部变量、操作数等 |
2. 上下文切换
项目 | 描述 |
---|---|
原因 | 时间片结束、阻塞操作、锁竞争等 |
过程 | 保存当前线程状态 → 恢复新线程状态 |
影响 | 过多切换影响系统性能,应合理设计线程任务 |
Java 线程方法详解
一、线程基础方法
1. start() 方法
方法说明
-
作用:启动一个新线程,使该线程开始执行
-
注意事项:
-
每个线程对象的start()方法只能调用一次
-
重复调用会抛出IllegalThreadStateException异常
-
start()方法会创建新的执行线程,而run()方法只会在当前线程中执行
示例代码
java
public class ThreadStartDemo {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
System.out.println("新线程执行中...");
System.out.println("当前线程名称:" + Thread.currentThread().getName());
});
System.out.println("调用start()方法前的线程状态:" + thread.getState());
thread.start();
System.out.println("调用start()方法后的线程状态:" + thread.getState());
// 错误示范:重复调用start()
try {
thread.start(); // 将抛出IllegalThreadStateException
} catch (IllegalThreadStateException e) {
System.out.println("不能重复调用start()方法");
}
}
}
2. run() 方法
方法说明
-
作用:定义线程要执行的任务代码
-
特点:
-
直接调用run()方法不会创建新线程
-
常通过匿名内部类或Lambda表达式重写
示例代码
java
public class ThreadRunDemo {
public static void main(String[] args) {
// 方式一:实现Runnable接口
Runnable task = () -> {
System.out.println("使用Runnable实现线程任务");
};
new Thread(task).start();
// 方式二:继承Thread类
class MyThread extends Thread {
@Override
public void run() {
System.out.println("继承Thread类实现线程任务");
}
}
new MyThread().start();
// 直接调用run()方法的效果
Thread thread = new Thread(() -> {
System.out.println("当前线程:" + Thread.currentThread().getName());
});
thread.run(); // 在主线程中执行,不会创建新线程
}
}
3. join() 方法
方法说明
-
作用:等待线程执行完成
-
重载形式:
-
join():无限等待
-
join(long millis):最多等待指定毫秒数
-
join(long millis, int nanos):可以精确到纳秒
- 注意事项:
-
可能抛出InterruptedException异常
-
会暂停当前线程的执行
-
主线程等待子线程执行完成后才能继续执行
-
适用于需要等待其他线程执行结果的场景
-
如果线程A调用线程B的join方法,那么线程A会进入WAITING状态,直到线程B执行完成
示例代码
java
public class ThreadJoinDemo {
public static void main(String[] args) throws InterruptedException {
Thread worker = new Thread(() -> {
try {
System.out.println("工作线程开始执行...");
Thread.sleep(2000); // 模拟耗时操作
System.out.println("工作线程执行完成");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
worker.start();
System.out.println("等待工作线程完成...");
worker.join(3000); // 最多等待3秒
System.out.println("主线程继续执行");
// 使用join()实现线程同步
List<Thread> threads = new ArrayList<>();
for (int i = 0; i < 3; i++) {
Thread t = new Thread(() -> {
try {
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + " 完成");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
threads.add(t);
t.start();
}
// 等待所有线程完成
for (Thread t : threads) {
t.join();
}
System.out.println("所有线程执行完成");
}
}
4. sleep() 方法
方法说明
-
作用:使当前线程暂停执行指定时间
-
特点:
-
静态方法(Thread.sleep())
-
不会释放锁资源
-
可以被中断(抛出InterruptedException)
-
休眠时会让出 CPU 的时间片给其他线程
-
调用sleep会让当前线程从Running进入Timed Waiting状态
-
其他线程可以使用interrupt方法打断正在睡眠的线程,这时sleep方法会抛出InterruptedException
CPU使用优化
-
在没有利用CPU来计算时,可以使用yield或sleep来让出CPU的使用权
-
避免使用while(true)空转浪费CPU资源
-
示例:防止CPU占用100%的实现
java
while(true) {
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
示例代码
java
public class ThreadSleepDemo {
public static void main(String[] args) {
// 基本使用
Thread thread = new Thread(() -> {
try {
System.out.println("线程开始休眠...");
Thread.sleep(2000); // 休眠2秒
System.out.println("线程休眠结束");
} catch (InterruptedException e) {
System.out.println("线程被中断");
}
});
thread.start();
// sleep与锁
Object lock = new Object();
Thread sleepThread = new Thread(() -> {
synchronized (lock) {
try {
System.out.println("线程获得锁并开始休眠");
Thread.sleep(3000);
System.out.println("线程休眠结束,释放锁");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread waitThread = new Thread(() -> {
synchronized (lock) {
System.out.println("等待线程获得锁");
}
});
sleepThread.start();
Thread.sleep(1000); // 确保sleepThread先执行
waitThread.start();
}
}
5. interrupt() 相关方法
interrupt() 方法说明
-
作用:中断线程
-
特点:
-
不会强制停止线程,而是设置一个中断标志,线程可以选择在合适的时候停止
-
设置线程的中断标志
-
如果线程处于sleep/wait/join状态,会抛出InterruptedException并清空中断状态
-
如果打断正常运行的线程,不会清空打断状态
isInterrupted() 和 interrupted() 方法说明
- isInterrupted()方法:
-
实例方法,判断线程是否被中断
-
不会清除中断标记
-
返回线程的中断状态
- interrupted()方法:
-
静态方法,判断当前线程是否被中断
-
会清除中断标记(将中断状态设为false)
-
返回当前线程的中断状态
stop() 方法被替代原因
❶ 不安全(Unsafe)
stop() 会在线程任意代码位置中断执行,可能导致资源未释放、锁未释放,引发死锁或数据不一致。
❷ 不可控(Unpredictable)
它会在任意地方抛出 ThreadDeath 异常,而不是你能控制的地方,中断时机和状态你完全无法预知。
❸ 破坏线程同步
如果一个线程正在持有某个共享对象的锁,stop() 会直接终止线程,锁永远不释放,导致其他线程永久阻塞。
示例代码
java
public class ThreadInterruptDemo {
public static void main(String[] args) throws InterruptedException {
// 中断睡眠中的线程
Thread sleepThread = new Thread(() -> {
try {
System.out.println("线程开始休眠");
Thread.sleep(10000);
} catch (InterruptedException e) {
System.out.println("线程被中断");
// 重新设置中断状态
Thread.currentThread().interrupt();
}
});
sleepThread.start();
Thread.sleep(2000); // 确保线程进入休眠状态
sleepThread.interrupt();
// 中断运行中的线程
Thread busyThread = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
// 执行任务
System.out.println("线程正在运行...");
}
System.out.println("线程检测到中断信号,退出执行");
});
busyThread.start();
Thread.sleep(2000);
busyThread.interrupt();
}
}
6. wait()/notify()/notifyAll() 方法
方法说明
-
wait():
-
使当前线程等待,直到被其他线程通过
notify()
或notifyAll()
或interrupt()
唤醒,被打断抛出InterruptedException
异常 -
当前线程必须持有目标对象的锁,否则抛出
IllegalMonitorStateException
-
执行后线程会释放锁 ,进入对象的 WaitSet 中,状态变为
WAITING
-
-
notify():
-
唤醒一个在当前对象上等待的线程(WaitSet 中的任意一个)
-
被唤醒线程不会立刻获得锁,而是进入 EntryList 中等待锁的释放
-
-
notifyAll():
-
唤醒在当前对象上等待的所有线程
-
所有线程进入 EntryList 队列,竞争锁执行
-
方法特性
-
属于
Object
类的方法,而非Thread
类 -
必须在
synchronized
代码块或方法中使用 -
wait()
会让线程进入WAITING
状态,并释放锁 -
notify()
不会释放锁,仅是发出唤醒信号
示例代码
java
public class WaitNotifyDemo {
private static final Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
// 等待线程
Thread waitingThread = new Thread(() -> {
synchronized (lock) {
System.out.println("线程1:进入等待状态");
try {
lock.wait(); // 释放锁,进入 WAITING 状态
System.out.println("线程1:被唤醒,继续执行");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
// 唤醒线程
Thread notifierThread = new Thread(() -> {
try {
Thread.sleep(1000); // 确保等待线程先进入等待
synchronized (lock) {
System.out.println("线程2:发出唤醒信号");
lock.notify(); // 唤醒线程1
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
waitingThread.start();
notifierThread.start();
waitingThread.join();
notifierThread.join();
}
}
二、线程状态与控制
1. getState() 方法
方法说明
-
作用:获取线程当前状态
-
返回值:Thread.State枚举类型
-
NEW:线程创建但未启动
-
RUNNABLE:线程正在运行或准备运行
-
BLOCKED:线程阻塞等待监视器锁
-
WAITING:线程无限期等待
-
TIMED_WAITING:线程等待指定时间
-
TERMINATED:线程已结束
示例代码
java
public class ThreadStateDemo {
public static void main(String[] args) throws InterruptedException {
Object lock = new Object();
Thread thread = new Thread(() -> {
System.out.println("初始状态:" + Thread.currentThread().getState());
synchronized (lock) {
try {
System.out.println("等待前状态:" + Thread.currentThread().getState());
lock.wait();
System.out.println("等待后状态:" + Thread.currentThread().getState());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
System.out.println("创建后状态:" + thread.getState());
thread.start();
Thread.sleep(1000); // 确保线程进入等待状态
System.out.println("等待中状态:" + thread.getState());
synchronized (lock) {
lock.notify();
}
Thread.sleep(1000);
System.out.println("结束后状态:" + thread.getState());
}
}
2. yield() 方法
方法说明
-
作用:提示线程调度器让出当前线程的执行
-
特点:
-
静态方法
-
只是建议性的,不保证一定让出CPU
-
只会让出一次CPU时间片
-
调用yield会让当前线程从Running进入Runnable状态
-
具体的实现依赖于操作系统的任务调度器
-
如果没有相同优先级的线程,那么yield的效果就会失效
示例代码
java
public class ThreadYieldDemo {
public static void main(String[] args) {
Runnable task = () -> {
for (int i = 0; i < 5; i++) {
System.out.println(
Thread.currentThread().getName() +
" 执行第 " + i + " 次");
if (i == 2) {
System.out.println(
Thread.currentThread().getName() +
" 让出CPU执行权");
Thread.yield();
}
}
};
Thread t1 = new Thread(task, "线程1");
Thread t2 = new Thread(task, "线程2");
t1.start();
t2.start();
}
}
线程1 执行第 0 次
线程2 执行第 0 次
线程1 执行第 1 次
线程2 执行第 1 次
线程1 执行第 2 次
线程1 让出CPU执行权
线程2 执行第 2 次
线程2 让出CPU执行权
线程1 执行第 3 次
线程2 执行第 3 次
线程1 执行第 4 次
线程2 执行第 4 次
三、线程基本操作方法
1. getId() 方法
方法说明
-
作用:获取线程的唯一标识符
-
返回值:long类型的线程ID
-
特点:
-
线程ID在JVM运行期间唯一
-
线程终止后,该ID可能被重用
示例代码
java
public class ThreadIdDemo {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
System.out.println("线程ID: " + Thread.currentThread().getId());
System.out.println("线程名称: " + Thread.currentThread().getName());
});
System.out.println("新建线程ID: " + thread.getId());
thread.start();
// 获取主线程ID
System.out.println("主线程ID: " + Thread.currentThread().getId());
}
}
2. getName() 和 setName() 方法
方法说明
- 作用:
-
getName():获取线程名称
-
setName(String name):设置线程名称
- 特点:
-
默认线程名格式为"Thread-N",N为递增数字
-
可以通过构造函数或setName()方法设置名称
-
线程名称便于调试和日志记录
示例代码
java
public class ThreadNameDemo {
public static void main(String[] args) {
// 通过构造函数设置线程名
Thread thread1 = new Thread(() -> {
System.out.println("当前线程名称: " + Thread.currentThread().getName());
}, "CustomThread-1");
// 通过setName()方法设置线程名
Thread thread2 = new Thread(() -> {
Thread.currentThread().setName("RenamedThread");
System.out.println("修改后的线程名称: " + Thread.currentThread().getName());
});
thread1.start();
thread2.start();
// 修改主线程名称
Thread.currentThread().setName("MainThread");
System.out.println("主线程名称: " + Thread.currentThread().getName());
}
}
3. currentThread() 方法
方法说明
-
作用:获取当前正在执行的线程对象的引用
-
特点:
-
静态方法
-
返回Thread类型的当前线程引用
-
常用于获取当前线程的各种属性
示例代码
java
public class CurrentThreadDemo {
public static void main(String[] args) {
// 在不同线程中获取当前线程引用
Thread thread = new Thread(() -> {
Thread current = Thread.currentThread();
System.out.println("子线程信息:");
System.out.println(" ID: " + current.getId());
System.out.println(" 名称: " + current.getName());
System.out.println(" 优先级: " + current.getPriority());
System.out.println(" 状态: " + current.getState());
});
thread.start();
// 获取主线程信息
Thread mainThread = Thread.currentThread();
System.out.println("主线程信息:");
System.out.println(" ID: " + mainThread.getId());
System.out.println(" 名称: " + mainThread.getName());
System.out.println(" 优先级: " + mainThread.getPriority());
}
}
4. setPriority() 方法
方法说明
-
作用:设置线程的优先级
-
参数范围:1-10
-
MIN_PRIORITY = 1
-
NORM_PRIORITY = 5
-
MAX_PRIORITY = 10
- 注意事项:
-
优先级仅作为建议,在cpu繁忙时候稍有作用,空闲时候几乎0作用
-
不同操作系统实现可能不同
-
优先级继承自父线程
示例代码
java
public class ThreadPriorityDemo {
public static void main(String[] args) {
Runnable counter = () -> {
int count = 0;
long start = System.currentTimeMillis();
while (System.currentTimeMillis() - start < 1000) {
count++;
}
System.out.println(
Thread.currentThread().getName() +
" 优先级: " + Thread.currentThread().getPriority() +
" 计数: " + count);
};
Thread lowPriority = new Thread(counter, "低优先级");
Thread highPriority = new Thread(counter, "高优先级");
lowPriority.setPriority(Thread.MIN_PRIORITY);
highPriority.setPriority(Thread.MAX_PRIORITY);
lowPriority.start();
highPriority.start();
}
}
四、最佳实践建议
- 线程创建
-
优先使用线程池而不是直接创建线程
-
使用Executors或ThreadPoolExecutor创建线程池
- 异常处理
-
始终捕获和处理InterruptedException
-
在catch块中恢复中断状态
- 资源管理
-
使用try-with-resources管理资源
-
及时释放锁和其他资源
- 线程安全
-
优先使用synchronized关键字
-
考虑使用并发集合类
-
避免在同步块中执行耗时操作
- 性能优化
-
合理设置线程优先级
-
避免过度使用Thread.sleep()
-
适当使用yield()让出CPU
五、两阶段终止模式
1. 两阶段终止模式
定义
- Two Phase Termination :
- 在一个线程 T1 中如何"优雅"终止线程 T2?这里的【优雅】指的是给 T2 一个处理后事的机会。
- 通过设置打断标记(
interrupt()
方法)并检查该标记(isInterrupted()
方法),可以实现线程的优雅终止。
错误思路
-
使用
stop()
方法停止线程:stop()
方法会直接杀死线程,如果此时线程锁住了共享资源,那么当它被杀死后就再也没有机会释放锁,其他线程将永远无法获取锁。
-
使用
System.exit(int)
方法停止线程:- 目的仅是停止一个线程,但这种做法会让整个程序都停止。
正确思路:两阶段终止模式
-
流程图:
plaintextwhile(true) | v 有没有被打断? | \ 是 否 | \ 料理后事 睡眠2s | | 结束循环 执行监控记录 | 有异常 | 设置打断标记
-
示例代码:
java
class TwoPhaseTermination {
private Thread monitor;
// 启动监控线程
public void start() {
monitor = new Thread(() -> {
Thread current = Thread.currentThread();
while (true) {
if (current.isInterrupted()) {
log.debug("料理后事");
break;
}
try {
Thread.sleep(1000); // 情况1
log.debug("执行监控记录"); // 情况2
} catch (InterruptedException e) {
e.printStackTrace();
// 重新设置打断标记
current.interrupt();
}
}
});
monitor.start();
}
// 停止监控线程
public void stop() {
monitor.interrupt();
}
}
2、park/unpark
:线程阻塞与精准唤醒机制
基本概念
-
park/unpark
属于LockSupport
工具类 ,用于阻塞/唤醒指定线程,不依赖锁。 -
用于构建底层同步工具,如 AQS、线程池、信号量等。
与 wait/notify 的核心区别
特性 | wait/notify |
park/unpark |
---|---|---|
所属类 | Object |
LockSupport |
是否依赖锁 | 是(需在 synchronized 中) | 否 |
阻塞单位 | 对象的等待队列 | 线程自身 |
唤醒粒度 | 随机一个 / 所有 | 指定线程精准唤醒 |
调用顺序限制 | 先 wait 再 notify | 先 unpark() 也可以 |
中断响应 | 抛出 InterruptedException |
不抛异常,但响应中断标记 |
park/unpark 的运行机制(许可模型)
-
每个线程都像背着一个"背包":
-
背包里只能放一个"干粮"------许可。
-
unpark(thread)
:往 thread 的背包放干粮(许可)。 -
park()
:如果背包有干粮就吃掉并立即返回,否则钻进帐篷(阻塞)。 -
干粮最多一份,多次
unpark()
不会叠加许可。
-
响应中断机制说明
-
park()
会被中断打断返回,但不会清除中断标记。 -
若线程中断标记为 true,调用
park()
会立即返回,相当于"没资格再阻塞了"。 -
可用
Thread.interrupted()
清除中断标志。
示例代码:验证中断会打断 park,并影响后续行为
java
public class ParkInterruptDemo {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
System.out.println("线程开始 park()...");
LockSupport.park(); // 第一次阻塞,会被中断打断返回
System.out.println("被唤醒,是否中断?" + Thread.currentThread().isInterrupted());
System.out.println("再次 park()...");
LockSupport.park(); // 中断标记未清除,会立刻返回
System.out.println("第二次返回(中断未清除)");
});
t1.start();
Thread.sleep(1000); // 确保 t1 进入 park 阻塞
t1.interrupt(); // 设置中断标志,唤醒 t1
}
}
输出示意:
线程开始 park()...
被唤醒,是否中断?true
再次 park()...
第二次返回(中断未清除)
要清除中断标记,可以加上:
java
if (Thread.interrupted()) {
// 清除中断状态,同时做一些善后处理
}
背包模型比喻
项目 | 含义 |
---|---|
干粮 | 唤醒许可(由 unpark() 发放) |
帐篷 | 阻塞等待状态 |
park() |
没干粮就进帐篷(阻塞线程) |
unpark() |
塞干粮,线程若在睡,立即醒来 |
特别提醒 | 干粮不叠加,最多一份;中断也能唤醒线程 |
Java 线程状态详解
一、操作系统层面的线程状态(以 Linux 为例)
操作系统中的线程状态主要包括以下几种:
1. 新建(New)
-
线程对象已在语言层面创建,但尚未与操作系统线程关联。
-
此时线程尚未启动。
2. 就绪(Ready)
-
线程已准备好执行,等待操作系统调度器分配 CPU。
-
处于就绪队列中,等待被选中运行。
3. 运行(Running)
-
线程正在 CPU 上执行其任务。
-
当时间片用完或有更高优先级的线程需要运行时,线程可能被抢占,返回就绪状态。
4. 阻塞(Blocked)
-
线程因等待某些事件(如 I/O 操作完成、资源可用等)而无法继续执行。
-
在阻塞状态下,线程不会被调度器调度,直到等待的事件发生。
5. 终止(Terminated)
-
线程执行完毕或被强制终止,进入不可再运行状态。
-
操作系统会清理与线程相关的资源。
这些状态之间的转换由操作系统的调度器和线程自身的行为共同决定。
二、Java API 层面的线程状态(Thread.State
枚举)
Java 中的线程状态由 Thread.State
枚举定义,主要包括以下六种:
![[Pasted image 20250423180252.png]]
1. NEW
- 线程已创建,但尚未调用
start()
方法。
2. RUNNABLE
-
线程已调用
start()
方法,处于可运行状态。 -
在操作系统层面,可能对应就绪、运行或阻塞状态。
3. BLOCKED
- 线程在等待获取对象的监视器锁,无法进入同步块或方法。
4. WAITING
-
线程无限期等待另一个线程执行特定操作以唤醒它。
-
典型方法:
Object.wait()
、Thread.join()
、LockSupport.park()
。
5. TIMED_WAITING
-
线程在指定时间内等待另一个线程的操作。
-
典型方法:
Thread.sleep()
、Object.wait(timeout)
、Thread.join(timeout)
、LockSupport.parkNanos()
、LockSupport.parkUntil()
。
6. TERMINATED
- 线程已完成执行或因异常退出。
需要注意的是,Java 的线程状态是对操作系统线程状态的抽象,不能完全对应操作系统的状态。
四、小结对比
层级 | 状态名称 | 说明 |
---|---|---|
操作系统层面 | 初始 | 语言层面线程对象,未与系统线程关联 |
可运行 | 可调度执行,包含运行中和 BIO 阻塞 | |
运行 | 正在执行任务 | |
阻塞 | 调用了阻塞 API,非锁阻塞 | |
终止 | 生命周期结束 | |
Java API 层面 | NEW | 创建未启动 |
RUNNABLE | 含运行/可运行/BIO 阻塞 | |
BLOCKED | 同步锁阻塞 | |
WAITING | 无限等待其他线程唤醒 | |
TIMED_WAITING | 限时等待 | |
TERMINATED | 线程结束 |
Java 多线程:竞态条件与临界区控制
一、竞态条件(Race Condition)
1. 定义
- 多个线程在访问共享资源时(即临界区),由于执行顺序不确定,导致结果不可预期的现象称为竞态条件。
2. 示例代码
java
static int counter = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
counter++;
}
}, "t1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
counter--;
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
log.debug("{}", counter);
}
- 预期结果是 0,实际可能是正数、负数或 0。
3. 原因分析:自增/自减不是原子操作
操作过程:
1. 读取 counter(getstatic)
2. 加/减运算(iadd/isub)
3. 写入 counter(putstatic)
- 多线程并发操作中,可能出现交叉读写,导致结果异常。
4. Java 内存模型:主内存与工作内存
- 主内存存储共享变量。
- 每个线程有工作内存,操作共享变量需在主内存与工作内存间同步。
5. 执行顺序问题举例
- T1、T2 都读取 counter 为 0。
- T1 加 1,写入主内存,counter 为 1。
- T2 减 1,也写入主内存,counter 变成 -1。
二、临界区(Critical Section)
1. 定义
- 对共享资源的读写操作所在的代码区域称为临界区。
2. 示例代码
java
static int counter = 0;
static void increment() {
counter++;
}
static void decrement() {
counter--;
}
increment()
和decrement()
方法内都是临界区。
3. 问题本质
- 多线程同时访问临界区时,若没有同步控制,就可能发生竞态条件。
4. 解决方案:同步机制
使用 synchronized
或其他锁机制保护临界区。
java
static void increment() {
synchronized (lock) {
counter++;
}
}
static void decrement() {
synchronized (lock) {
counter--;
}
}
三、解决竞态条件的方法
1. 阻塞式同步机制
synchronized
Lock
(如ReentrantLock
)
2. 非阻塞式同步机制
- 原子类(
AtomicInteger
、AtomicLong
等)
四、synchronized 的工作机制
1. 对象锁
synchronized
使用对象锁控制并发访问,保证临界区互斥执行。
2. 互斥与同步的区别
概念 | 含义 |
---|---|
互斥 | 控制临界区只能同时由一个线程执行 |
同步 | 控制线程间执行的先后顺序 |
五、synchronized 的执行过程
1. 获取锁
- 尝试获得同步锁对象。
2. 执行代码
- 获得锁后执行临界区代码,不会被其他线程打断。
3. 释放锁
- 执行结束后自动释放锁,其他线程有机会继续执行。
六、synchronized 使用细节解析
1. for 循环外部加锁
java
synchronized (lock) {
for (int i = 0; i < 5000; i++) {
counter++;
}
}
- 整个循环受保护,效率更高。
2. 不同对象锁之间不互斥
java
synchronized (obj1) { ... }
synchronized (obj2) { ... }
- 互不影响。
3. 未加锁线程可破坏同步
java
// 其他线程未加锁操作
counter++;
- 如果某个线程不守规矩,没有使用 synchronized 来修改共享变量(比如 counter),就会绕过锁直接改值,会破坏原有锁的互斥效果,造成竞态。
七、总结
概念 | 说明 |
---|---|
竞态条件 | 多线程因执行顺序不确定,导致共享资源结果异常 |
临界区 | 涉及共享资源访问的代码块 |
synchronized | Java 原生同步机制,确保临界区互斥访问 |
同步 vs 互斥 | 同步控制执行顺序,互斥控制同一时刻只有一个线程进入临界区 |
变量的线程安全分析
当一段代码或方法 在被多个线程同时调用 时,仍能正确处理共享变量 ,不出现竞态条件或数据错误,这段代码就被认为是线程安全的。
为了实现线程安全,需要解决以下三个核心问题:
① 原子性(Atomicity)
原子性指的是一个操作要么全部执行,要么全部不执行 ,中间不会被线程切换打断。
常见方式:加锁(如 synchronized
、ReentrantLock
)或使用原子类(如 AtomicInteger
)。
示例:
i++
不是原子操作,必须使用加锁或原子类才能保证其线程安全。
② 可见性(Visibility)
可见性是指一个线程对共享变量的修改,能够被其他线程立即看到 。
JMM(Java 内存模型)允许线程缓存变量的副本,因此一个线程修改的值,可能对其他线程不可见。
常见方式:使用 volatile
、synchronized
或显式加锁来保证可见性。
③ 有序性(Ordering)
有序性是指程序执行的顺序符合预期,不能因指令重排序或线程间交替执行而出错(死锁、饥饿、活锁)。
在多线程下,JVM 和 CPU 可能会对指令进行优化重排。为了保证有序性:
-
使用
volatile
禁止指令重排序; -
使用
synchronized
或Lock
,这些同步机制天然就包含了有序性保障。
1. 成员变量和静态变量的线程安全性
- 没有共享:线程安全。
- 有共享 :
- 只读操作:线程安全。
- 读写操作:临界区,需要考虑线程安全。
2. 局部变量的线程安全性
- 局部变量本身:线程安全。
- 局部变量引用的对象 :
- 未逃逸方法作用范围:线程安全。
- 逃逸方法作用范围:需要考虑线程安全。
示例:
java
public static void test1() {
int i = 10;
i++;
}
- 每个线程调用
test1()
时,局部变量i
在每个线程的栈帧内存中被创建多份,因此不存在共享,线程安全。
3. 共享对象的线程安全分析
示例:
java
class ThreadSafe {
public void method1(int loopNumber) {
ArrayList<String> list = new ArrayList<>();
for (int i = 0; i < loopNumber; i++) {
method2(list);
method3(list);
}
}
public void method2(ArrayList<String> list) {
list.add("1");
}
public void method3(ArrayList<String> list) {
list.remove(0);
}
}
class ThreadSafeSubClass extends ThreadSafe {
@Override
public void method3(ArrayList<String> list) {
new Thread(() -> {
list.remove(0);
}).start();
}
}
情况分析:
- 情况1 :
method2()
和method3()
可能由其他线程调用,list
是共享的,存在线程安全问题。
- 情况2 :
- 在子类中重写
method3()
并在新线程中执行list.remove(0)
,进一步增加了线程安全风险。
- 在子类中重写
✅ 解决方案:
- 将
public
改为private
,防止子类覆盖方法,减少线程安全问题。 - 使用同步机制(
synchronized
)保护共享资源的访问。
4. 常见线程安全类
线程安全类:
- String
- Integer
- StringBuffer
- Random
- Vector
- Hashtable
- java.util.concurrent 包下的类
注意事项:
- 单个方法的原子性:上述类中的每个方法是线程安全的,不会被中断。
- 组合操作的非原子性:多个方法组合操作时,整体不一定是线程安全的。
5. 线程安全类方法的组合问题
示例:
java
Hashtable table = new Hashtable();
// 线程1, 线程2
if (table.get("key") == null) {
table.put("key", value);
}
问题分析:
- 虽然
get()
和put()
是线程安全的,但组合操作存在竞态条件。 - 两个线程可能同时执行
get()
并返回null
,然后分别执行put()
,导致数据覆盖。
✅ 解决方案:
java
synchronized(table) {
if (table.get("key") == null) {
table.put("key", value);
}
}
- 使用同步块保证整体操作的原子性。
6. 不可变类的线程安全性
特点:
- 内部状态不可改变 :如
String
和Integer
,创建后不能修改。 - 方法的线程安全性:由于对象的状态是只读的,因此所有方法都是线程安全的。
示例:
java
public class Immutable {
private final int value;
public Immutable(int value) {
this.value = value;
}
public int getValue() {
return this.value;
}
public Immutable increment() {
return new Immutable(this.value + 1);
}
}
- 每次操作返回新对象,保证线程安全,但是进行组合操作还是会导致线程不安全,
引用可以被多个线程修改。
7. Servlet 中的线程安全问题与解决方案
1️⃣ Servlet 的线程安全问题
- 单实例多线程:Servlet 是单实例多线程处理请求,需要考虑线程安全。
- 共享资源 :在 Servlet 中使用共享可变资源(如
Map
、Date
)会导致线程安全问题。
2️⃣ 示例分析
✅ 不安全:
java
public class MyServlet extends HttpServlet {
Map<String, Object> map = new HashMap<>(); // 不安全,共享可变对象
Date date = new Date(); // 不安全,共享可变对象
}
✅ 安全:
java
public class MyServlet extends HttpServlet {
private final UserService userService = new UserServiceImpl();
public void doGet(HttpServletRequest request, HttpServletResponse response) {
userService.update(...);
}
}
public class UserServiceImpl implements UserService {
private final UserDao userDao = new UserDaoImpl();
public synchronized void update() { // 使用 synchronized 保证线程安全
userDao.update();
}
}
- 将方法加上
synchronized
,保证线程安全。
8. String
类的线程安全性与 final
修饰符
特点:
- 不可变性 :
String
类是不可变类,线程安全。 final
修饰符:防止子类覆盖方法,保证线程安全。
示例:
java
public final class String {
private final char value[];
}
value
数组用final
修饰,防止修改。
9. SimpleDateFormat
的线程不安全性与解决方案
当你 new 出一个 SimpleDateFormat sdf = new SimpleDateFormat(...)
实例时,这个 sdf
实例会内部维护一个 Calendar
对象 和一个 NumberFormat
对象(专门处理日期/数字的转换)。
-
这两个对象在
SimpleDateFormat
中是 成员变量,也就是说,多个线程共享这个 sdf 实例时,实际上也在共享这两个对象。 -
当一个线程在调用
sdf.parse(...)
时,它会修改Calendar
的状态(比如设置年、月、日、时、分、秒等), -
如果此时另一个线程也调用
parse
,它也会修改同一个Calendar
,那么两个线程的操作就会互相干扰!
示例:
foo
方法的行为是不确定的,可能导致不安全的发生,被称之为"外星方法"。
java
public abstract class Test {
public void bar() {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
foo(sdf);
}
public abstract void foo(SimpleDateFormat sdf);
public static void main(String[] args) {
new Test() {
@Override
public void foo(SimpleDateFormat sdf) {
String dateStr = "1999-10-11 00:00:00";
for (int i = 0; i < 20; i++) {
new Thread(() -> {
try {
sdf.parse(dateStr); // 线程不安全
} catch (ParseException e) {
e.printStackTrace();
}
}).start();
}
}
}.bar();
}
}
问题:
SimpleDateFormat
不是线程安全的,多个线程同时调用会抛出异常或返回错误结果。
✅ 解决方案:
java
public abstract class Test {
public void bar() {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
foo(sdf);
}
public abstract void foo(SimpleDateFormat sdf);
public static void main(String[] args) {
new Test() {
@Override
public void foo(SimpleDateFormat sdf) {
String dateStr = "1999-10-11 00:00:00";
for (int i = 0; i < 20; i++) {
new Thread(() -> {
synchronized (sdf) {
try {
sdf.parse(dateStr); // 加同步块保证线程安全
} catch (ParseException e) {
e.printStackTrace();
}
}
}).start();
}
}
}.bar();
}
}
- 使用同步块保证线程安全。
Monitor 概念
1. Java 对象头
在 Java 虚拟机(JVM)中,每个对象都有一个对象头(Object Header),用于存储与对象相关的重要信息。
✅ 对象头结构
(1)普通对象
- 对象头 :占用 64 位(8 字节)。
- Mark Word:32 位,用于存储对象的元数据(如锁状态、偏向锁线程 ID 等)。
- Klass Word:32 位,指向对象所属的类(Class)。
(2)数组对象
- 对象头 :占用 96 位(12 字节)。
- Mark Word:32 位,与普通对象相同。
- Klass Word:32 位,指向数组的类。
- array length:32 位,存储数组长度。
2. Mark Word 的结构
Mark Word
是对象头中最重要的部分之一,用于存储对象的状态信息。
✅ Mark Word 的状态
- Normal:正常状态,未被锁定。
- Biased:偏向锁状态,记录偏向的线程 ID。
- Lightweight Locked:轻量级锁状态,记录持有锁的线程 ID。
- Heavyweight Locked :重量级锁状态,指向
Monitor
对象。 - Marked for GC:标记为垃圾回收状态。
✅ Mark Word 的字段
- hashcode:对象的哈希码。
- age:对象年龄,用于分代垃圾回收。
- biased_lock:偏向锁标志位。
- ptr_to_lock_record:指向锁记录的指针。
- ptr_to_heavyweight_monitor :指向重量级锁的
Monitor
对象。
![[Pasted image 20250422145717.png]]
3. Monitor(监视器)
Monitor
是 JVM 中实现同步的核心机制,每个 Java 对象都可以关联一个 Monitor
对象。当使用 synchronized
关键字对对象加锁时,该对象的 Mark Word
中会被设置为指向 Monitor
对象的指针。
✅ Monitor 的结构
- Owner:当前持有锁的线程。
- EntryList:等待获取锁的线程队列。
- WaitSet:等待条件变量的线程集合。
✅ Monitor 的工作原理
-
初始状态
Monitor
中的Owner
为null
,表示没有线程持有锁。
-
线程获取锁
- 当线程执行
synchronized(obj)
时,尝试获取obj
的锁。 - 如果锁未被其他线程持有:
- 将
Monitor
的Owner
设置为当前线程。 - 进入临界区。
- 将
- 如果锁已被其他线程持有:
- 当前线程加入到
EntryList
中,进入BLOCKED
状态,等待锁的释放。
- 当前线程加入到
- 当线程执行
-
线程释放锁
- 当持有锁的线程退出临界区时:
- 释放锁。
- 将
Monitor
的Owner
置为null
。 - 如果
EntryList
中有等待的线程:- 唤醒其中一个线程,尝试获取锁。
- 当持有锁的线程退出临界区时:
在 Hotspot 虚拟机中,Monitor 由 ObjectMonitor 实现:
java
ObjectMonitor() {
_count = 0; // 记录线程获取锁的次数
_owner = NULL; // 指向持有ObjectMonitor对象的线程
_WaitSet = NULL; // 处于wait状态的线程,会被加入到_WaitSet
_cxq = NULL ;
_EntryList = NULL ; // 处于等待锁block状态的线程,会被加入到该列表
}
+----------------------+
| ObjectMonitor |
| ---------------- |
| _owner = Thread-1 | // 当前持有锁的线程
| _count = 1 | // 线程获取锁的次数
| _WaitSet -> T3,T4 | // 执行 wait() 的线程
| _EntryList -> T2,T5| // 竞争锁的线程
| _cxq -> T6,T7 | // 新进入的线程
+----------------------+
-
_owner
:当前持有 ObjectMonitor 的线程,初始值为 null,表示没有线程持有锁。线程成功获取锁后,该值更新为线程 ID,释放锁后重置为 null。 -
_count
:记录当前线程获取锁的次数(可重入锁),每次成功加锁 _count + 1,释放锁 _count - 1。 -
_WaitSet
:等待队列,调用 wait() 方法后,线程会释放锁,并加入 _WaitSet,进入 WAITING 状态,等待 notify() 唤醒。 -
_cxq
:阻塞队列,用于存放刚进入 Monitor 的线程(还未进入 _EntryList)。 -
_EntryList
:竞争队列,所有等待获取锁的线程(BLOCKED 状态)会进入 _EntryList
,等待锁释放后竞争执行权。
4. synchronized 的实现
synchronized
是基于 Monitor
实现的,保证多线程环境下的对象级别互斥锁。
✅ 示例
java
synchronized (obj) { // 加锁
// 临界区代码
}
✅ 加锁过程
- 线程尝试获取
obj
的锁。 - 如果锁未被其他线程持有:
- 将
Monitor
的Owner
设置为当前线程。 - 进入临界区。
- 将
- 如果锁已被其他线程持有:
- 当前线程加入到
EntryList
中,进入BLOCKED
状态。
- 当前线程加入到
✅ 解锁过程
- 线程退出临界区时:
- 释放锁。
- 将
Monitor
的Owner
置为null
。
- 如果
EntryList
中有等待线程:- 唤醒其中一个线程,使其尝试获取锁。
6. 总结
-
Mark Word
- 是对象头中的关键部分。
- 存储对象状态信息,如锁状态、偏向锁线程 ID 等。
-
Monitor
- 用于实现同步的关键机制。
- 每个 Java 对象都可以关联一个
Monitor
对象。
-
synchronized
- 基于
Monitor
实现对象级别的互斥锁。 - 确保临界区代码的线程安全性。
- 基于
Java锁机制
引言
Java的同步机制包括轻量级锁、重量级锁和偏向锁等多种形式,旨在平衡线程安全与性能。
1. 轻量级锁
使用场景
- 适用场景:当对象被多线程访问但访问时间错开(无竞争)时,轻量级锁可优化性能。
- 透明性 :对开发者透明,语法仍为
synchronized
,无需额外操作。
核心机制
锁记录(Lock Record)
- 每个线程的栈帧包含锁记录结构,用于存储锁定对象的Mark Word。
- 锁记录字段 :
Object reference
:指向被锁定的对象。lock record address
:存储锁记录的地址。
加锁过程
- 尝试获取锁 :
- 进入
synchronized
块时,线程用 CAS 把 obj 的 Mark Word(对象头信息)放进自己的 Lock Record。 - 如果CAS成功:
- 将Mark Word存入锁记录。
- 对象头存储锁记录地址和状态
0x01
,表示线程成功持有锁。
- 如果CAS失败:
- 可能情况:
- CAS自旋
- 其他线程已持有轻量级锁,进入锁膨胀。
- 自身重入,添加新锁记录作为重入计数。
- 可能情况:
- 进入
解锁过程
- 退出
synchronized
块时 :- 如果锁记录为
null
,表示重入,重置锁记录,重入计数减一。 - 如果锁记录不为
null
,使用CAS恢复Mark Word到对象头:- 成功:解锁成功。
- 失败:锁已膨胀或升级为重量级锁,进入相应解锁流程。
- 如果锁记录为
图解过程
- 初始状态 :对象头存储
Hashcode Age Bias
和Klass Word
(![[Pasted image 20250321095606.png]])。 - CAS成功后 :对象头存储锁记录地址和状态
0x01
,锁记录中存Mark Word(![[Pasted image 20250321095626.png]])。 - CAS失败后:进入锁膨胀或重入处理(![[Pasted image 20250321095646.png]])。
- 解锁成功:恢复Mark Word(![[Pasted image 20250321095714.png]])。
总结
- 优点 :
- 通过CAS高效实现无锁化,避免重量级锁开销。
- 对开发者透明,使用方式一致。
- 局限性 :
- 竞争时升级为重量级锁,性能下降。
- 适用于竞争较少的场景。
重量级锁、锁膨胀与自旋优化
重量级锁
- 基于操作系统的互斥量(Mutex)或条件变量,传统同步机制。
- 阻塞与上下文切换 :
- 锁被占用时,线程阻塞,涉及上下文切换,性能开销大。
- 适用场景 :
- 适合竞争激烈的场景,多线程频繁争夺同一锁。
锁膨胀
- 当轻量级锁无法满足需求时,升级为重量级锁的过程。
轻量级锁加锁失败
- 场景:线程用CAS获取锁,发现对象已被其他线程持有。
- 结果:进入锁膨胀。
锁膨胀流程
- 申请Monitor锁 :
- 为对象申请Monitor锁,Mark Word更新为指向Monitor的地址。
- 进入EntryList :
- 当前线程进入Monitor的
EntryList
,处于BLOCKED状态等待。
- 当前线程进入Monitor的
- Monitor结构 :
- Owner:当前持有锁的线程。
- EntryList:等待获取锁的线程队列。
- WaitSet :调用
wait()
进入等待的线程。
图示说明
- 初始状态:Thread-0持有轻量级锁,Mark Word存储锁记录地址(![[Pasted image 20250321100653.png]])。
- 锁膨胀后 :Thread-1申请Monitor锁,Mark Word指向Monitor,进入
EntryList
。
自旋优化
- 在重量级锁竞争中,线程不立即阻塞,通过循环尝试获取锁。
自旋成功的场景
线程 1(CPU 1 上) | 对象 Mark | 线程 2(CPU 2 上) |
---|---|---|
- | 10(重量锁) | - |
获取monitor成功 | 10(重量锁) | - |
执行同步块 | 10(重量锁) | 尝试获取monitor |
执行完毕 | 01(无锁) | 成功获取锁 |
- 描述:Thread-1释放锁后,Thread-2立即获取。
自旋失败的场景
线程 1(CPU 1 上) | 对象 Mark | 线程 2(CPU 2 上) |
---|---|---|
- | 10(重量锁) | - |
获取monitor成功 | 10(重量锁) | 尝试获取monitor |
执行同步块 | 10(重量锁) | 自旋重试 |
执行同步块 | 10(重量锁) | 阻塞 |
- 描述:Thread-1执行时间长,Thread-2自旋失败后阻塞。
自旋锁特点
- 自适应性:Java 6后自适应,若最近自旋成功,增加自旋次数;否则减少。
- 多核CPU优势:多核 CPU 能同时运行多个线程,自旋线程不会阻塞 CPU
- Java 7后:自旋由JVM自动管理,开发者无法手动控制。
总结
- 重量级锁适合高竞争场景,但开销大。
- 锁膨胀处理竞争升级,涉及Monitor机制。
- 自旋优化适合多核CPU,动态调整策略。
偏向锁的原理与实现
概念
- Java 6引入,优化单线程环境,假设对象主要由一个线程访问,绑定锁减少CAS。
工作原理
启用
- 默认开启(
-XX:+UseBiasedLocking
)。 - 对象创建时,Mark Word最后3位为
101
,表示偏向锁状态,初始未绑定线程。
激活
- 线程首次获取锁,用CAS将线程ID写入Mark Word,之前是地址。
- 成功后,后续访问无需CAS,直接检查线程ID。
状态
- Normal:未锁定。
- Biased:偏向锁状态。
- Lightweight Locked:轻量级锁。
- Heavyweight Locked:重量级锁。
- Marked for GC:垃圾回收标记。
偏向锁的撤销
- 多线程竞争或特定操作触发撤销,重新偏向于新的线程,升级为轻量级/重量级锁。
调用hashCode()
- 修改Mark Word存储哈希码,需撤销偏向锁,对象头里要放 hashCode,就没地方存线程 ID,只能撤销偏向。
其他线程尝试获取锁
-
检查发现 ID 不匹配
-
JVM 撤销偏向,重新偏向于新的线程或升级为轻量级锁或重量级锁
优缺点
- 优点 :
- 单线程环境性能提升,避免CAS。
- 延迟生效,竞争时才撤销。
- 缺点 :
- 多线程竞争时需升级,额外开销。
- 增加JVM复杂性。
偏向锁的优化与撤销机制
重新偏向
- 对象被多线程访问无竞争时,JVM可能重新偏向新线程。
批量重偏向与批量撤销
批量重偏向
当某个锁(或一批同类对象)频繁被多次偏向撤销(撤销次数超过20次)后,说明这种对象在实际运行中经常由不止一个线程使用。原来的偏向锁机制不断被"撤销"后,反而消耗性能,这时JVM会尝试将这批对象一次性"重偏向"给当前经常加锁的线程,也可以理解为对这些对象执行统一"重新偏向"的操作。
批量撤销
如果对某个类的对象偏向锁撤销次数超过40次,说明这些对象的使用模式严重不适合偏向锁(即竞争非常激烈)。在这种情况下,JVM会选择对该类所有新创建的对象直接标记为"不可偏向"(即禁用偏向锁)。
锁消除
- JIT编译器移除无意义同步块,提升性能。
- 可通过
-XX:-EliminateLocks
禁用。
Java 并发设计模式
一、同步模式 - 保护性暂停模式(Guarded Suspension)
1. 核心概念
Guarded Suspension 是一种线程间通信模式,适用于一个线程等待另一个线程结果的场景。通过一个共享对象 GuardedObject
来协调两个线程间的同步操作。
-
线程 t1:等待结果。
-
线程 t2:完成任务后设置结果并通知 t1。
2. 关键要点
共享对象(GuardedObject)
-
充当线程之间的"桥梁"。
-
内部封装结果
response
。 -
提供阻塞式
get()
方法获取结果,及非阻塞的complete()
方法设置结果。 -
典型实现使用
synchronized + wait/notify
。
使用场景
-
一次性结果传递:适合请求-响应式通信,如:
-
网络请求结果
-
异步任务完成通知
-
-
不适用于持续数据交换,应改为生产者-消费者模型(例如阻塞队列)。
JDK 中的体现
-
Thread.join()
:主线程等待子线程结束。 -
Future.get()
:获取异步任务结果。 -
CountDownLatch
/CyclicBarrier
:高级同步工具,底层思想类似。
3. 工作流程图解
text
t1 线程 t2 线程
↓ ↓
调用 get() -> wait() 执行任务
←------------- 调用 complete() + notify()
继续执行 <- 被唤醒,返回结果
4. 虚假唤醒与 while 判断的必要性
什么是虚假唤醒?
-
即使没有调用
notify()
,线程也可能被唤醒。 -
这是 JVM 规范中允许的行为,出于性能优化考虑。
为什么要用 while(response == null)
?
-
避免被唤醒时条件未满足就返回,导致逻辑错误。
-
while
可以保证 重新判断条件是否成立,是多线程编程的基本规范 -
假如在唤醒后 response 被其他线程修改或置空,线程依旧不会检查,会误以为条件成立。
5. 示例代码(无超时 + 带超时)
GuardedObject 类
java
class GuardedObject {
private Object response;
public Object get() {
synchronized (this) {
while (response == null) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return response;
}
}
public Object get(long timeout) {
synchronized (this) {
long begin = System.currentTimeMillis();
long elapsed = 0;
while (response == null) {
long remaining = timeout - elapsed;
if (remaining <= 0) break;
try {
this.wait(remaining);
} catch (InterruptedException e) {
e.printStackTrace();
}
elapsed = System.currentTimeMillis() - begin;
}
return response;
}
}
public void complete(Object response) {
synchronized (this) {
this.response = response;
this.notifyAll();
}
}
}
特点:
-
支持超时等待,避免线程无限阻塞。
-
仍需使用
while
来处理虚假唤醒。
使用示例
java
public class TestGuardedObject {
public static void main(String[] args) {
GuardedObject go = new GuardedObject();
new Thread(() -> {
log.debug("等待结果");
Object result = go.get(3000L); // 支持超时
log.debug("收到结果: {}", result);
}, "t1").start();
new Thread(() -> {
log.debug("执行任务");
try {
Thread.sleep(1000); // 模拟任务耗时
go.complete("任务完成");
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "t2").start();
}
}
7. 使用场景
场景 | 是否适合 Guarded Suspension |
---|---|
单次请求 - 响应(如 RPC 回调) | ✅ 适合 |
任务结果等待(如 Future.get) | ✅ 适合 |
多次通信(如日志写入) | ❌ 改用生产者-消费者模型 |
高并发场景 | ❌ 使用队列 + 线程池更高效 |
8. 与其他模型对比
模型 | 特点 | 用途 |
---|---|---|
Guarded Suspension | 等结果 | 请求-响应 |
Future / CompletableFuture | 支持超时、回调 | 异步编程 |
生产者-消费者 | 持续处理数据流 | 队列模型 |
Reactor 模式 | 事件驱动非阻塞 | 高性能网络编程(Netty) |
9.join原理
java
public final synchronized void join(long millis)
throws InterruptedException {
// 1. 检查参数是否合法
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
// 2. 如果 millis == 0,则无限等待
if (millis == 0) {
while (isAlive()) { // 循环检查目标线程是否存活
wait(0); // 阻塞当前线程,无限等待
}
} else {
// 3. 如果有超时时间,则计算剩余时间并循环等待
long base = System.currentTimeMillis(); // 记录开始时间
long now = 0;
while (isAlive()) { // 循环检查目标线程是否存活
long delay = millis - now; // 计算剩余等待时间
// 4. 如果剩余时间小于等于 0,退出循环
if (delay <= 0) {
break;
}
// 5. 调用 wait(delay),阻塞当前线程,最多等待 delay 毫秒
wait(delay);
// 6. 更新已过去的时间
now = System.currentTimeMillis() - base;
}
}
}
-
内部机制:
- 线程结束后会自动调用
notifyAll()
。
join()
会循环判断isAlive()
,在不满足时调用wait()
。 - 线程结束后会自动调用
10.Futures 的设计与应用(信箱模型)
背景
虽然 GuardedObject
模式能解决"线程等待结果"的问题,但存在以下局限:
-
一次性交互 :每个
GuardedObject
实例只能传递一次结果。 -
耦合度高 :在多个类间传递
GuardedObject
作为参数繁琐,不利于代码解耦。
引入 Futures(信箱模型)
我们设计一个 Futures
类,用来统一管理多个"等待对象"(GuardedObject
),实现更优雅的线程同步机制。
核心思想
-
将每个线程的"结果等待"对象用唯一 ID 管理(如 UUID、订单号等)。
-
Futures
类统一管理所有信箱(即 GuardedObject 实例)。 -
线程通过 ID 获取对应的
GuardedObject
并阻塞等待,直到另一个线程传入结果。
角色说明
线程 | 角色 | 描述 |
---|---|---|
t0, t2, t4 |
居民(等待者) | 从 Futures 中获取 GuardedObject 并调用 get() 等待结果 |
t1, t3, t5 |
邮递员(生产者) | 向 Futures 中的 GuardedObject 调用 complete() 传入结果 |
示例代码
GuardedObject
实现
java
public class GuardedObject<T> {
private T response;
public synchronized T get(long timeout) {
long begin = System.currentTimeMillis();
long timePassed = 0;
while (response == null) {
long waitTime = timeout - timePassed;
if (waitTime <= 0) break;
try {
this.wait(waitTime);
} catch (InterruptedException e) {
e.printStackTrace();
}
timePassed = System.currentTimeMillis() - begin;
}
return response;
}
public synchronized void complete(T response) {
this.response = response;
this.notifyAll();
}
}
Futures
类(信箱统一管理)
java
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
public class Futures {
private final Map<Long, GuardedObject<String>> boxes = new ConcurrentHashMap<>();
private final AtomicLong idGenerator = new AtomicLong(1);
// 创建新的信箱并返回 ID
public long create() {
long id = idGenerator.getAndIncrement();
boxes.put(id, new GuardedObject<>());
return id;
}
// 获取信箱
public GuardedObject<String> getBox(long id) {
return boxes.get(id);
}
// 发送消息后移除信箱
public void complete(long id, String msg) {
GuardedObject<String> box = boxes.remove(id);
if (box != null) {
box.complete(msg);
}
}
}
测试代码:模拟居民和邮递员
java
public class Demo {
public static void main(String[] args) {
Futures futures = new Futures();
// 模拟 3 个居民线程等待消息
for (int i = 0; i < 3; i++) {
int id = i;
new Thread(() -> {
long boxId = futures.create();
System.out.println("居民 " + id + " 正在等待信箱ID: " + boxId);
String result = futures.getBox(boxId).get(5000); // 最多等待5秒
System.out.println("居民 " + id + " 收到消息: " + result);
}).start();
}
// 模拟 3 个邮递员线程投递消息
for (int i = 0; i < 3; i++) {
int id = i;
new Thread(() -> {
try {
Thread.sleep(1000 + id * 500); // 模拟工作时间
} catch (InterruptedException e) {
e.printStackTrace();
}
futures.complete(id + 1, "你好,我是邮递员 " + id);
}).start();
}
}
}
总结
模块 | 说明 |
---|---|
GuardedObject<T> |
单个等待/唤醒机制 |
Futures |
管理所有信箱,解耦请求与响应 |
居民线程 |
通过 get() 等待消息 |
邮递员线程 |
通过 complete() 投递消息 |
异步模式 - 生产者 / 消费者模式
一、定义
生产者 / 消费者模式(Producer-Consumer Pattern) 是一种异步编程模型,核心思想是通过一个共享的缓冲区(消息队列)来连接生产者线程和消费者线程,实现解耦。它使得两者无需直接通信即可协同工作。
二、核心要点
1. 与 GuardObject 的区别
-
GuardObject 强调的是"一对一"通信,即一个线程等待另一个线程的结果。
-
Producer-Consumer 强调的是"多对多"解耦,生产者只管生成,消费者只管处理,彼此独立。
2. 消息队列的作用
-
解耦生产者和消费者的执行时机。
-
控制系统资源的使用,避免过度生产或过度消费。
-
实现线程间的数据传递。
3. 队列容量限制
-
满时 :阻塞生产者(如阻塞
put
操作)。 -
空时 :阻塞消费者(如阻塞
take
操作)。
4. JDK 应用
-
JDK 中提供了多种阻塞队列(
BlockingQueue
接口)实现了该模式,如:-
ArrayBlockingQueue
-
LinkedBlockingQueue
-
PriorityBlockingQueue
-
三、工作流程
1. 生产者线程
-
生成数据,调用
put()
放入队列; -
如果队列已满,则阻塞,直到队列有空位。
2. 消费者线程
-
从队列中调用
take()
获取数据; -
如果队列为空,则阻塞,直到有新数据可取。
3. 消息队列的桥梁作用
-
实现线程间的通信;
-
平衡生产与消费速率;
-
缓冲数据,提升系统吞吐量。
四、基于 BlockingQueue 的实现(推荐)
java
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
public class ProducerConsumerExample {
private static BlockingQueue<Integer> queue = new LinkedBlockingQueue<>(3);
public static class Producer implements Runnable {
@Override
public void run() {
int value = 0;
while (true) {
try {
queue.put(value);
System.out.println("Produced: " + value++);
} catch (InterruptedException e) {
break;
}
}
}
}
public static class Consumer implements Runnable {
@Override
public void run() {
while (true) {
try {
int consumed = queue.take();
System.out.println("Consumed: " + consumed);
} catch (InterruptedException e) {
break;
}
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread producer = new Thread(new Producer());
Thread consumer = new Thread(new Consumer());
producer.start();
consumer.start();
Thread.sleep(5000); // 运行 5 秒后中断线程
producer.interrupt();
consumer.interrupt();
}
}
五、自定义消息队列实现(synchronized + wait/notify)
消息类定义:
java
class Message {
private final int id;
private final Object value;
public Message(int id, Object value) {
this.id = id;
this.value = value;
}
public int getId() {
return id;
}
public Object getValue() {
return value;
}
@Override
public String toString() {
return "Message{id=" + id + ", value=" + value + '}';
}
}
自定义阻塞队列:
java
import java.util.LinkedList;
public class MessageQueue {
private final LinkedList<Message> list = new LinkedList<>();
private final int capacity;
public MessageQueue(int capacity) {
this.capacity = capacity;
}
public void put(Message message) {
synchronized (list) {
while (list.size() == capacity) {
try {
list.wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
list.addLast(message);
list.notifyAll(); // 通知消费者
}
}
public Message take() {
synchronized (list) {
while (list.isEmpty()) {
try {
list.wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
Message message = list.removeFirst();
list.notifyAll(); // 通知生产者
return message;
}
}
}
六、总结
项目 | 说明 |
---|---|
模式核心 | 解耦生产与消费,提升系统并发度 |
通信方式 | 线程间通信,使用共享队列(线程安全) |
阻塞行为 | 满了阻塞生产者,空了阻塞消费者 |
JDK实现 | BlockingQueue 是该模式的官方支持 |
应用场景 | 日志系统、消息传递、线程池任务提交等 |
⚠️ 线程间通信 ≠ 进程间通信(MQ等消息中间件如 Kafka、RabbitMQ 属于进程间通信)
Java 锁的活跃性
死锁
锁的力度太大,并发性不好,力度小,容易导致死锁等问题
1. 死锁的定义
- 死锁是指两个或多个线程因互相等待对方释放资源而陷入无限等待的状态,导致程序无法继续执行。
2. 死锁发生的条件
死锁的发生需要满足以下四个必要条件(称为"死锁四要素"):
-
互斥条件(Mutual Exclusion):
- 资源不能被共享,即在同一时刻只能有一个线程使用该资源。
-
占有并等待条件(Hold and Wait):
- 线程在持有某个资源的同时,又请求其他资源。
-
不可剥夺条件(No Preemption):
- 已经分配给线程的资源不能被强制剥夺,只能由线程自己释放。
-
循环等待条件(Circular Wait):
- 存在一个线程链,每个线程都在等待下一个线程所持有的资源。
若想避免死锁,必须破坏其中至少一个条件。
哲学家问题
** 问题背景**
哲学家就餐问题是计算机科学中经典的同步问题,用于描述多线程环境下的资源竞争和死锁问题。该问题的场景如下:
-
场景描述:
- 有 5 位哲学家 围坐在一张圆桌旁。
- 每位哲学家面前有一盘食物,但需要使用 两根筷子 才能吃饭。
- 桌上共有 5 根筷子,每位哲学家左右手边各有一根筷子。
- 哲学家只能使用左手边和右手边的筷子,不能跨越其他哲学家拿筷子。
-
哲学家的行为:
- 哲学家交替进行 思考 和 吃饭 的动作。
- 当哲学家想吃饭时,必须同时获得左手边和右手边的筷子。
- 如果筷子被其他哲学家占用,则当前哲学家需要等待。
-
潜在问题:
- 如果每个哲学家都试图同时拿起左右手的筷子,可能会导致 死锁,即所有哲学家都永远无法获得足够的筷子来吃饭。
-
常见的解决方案:
- 限制哲学家同时拿起两根筷子:确保哲学家在拿到两根筷子后才能吃饭。
- 引入优先级或顺序规则:例如,规定哲学家必须按照某种顺序(如顺时针或逆时针)获取筷子。
- 使用信号量或其他同步机制 :通过更高级的同步工具(如
Semaphore
)管理筷子的分配。
示例代码
java
class Chopstick {
private final String name;
public Chopstick(String name) {
this.name = name;
}
@Override
public String toString() {
return "Chopstick{" + name + "}";
}
}
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@SLF4J(topic = "c.Philosopher")
class Philosopher extends Thread {
private static final Logger log = LoggerFactory.getLogger("c.Philosopher");
private final String name;
private final Chopstick left;
private final Chopstick right;
public Philosopher(String name, Chopstick left, Chopstick right) {
super(name);
this.name = name;
this.left = left;
this.right = right;
}
@Override
public void run() {
while (true) {
// 尝试获得左手筷子
synchronized (left) {
log.debug("{} got left chopstick: {}", name, left);
// 尝试获得右手筷子
synchronized (right) {
log.debug("{} got right chopstick: {}", name, right);
eat();
}
log.debug("{} released right chopstick: {}", name, right);
}
log.debug("{} released left chopstick: {}", name, left);
}
}
private void eat() {
log.debug("{} is eating...", name);
Sleeper.sleep(1); // 模拟吃饭的时间
}
}
public class TestDeadlock {
// 一开始是可能你吃一下我吃一下这样,因为获取锁速度不一样,但后面就会死锁卡住
public static void main(String[] args) {
Chopstick c1 = new Chopstick("1");
Chopstick c2 = new Chopstick("2");
Chopstick c3 = new Chopstick("3");
Chopstick c4 = new Chopstick("4");
Chopstick c5 = new Chopstick("5");
new Philosopher("苏格拉底", c1, c2).start();
new Philosopher("柏拉图", c2, c3).start();
new Philosopher("亚里士多德", c3, c4).start();
new Philosopher("赫拉克利特", c4, c5).start();
new Philosopher("阿基米德", c5, c1).start();
}
}
4. 定位死锁的方法
死锁发生时,程序会陷入僵持状态,无法继续执行。为了定位死锁,可以使用以下工具和方法:
(1) 使用 jstack
查看线程堆栈
-
步骤:
-
使用
jps
命令找到 JVM 进程的 ID:bashjps
输出类似:
12345 Jps 67890 Main
-
使用
jstack
命令生成线程堆栈信息:bashjstack 67890 > thread_dump.txt
-
在生成的
thread_dump.txt
文件中查找关键字"waiting for monitor entry"
或"BLOCKED"
,这些关键字表明线程正在等待锁。
-
-
示例输出:
plaintext"Thread-1" #12 prio=5 os_prio=0 tid=0x000000001eb69000 nid=0xd40 waiting for monitor entry [0x00000001f54f000] java.lang.Thread.State: BLOCKED (on object monitor) at thread.TestDeadLock.lambda$main$1(TestDeadLock.java:28) - waiting to lock <0x000000076b5bfid> (a java.lang.Object) - locked <0x000000076b5bfid> (a java.lang.Object) at thread.TestDeadLock$$Lambda$2/883049899.run(Unknown Source)
-
解释:
-
"BLOCKED"
表明线程处于阻塞状态。 -
"waiting to lock <0x...>
表明线程正在等待某个对象的锁。 -
"locked <0x...>
表明线程已经持有的锁。
-
(2) 使用 jconsole
工具
- 步骤:
-
启动
jconsole
工具。 -
连接到目标 JVM 进程。
-
在
Threads
标签页中查看线程状态,重点关注BLOCKED
状态的线程。 -
查看线程的堆栈信息,确认是否存在循环等待的情况。
-
(3) 使用日志记录
- 在代码中添加日志,记录线程获取锁的时间点和锁的顺序,便于排查死锁原因。
五、避免死锁的策略
1. 固定加锁顺序
- 所有线程按相同顺序请求锁资源(例如按 hashcode 顺序加锁)。
2. 尝试加锁 + 超时回退
- 使用
tryLock(timeout)
方式加锁,若超时未获得锁则主动放弃已有资源并重试。
3. 引入资源分配调度
- 通过信号量(
Semaphore
)、调度器控制资源分配,避免资源争抢。
活锁(Livelock)
1. 活锁的定义
活锁是一种特殊的并发问题,其中多个线程或进程不断地尝试执行操作,但因为过多的状态变化或资源竞争,它们无法有效完成任务。虽然线程或进程并不会像死锁那样无限阻塞,但它们仍然无法推进程序的进度。
与死锁的"资源占用"不同,活锁通常表现为资源被不断释放和重新申请,却永远无法取得进展。活锁是系统中的一种活跃状态,但没有实际的计算或工作完成。
2. 活锁的特点
-
线程或进程不会被阻塞:线程一直在改变状态,尝试执行任务。
-
没有实际进展:尽管线程不断改变状态,它们始终无法完成任务或产生预期结果。
-
资源竞争不断:线程反复试图占用资源,但由于某种条件(如资源的独占性)总是无法成功获取资源。
3. 活锁的常见场景
-
资源竞争过度:当多个线程同时执行相同的资源请求时,可能会导致资源的争抢不断地发生,造成线程之间的频繁切换和状态变更,而始终无法完成任务。
-
调度策略不当:线程在执行时,可能由于优先级的动态调整、资源调度等原因,导致线程始终处于"尝试"状态,但没有取得真正的资源或完成任务。
-
线程不断反应而无法前进:例如,一个线程不断地退后一步以让另一个线程执行,另一个线程又反过来退后一步以给第一个线程让路。这种"相互让步"的行为会导致两个线程一直处于"等待中",而无法完成实际任务。
4. 活锁的发生条件
活锁的发生需要满足以下条件:
-
并发操作:存在多个线程或进程。
-
资源争用:多个线程或进程争夺同一资源或共享资源。
-
不断变更状态:线程或进程由于某些策略(如退让、重试等)不断变更状态,导致无法完成任务。
5. 活锁的解决方法
与死锁不同,活锁的处理策略通常会更灵活,避免线程之间进入无休止的状态变更。
-
避免过度让步 :避免线程之间的无限让步操作。可以通过设定最大重试次数 或引入时间戳来限制线程的让步。
-
引入适当的等待机制 :增加线程之间的等待时间或随机化等待时间,避免所有线程在相同时间进行状态变化。
-
使用公平锁或优先级队列 :使用公平锁(例如
ReentrantLock
的公平模式)来保证线程不会频繁地处于让步状态。优先级队列可以按照一定的顺序来调度任务,避免过度竞争。 -
超时机制 :为每个操作设置超时时间,当线程无法在合理时间内完成任务时,强制返回并做适当的回滚或重试。
6. 活锁的代码示例
java
import java.util.concurrent.locks.ReentrantLock;
class Account {
private int balance;
private final ReentrantLock lock = new ReentrantLock();
public Account(int balance) {
this.balance = balance;
}
public void transfer(Account target, int amount) {
while (true) {
if (this.lock.tryLock()) {
try {
if (target.lock.tryLock()) {
try {
if (this.balance >= amount) {
this.balance -= amount;
target.balance += amount;
System.out.println("Transfer successful!");
break;
}
} finally {
target.lock.unlock();
}
}
} finally {
this.lock.unlock();
}
}
}
}
}
public class TestLivelock {
public static void main(String[] args) {
Account account1 = new Account(100);
Account account2 = new Account(50);
// 两个线程试图互转金额
new Thread(() -> account1.transfer(account2, 50)).start();
new Thread(() -> account2.transfer(account1, 50)).start();
}
}
// 另一个例子
public class TestLivelock {
static volatile int count = 10;
static final Object lock = new Object();
public static void main(String[] args) {
new Thread(() -> {
// 期望减到 0 退出循环
while (count > 0) {
sleep(0.2);
count--;
log.debug("count: {}", count);
}
}, "t1").start();
new Thread(() -> {
// 期望超过 20 退出循环
while (count < 20) {
sleep(0.2);
count++;
log.debug("count: {}", count);
}
}, "t2").start();
}
private static void sleep(double seconds) {
try {
Thread.sleep((long) (seconds * 1000));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
问题分析:
-
上述代码中两个账户试图同时转账。由于使用
tryLock
而不是直接使用lock
,两个线程会不断尝试获取锁。如果两个线程都成功获取了各自的第一个锁,它们会继续尝试获取对方的锁,如果对方已经持有锁,则会释放自己持有的锁并重新尝试。 -
这种反复尝试和释放锁的行为会导致活锁,线程不断地变换状态,但不会成功完成转账操作。
解决方案:
可以通过添加一些超时机制,在多次尝试后停止重试,避免系统陷入无限状态变化的活锁问题。
7. 活锁与死锁的区别
-
死锁是指多个线程在资源上形成了一个循环依赖,导致系统无法继续执行,所有线程都被永久阻塞。
-
活锁则是指多个线程在尝试执行任务时,由于过多的状态改变,它们始终无法取得资源或完成任务,尽管线程处于活跃状态,但系统进展停滞。
线程饥饿(Thread Starvation)
1. 线程饥饿的定义
线程饥饿是指一个线程长时间得不到执行,无法获得所需的资源,导致它无法完成任务的状态。饥饿线程通常是由于系统中的调度策略不公平或资源分配不均衡所导致的。
2. 线程饥饿的原因
线程饥饿通常发生在以下情况下:
-
优先级调度:当某些线程的优先级较高,而低优先级线程长时间得不到调度,导致它们无法执行。
-
资源竞争:某些线程可能因一直无法获取锁而被阻塞,造成长时间的等待,最终导致线程饥饿。
-
不公平的锁机制:如果线程总是按照某种顺序获取锁,低优先级线程可能永远无法获得资源。
-
无休止的线程切换:调度器过于频繁地切换线程,导致某些线程总是被推迟执行。
3. 线程饥饿的影响
-
性能下降:如果某些线程得不到执行,可能导致程序性能严重下降。
-
死锁或活锁:在某些情况下,线程饥饿可能与死锁或活锁一起发生,造成更严重的问题。
4. 解决线程饥饿的方法
-
公平锁 :使用公平锁来保证所有线程都有机会执行。
ReentrantLock
提供了公平模式,确保线程按照请求的顺序获取锁。 -
线程优先级调整:通过合理的设置线程优先级,避免低优先级线程长时间得不到执行。
-
避免过度依赖锁:使用无锁的并发数据结构,减少锁的竞争,降低线程饥饿的风险。
-
资源分配策略:实现资源分配的策略,确保每个线程都有机会获取资源,而不是一直被其他线程抢占。
**线程饥饿示例:
在哲学家就餐问题中,如果按照严格的顺序(如顺时针方向)来获取筷子,可能会导致某些哲学家长时间无法获得足够的筷子,进而造成线程饥饿。
示例代码
java
class Philosopher extends Thread {
private final Chopstick left;
private final Chopstick right;
public Philosopher(String name, Chopstick left, Chopstick right) {
super(name);
this.left = left;
this.right = right;
}
@Override
public void run() {
while (true) {
// 哲学家思考
think();
// 按顺序获取筷子(可能导致线程饥饿)
synchronized (left) {
synchronized (right) {
eat();
}
}
}
}
private void think() {
System.out.println(Thread.currentThread().getName() + " is thinking...");
try {
Thread.sleep(100); // 模拟思考时间
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
private void eat() {
System.out.println(Thread.currentThread().getName() + " is eating...");
try {
Thread.sleep(100); // 模拟吃饭时间
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
public class TestStarvation {
public static void main(String[] args) {
Chopstick c1 = new Chopstick("1");
Chopstick c2 = new Chopstick("2");
Chopstick c3 = new Chopstick("3");
Chopstick c4 = new Chopstick("4");
Chopstick c5 = new Chopstick("5");
// 哲学家按照顺时针方向获取筷子
new Philosopher("苏格拉底", c1, c2).start();
new Philosopher("柏拉图", c2, c3).start();
new Philosopher("亚里士多德", c3, c4).start();
new Philosopher("赫拉克利特", c4, c5).start();
new Philosopher("阿基米德", c1, c5).start(); // (然后就阿基米德拿不到筷子,基本都是赫拉克利特和亚里士多德)
}
}