原文链接(对原文中的demo的情况进行解释,方便理解指令重排带来的问题):https://blog.csdn.net/t894690230/article/details/50588129
1. 内存可见性
Java 内存模型规定,对于多个线程共享的变量,存储在主内存当中,每个线程都有自己独立的工作内存,并且线程只能访问自己的工作内存,不可以访问其它线程的工作内存。工作内存中保存了主内存中共享变量的副本,线程要操作这些共享变量,只能通过操作工作内存中的副本来实现,操作完毕之后再同步回到主内存当中,其 JVM 模型大致如下图。
JVM 模型规定:
- 线程对共享变量的所有操作必须在自己的内存中进行,不能直接从主内存中读写;
- 不同线程之间无法直接访问其它线程工作内存中的变量,线程间变量值的传递需要通过主内存来完成。
内存可见性指:当一个线程修改了某个状态对象后,其它线程能够看到发生的状态变化。比如线程 1 修改了变量 A 的值,线程 2 能立即读取到变量 A 的最新值,否则线程 2 如果读取到的是一个过期的值,也许会带来一些意想不到的后果。那么如果要保证内存可见性,必须得保证以下两点:
线程修改后的共享变量值能够及时刷新从工作内存中刷新回主内存;其它线程能够及时的把共享变量的值从主内存中更新到自己的工作内存中;为此,Java 提供了一种稍弱的同步机制,即 volatile 变量,用来确保将变量的更新操作通知到其它线程。当把共享变量声明为 volatile 类型后,线程对该变量修改时会将该变量的值立即刷新回主内存,同时会使其它线程中缓存的该变量无效,从而其它线程在读取该值时会从主内中重新读取该值(参考缓存一致性)。因此在读取 volatile 类型的变量时总是会返回最新写入的值。
除了使用 volatile 关键字来保证内存可见性之外,使用 synchronizer 或其它加锁也能保证变量的内存可见性。只是相比而言使用 volatile 关键字开销更小,但是 volatile 并不能保证原子性
2、指令重排
JVM可以对它们在不改变数据依赖关系的情况下进行任意排序以提高程序性能。
编译器和处理器在重排序时,会遵守数据依赖性,编译器和处理器不会改变存在数据依赖关系的两个操作的执行顺序。
前提: 数据依赖性仅针对单个处理器中执行的指令序列和单个线程中执行的操作,不同处理器之间和不同线程之间的数据依赖性不会被编译器和处理器考虑
场景: 2个线程(线程A和线程B)情况下,对同一个TestVolatile对象,线程A调用method1,线程B调用method2方法,A线程可能对method1内的(1)、(2)两行指令进行重排序
情况1: 如果method1不进行指令重排序,那么执行method2的时候犹豫线程执行顺序的原因可能会出现以下3种情况:
【flag=true,testVolatile != null】
【flag=false,testVolatile != null】
【flag=false,testVolatile = null】
但是这3种情况,仅仅是因为线程执行顺序的原因造成的,这些问题再程序开发过程中就应该预料到。
情况2: 如果method1进行指令重排序(即:先执行(2),再执行(1)),那么再这种情况下执行method2与【情况1】相比较,则会还会出现:
【flag=true,testVolatile = null】
这个时候,执行(4)则会报空指针异常
代码如下:
static class TestVolatile{
TestVolatile testVolatile = null;
boolean flag = false;
public TestVolatile initContent() {
return new TestVolatile();
}
public void operation() {
System.out.println("operation");
}
public void method1() {
testVolatile = initContent(); //(1)
flag = true; //(2)
}
public void method2() {
if (flag) { //(3)
testVolatile.operation(); //(4)
}
}
}