并发编程三大特性
原子性
原子性要求一段代码中的一个或者多个操作必须作为一个不可分割的整体执行。 具有下面两种性质:
- 不可分割性: 这些操作在执行过程中不能穿插别的线程的操作。
- 完整性: 要么所有步骤都完成,要么全部不执行,中间出错了的话,就全部不执行,不存在“部分完成”的状态。
下面举两个例子:
数据库事务: 小明向小红转账100元,从小明账户扣除100元和向小红账户增加100元,这两个操作必须都执行完成并且成功,如果中途失败,就必须回滚,回归到没有转账的初始状态,不存在小明账户扣除了100元,但是小红账户没有增加的现象。
多线程操作共享数据: i 的初始值为0,线程 A 执行 i++ 代码,线程 B 执行 i-- 代码。首先来拆分一下 i++这条代码,它可以拆分成下面这几个操作。
- 读取: 通过总线事务从主存中将 i 的初始值读入寄存器中等待计算。
- 计算: 利用计算单元计算 i + 1 的值并写回到寄存器中等待写回主存。
- 写入: 等待合适的时机通过总线事务将修改后的i的值写回到主存。
考虑以下场景:线程A和线程B都将i=0的初始值读到了寄存器中,然后一个执行了+1操作,一个执行了 -1 操作,并且线程A先写回到了主存,线程B再写回到主存,那么最终结果是 -1,不是想要的解雇了。
可见性
可见性是指一个线程对共享数据的操作可以立刻被另外一个线程看见,另外一个线程获取到的共享数据永远是最新值。
看下面的代码:
class VisibilityProblem {
private static boolean running = true; // 线程可能无法看到这个值的变化
public static void main(String[] args) throws InterruptedException {
Thread worker = new Thread(() -> {
while (running) {
// 执行一些任务
}
System.out.println("Worker thread stopped.");
});
worker.start();
Thread.sleep(1000); // 主线程等待一秒
running = false; // 主线程修改变量的值
System.out.println("Main thread set running to false.");
}
}
按照正常的思考逻辑,我们在主线程将running修改成了false,按理说worker线程中的while循环应该停止才对,但是结果是while循环并没有停止,这就是一个因为Java可见性而发生的错误。
可能一:
首先,我们要知道.java文件的代码在运行时会经过javac编译器先编译成.class字节码文件,然后会放到JVM虚拟机上运行,放到虚拟机上运行的字节码文件有两种执行方式,一种是通过解释器解释执行,还有一种就是通过jit编译器进行运行时优化,会将原来的循环代码优化成下面这段代码:
if (running) {
while (true) {
// running 永远为 true
}
}
这样 worker 线程可能会被无限循环,因为 JIT 认为 running 永远不会变。
可能二:
worker 线程在第一次读取 running 时,会把它加载到 CPU 缓存(L1/L2) 或 工作内存。之后的循环中,worker 线程不会主动去主存读取 running,而是一直使用 CPU 缓存中的值。当 main 线程修改 running = false 时,main 线程将 false 写入主存。但是 worker 线程的 CPU 缓存不会自动刷新,导致它继续读取旧值 true,从而进入死循环。
上面的这两种可能都引发了可见性导致的程序错误。
有序性
在 Java 内存模型(JMM)中,“有序性”指的是 程序执行的顺序是否与源代码一致。
由于 CPU 指令重排序 和 编译器优化,代码的实际执行顺序 可能和你写的顺序不同。
- CPU指令重排序
CPU 会根据指令流水线、数据依赖性等优化代码执行顺序。例如:
int a = 10;
int b = 20;
int c = a + b;
可能会被 CPU 重新安排成:
int b = 20;
int a = 10;
int c = a + b;
- 编译器优化
JIT(即时编译器)可能会重排序代码,以便更好地利用 CPU 资源。例如:
int x = 1;
int y = 2;
doSomething();
int z = x + y;
JIT 可能会发现 z = x + y 与 doSomething() 无关,所以会重排成:
int x = 1;
int y = 2;
int z = x + y; // 提前计算 z
doSomething();
这些就不满足有序性。
Volatile
在 Java 多线程环境中,volatile 主要提供两个保证:
- 可见性(Visibility):
线程读取 volatile 变量时,始终从主存中读取最新值,不会从 CPU 缓存中读取过期数据。
线程写入 volatile 变量时,会立即刷新到主存,并通知其他线程变量值已更改。 - 禁止指令重排序(Ordering):
volatile 确保变量的读写操作不会被编译器或 CPU 重排序,防止代码执行顺序错乱。