跳到主要内容Java 线程安全核心:synchronized、volatile 与 wait/notify | 极客日志Javajava
Java 线程安全核心:synchronized、volatile 与 wait/notify
探讨 Java 多线程环境下的线程安全问题,分析导致不安全的三大原因:原子性、内存可见性与指令重排序。重点讲解 synchronized 关键字如何实现原子性、可见性及互斥锁,volatile 如何保证可见性与禁止重排序,以及 wait/notify 机制在线程通信中的应用。同时涵盖死锁产生的条件与预防策略,并通过代码示例演示各机制的实际效果。
CoderByte21 浏览 1.线程安全
1.1 概念&示例
概念:指在多线程环境下,某个代码、函数或对象能够被多个线程同时调用或访问时,仍能保持正确的行为和数据一致性。简单来说,线程安全的代码在多线程环境下运行可靠,不会因线程间的交互而产生不可预测的结果
示例:
public class ThreadDemo {
public static int count = 0;
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 500000; i++) {
count++;
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 500000; i++) {
count++;
}
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("count = " + count);
}
}
按照上述代码的逻辑,期望得到的结果是 1000000,但实际计算的结果与期望值不一致。线程不安全:当多个线程同时访问或修改时,由于缺乏适当的,可能导致程序行为不可预测、数据损坏或错误结果的现象
共享资源
同步机制
1.2 线程不安全的原因
1.访问修改共享变量:当多个线程同时读写同一内存区域时,可能导致数据状态不一致
5.线程之间抢占式执行:这是操作系统层面的调度机制,线程的执行顺序是随机的和不可预测的。操作系统可能随时中断一个线程 (抢占),切换到另一个线程执行。一般不轻易改变,当引发线程安全时优先考虑前 4 个原因
共享变量访问修改是线程安全问题的前提,但需结合 2/3/4 才会引发问题;抢占式执行是线程调度的特性,无法避免
4.指令重排序:是计算机处理器或编译器为了提高程序执行效率,对指令执行顺序进行优化的一种技术。在保证程序最终结果正确的前提下,允许指令的执行顺序与代码编写的顺序不一致。但可能导致多线程下的逻辑错误
3.内存可见性:在多线程编程中,每个线程都有自己的工作内存 (本地内存),用于存储共享变量的副本。由于 CPU 缓存、编译器优化等因素,操作可能只发生在工作内存中,而不是直接在主内存中进行,导致程序行为不符合预期
2.原子性:原子性指一个操作是不可分割的单元,要么完全执行,要么完全不执行。如果操作不是原子的,在并发环境下,线程可能被中断在中间状态,导致部分修改
2.synchronized 关键字
2.1 概念
synchronized(监视器锁 monitor lock):用于实现线程同步,确保多线程环境下对共享资源的访问安全。通过加锁机制,防止多个线程同时访问同步块代码或对象,避免数据不一致问题
2.2 特性
2.2.1 原子性
确保了代码块的原子性,即被同步的代码块在执行过程中不会被其他线程中断。这意味着在一个线程执行完整个同步块之前,其他线程无法进入同一个同步块,从而保证了操作的完整性
public class ThreadDemo {
private static final Object locker = new Object();
private static int count = 0;
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 500000; i++) {
synchronized (locker) {
count++;
}
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 500000; i++) {
synchronized (locker) {
count++;
}
}
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("count = " + count);
}
}
2.2.2 内存可见性
获取锁时:线程会将工作内存中的变量副本失效,强制从主内存重新读取最新值
释放锁时:线程会将工作内存中修改过的变量刷新到主内存
2.2.3 互斥性
确保在同一时间只有一个线程可以进入被同步的代码块或方法,这意味着当一个线程进入同步块或方法时,其他试图进入同一同步块的线程会被阻塞,直到第一个线程退出同步块
2.2.4 可重入性
synchronized 关键字是可重入的,这意味着如果一个线程已经持有某个对象的锁,那么它可以再次获取该对象的锁,而不会被阻塞。可重入锁通常会维护一个计数器,记录当前线程获取锁的次数。每次获取锁时,计数器加一;释放锁时,计数器减一。当计数器为零时,锁才真正被释放
public class Reentry_Lock {
public static void main(String[] args) {
Object locker = new Object();
Thread thread = new Thread(() -> {
synchronized (locker) {
System.out.println("第一层锁");
synchronized (locker) {
System.out.println("第二层锁");
}
}
});
thread.start();
}
}
2.3 类型
2.3.1 实例锁
作用于对象实例,每个对象实例拥有自己的锁。当一个线程访问对象的 synchronized 实例方法或代码块时,其他线程无法访问该对象的其他 synchronized 方法或代码块,但可以访问非 synchronized 方法或代码块
public class Example {
public synchronized void instanceMethod() {
}
public void anotherMethod() {
synchronized (this) {
}
}
}
2.3.2 静态锁
作用于类的 Class 对象,所有实例共享同一把锁。当一个线程访问 synchronized 静态方法或代码块时,其他线程无法访问该类的其他 synchronized 静态方法或代码块,但可以访问非 synchronized 静态方法或代码块
public class Example {
public static synchronized void staticMethod() {
}
public static void anotherStaticMethod() {
synchronized (Example.class) {
}
}
}
2.4 死锁
概念:指两个或多个线程在执行过程中,因争夺资源而造成的一种互相等待的现象,导致这些线程都无法继续执行下去。这种情况下,系统资源被占用,但程序无法继续运行
1.互斥条件 (Mutual Exclusion):至少有一个资源必须处于非共享模式,即一次只能被一个线程使用。如果另一个线程请求该资源,那么请求线程必须等待,直到该资源被释放
2.请求与保持条件 (Hold and Wait):一个线程已经持有至少一个资源,并且正在等待获取其他被其他线程占用的资源
3.不可剥夺条件 (No Preemption):资源一旦被分配给某个线程,就不能被强制性地剥夺,只能由占有该资源的线程自行释放
4.环路等待条件 (Circular Wait):存在一个线程资源的循环等待链,其中每个线程都在等待下一个进程所持有的资源
预防死锁:通过破坏死锁的四个必要条件之一,可以预防死锁的发生
2.破坏占有且等待:一次性申请所有需要的资源,避免部分持有
3.破坏非抢占条件 (不建议):允许系统强行剥夺某些进程已占有的资源,分配给其他进程。这种方法可能导致进程执行的不稳定性
4.破坏循环等待条件:对资源进行排序,按固定顺序申请资源
3.volatile
3.1 概念
volatile:是编程语言中的关键字,用于修饰变量,告知编译器该变量可能被意外修改。其核心作用是防止编译器优化导致的数据不一致问题(在 Java 中仅能修饰成员变量)
3.2 特性
3.2.1 内存可见性
对 volatile 变量的每次访问都会强制从主内存读取,每次修改都会立即写回主内存
public class demo_volatile {
public static volatile int num = 0;
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
while (num == 0) {}
System.out.println("Over thread1");
});
Thread thread2 = new Thread(() -> {
System.out.println("请输入一个整数");
Scanner in = new Scanner(System.in);
num = in.nextInt();
});
thread1.start();
thread2.start();
}
}
3.2.2 禁止指令重排序
在多线程场景下,指令重排序可能会导致线程间数据同步问题。volatile 变量通过插入内存屏障 (Memory Barrier) 来禁止重排序
写操作前插入'StoreStore'屏障,写操作后插入'StoreLoad'屏障
读操作前插入'LoadLoad'屏障,读操作后插入'LoadStore'屏障
public class FixedReorderingExample {
int a = 0;
int b = 0;
volatile boolean flag = false;
public void writer() {
a = 1;
b = 1;
flag = true;
}
public void reader() {
if (flag) {
int r1 = a;
int r2 = b;
System.out.println("r1: " + r1 + ", r2: " + r2);
}
}
}
3.2.3 不保证原子性
volatile 不保证操作的原子性,多线程环境下仍需结合锁或原子操作
4.wait/notify
概念:wait() 和 notify() 是 Java 中用于线程间通信的机制,属于 Object 类的方法。它们必须在同步代码块 (如 synchronized 块) 中使用,否则会抛出IllegalMonitorStateException
wait(): 让当前线程进入等待状态,释放锁,直到其他线程调用 notify() 或 notifyAll() 唤醒它
notify(): 随机唤醒一个等待该对象锁的线程
notifyAll(): 唤醒所有等待该对象锁的线程
public class Demo {
public static void main(String[] args) throws InterruptedException {
Object locker = new Object();
Thread thread1 = new Thread(() -> {
synchronized (locker) {
System.out.println("thread1 线程 wait 之前");
try {
locker.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("thread1 线程 wait 之后");
}
});
Thread thread2 = new Thread(() -> {
synchronized (locker) {
System.out.println("thread2 线程 notify 之前");
locker.notify();
System.out.println("thread2 线程 notify 之后");
}
});
thread1.start();
Thread.sleep(1000);
thread2.start();
}
}
概念:wait 是 Object 类的方法,用于线程间的通信,必须配合 synchronized 使用,调用 wait 的线程会释放锁。sleep 是 Thread 类的静态方法,用于暂停当前线程的执行,调用 sleep 的线程不会释放锁
锁的释放行为差异:wait 会释放当前线程持有的锁,允许其他线程获取该锁并执行同步代码块,这一特性使得 wait 适用于多线程协作的场景。sleep 不会释放任何锁,即使线程休眠,其他线程也无法获取该线程持有的锁,这可能导致死锁或性能问题
唤醒机制:wait 需要通过 notify 或 notifyAll 主动唤醒,否则线程会一直等待 (可以设置最大等待时间),唤醒后线程需要重新获取锁才能继续执行。sleep 无需外部唤醒,到达指定时间后自动恢复,恢复执行的线程直接从 sleep 调用处继续执行
相关免费在线工具
- Keycode 信息
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
- Escape 与 Native 编解码
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
- JavaScript / HTML 格式化
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
- JavaScript 压缩与混淆
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online