目录
volatile 两大特性
可见性
有序性
总结
什么是内存屏障
四个 CPU 指令
四大屏障
重排
重排的类型
为什么会有重排?
线程中的重排和可见性问题
如何防止重排引发的问题?
总结
happens-before 和 volatile 变量规则
内存屏障指令 写操作
volatile 写操作前有一个 storestore 屏障
volatile 写操作后有一个 storeload 屏障
内存屏障指令 读操作
volatile 读操作后有一个loadload屏障和一个 loadstore 屏障
volatile 两大特性
可见性
在多线程环境下,每个线程可能会维护自己的本地缓存(例如 CPU 缓存或者线程私有的缓存),因此一个线程对 volatile
变量的修改对其他线程是立即可见的。
有序性
在多线程环境下,每个线程可能会维护自己的本地缓存(例如 CPU 缓存或者线程私有的缓存),因此一个线程对 volatile
变量的修改对其他线程是立即可见的。
- 当一个线程修改了
volatile
变量的值,其他线程可以看到这个修改,因为volatile
变量的更新会直接刷新到主内存中(而不是线程本地的缓存)。 - 缓存一致性:Java 内存模型 (JMM) 保证了写入
volatile
变量会直接更新主内存,并且读取时会从主内存中获取数据,从而确保了其他线程能够及时看到变量的最新值。
总结
当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值立即刷新回主内存中。
当读一个volatile变量时,JMM会把该线程对应的本地内存设置为无效,直接从主内存中读取共享变量
所以volatile的写内存语义是直接刷新到主内存中,读的内存语义是直接从主内存中读取。
什么是内存屏障
Java 操作内存的后门
四个 CPU 指令
加载 和 存储
loadload()
storestore()
loadstore()
storeload()
四大屏障
屏障类型是 loadload
屏障类型是 storestore
屏障类型是 loadstore
屏障类型是 loadstore
重排
在多线程编程中,重排(Reordering)是指编译器、CPU 或 JVM 在执行程序时,出于性能优化的目的,改变了代码执行的顺序,但不改变程序的最终行为(如果没有数据依赖关系的话)。这通常是在指令级别或者内存访问顺序上发生的。
重排的类型
- 指令重排(Instruction Reordering):
-
- 编译器可能会改变语句的执行顺序,但保证语义不变。
- 例如,在循环中,如果没有数据依赖关系,编译器可以将某些计算移到循环外部执行。
- 内存重排(Memory Reordering):
-
- CPU 或缓存系统可能会改变内存操作的顺序,从而影响多个线程对共享变量的访问顺序。
- 这可能会导致不同步的线程看到的数据状态不一致,尤其是在没有适当同步的情况下。
为什么会有重排?
重排的主要目的是提高程序的执行效率。例如,编译器或处理器可能将某些不依赖的操作交换顺序,从而减少等待时间或提高并行性。然而,如果不小心,这也可能导致并发程序中的可见性问题,即线程看到的数据不一致,或者多线程的执行顺序与我们期望的不一致。
线程中的重排和可见性问题
在多线程程序中,重排的危险通常与线程之间的内存可见性和执行顺序有关。举个简单的例子:
boolean flag = false;
Thread t1 = new Thread(() -> {
flag = true; // A
});
Thread t2 = new Thread(() -> {
if (flag) { // B
System.out.println("Flag is true");
}
});
t1.start();
t2.start();
在这个例子中,如果没有适当的同步机制,t2
可能会比 t1
先执行,即 t2
可能在 flag
变量还没有被 t1
更新时就执行了。如果处理器或编译器对内存访问进行了重排,t2
可能看到的是 flag
为 false
,即使 t1
已经把它设置为 true
。这是因为在不同线程间的内存访问可能没有同步,导致重排影响了线程间的可见性。
如何防止重排引发的问题?
- 使用
volatile
关键字:
-
volatile
关键字能够防止对变量的重排序,也确保每个线程都能看到变量的最新值。- 它告诉 JVM 和 CPU,所有的线程都应该直接从主内存中读取该变量的值,而不是从缓存中读取。
java
复制编辑
private volatile boolean flag = false;
- 使用同步(
synchronized
或Lock
):
-
synchronized
能够保证同一时刻只有一个线程执行某个代码块,并且它还可以保证内存的可见性,即写入的值对其他线程可见。- 通过使用
synchronized
或显式的Lock
,可以避免并发环境中的重排序和可见性问题。
- 使用
final
关键字:
-
- 对于一些常量(即初始化后不再修改的变量),使用
final
可以避免变量被重排,确保多线程读取时不会遇到问题。
- 对于一些常量(即初始化后不再修改的变量),使用
总结
重排是为了优化性能而发生的一种行为,但在多线程环境中,若没有正确的同步机制,重排可能导致程序行为不一致,尤其是在共享数据的访问上。为了防止这类问题,我们通常会使用 volatile
、synchronized
或其他同步机制来保证多线程程序的正确性。
如果你有更多具体的多线程问题,或者有其他相关概念不太明白,随时可以继续问我!
happens-before 和 volatile 变量规则
只要是第一个操作是 voliatile 读 都不能重排
只要是第二个操作是 voliatile 写 都不能重排
voliatile 读写 后 voliatile 读写 都不难重排