目录
一、内存可见性
1.1 Java内存模型(JMM)
1.2 内存可见性演示
二、指令重排序
三、关键字 volatile
一、内存可见性
1.1 Java内存模型(JMM)
1)什么是Java内存模型(JMM)? |
Java内存模型即Java Memory Model,简称JMM。用于屏蔽各种硬件和操作系统的内存访问差异,以实现让Java程序在各平台下都能够达到一致的内存访问效果,即实现“跨平台”。 |
2)JMM中的“主内存”概念和“工作内存”概念 | |
“主内存” | 硬件中的内存,在JMM中表述为“主内存”,其中存储了线程间的共享变量等数据。 |
“工作内存” | CPU寄存器和缓存等临时存储区,在JMM中表述为“工作内存”,每个线程都有自己的“工作内存”。 |
3)线程和“主内存”、“工作内存”的关系 |
当线程需要读取共享变量时,会从“主内存”拷贝至“工作内存”,再从“工作内存”读取。 当线程需要修改共享变量时,会先修改“工作内存”中的数据副本,再将数据同步回“主内存”。 线程运行中,数据的交互是频繁且持续的,而CPU访问自身寄存器和高速缓存的速度远高于访问内存的速度。 因此,采用频繁与“工作内存”交互、需要时再与“主内存”交互的工作策略,有利于提高运行效率,是编译器优化的方式之一。 |
1.2 内存可见性演示
什么是内存可见性? |
内存可见性是指,线程对共享变量值的修改,能否被其他线程及时察觉。 如果一个线程修改了共享变量值,但没有及时写回内存中,导致其他线程无法获得已修改的正确数据,这就被认为出现了线程安全问题。 内存可见性是导致线程不安全的原因之一。 |
代码演示内存不可见导致线程不安全:
public class Volatile_Demo0 {//有一个共享变量flag,注意该变量没有被 volatile 修饰;public static int flag = 0;public static void main(String[] args) throws InterruptedException {//创建一个线程,线程中当flag为0时,一直循环判断;Thread t = new Thread(()->{while (flag == 0){}});//启动线程;System.out.println("run开始");t.start();//让main线程休眠两秒后,将flag的值改为1;Thread.sleep(2000);flag = 1;//让main线程等待t线程结束;t.join();System.out.println("run结束");}
}//运行结果:
run开始
...程序一直在执行,没有打印“run结束”。
出现了线程安全问题。
上述代码问题分析:
程序无法结束的原因是什么? |
根据代码,flag 在线程启动两秒后被改为 1 ,此时 t 线程应该因为跳出 while 循环而执行完毕。 但实际情况却不是这样,t 线程没有结束。 正如上文“线程和‘主内存’、‘工作内存’的关系”中提到的,线程读取共享数据到“工作内存中”,再从“工作内存”读取数据。 所以此时在 t 线程中,参与 while 循环条件判断的 flag ,实际上是一个存储在“工作内存”的 flag 副本。 当 flag 通过另一线程改变值,改变的是“主内存”中的 flag,t 线程并不能察觉。 因此 t 线程无法从 while 循环中跳出并结束。 这就是内存可见性影响线程安全的情况之一。 |
二、指令重排序
1)什么是指令重排序? |
指令重排序是指编译器自动调整原有代码的执行顺序,在保证逻辑不变的前提下,提高程序运行效率。 指令重排序也是编译器优化的方式之一。 |
2)指令重排序存在什么问题? |
指令重排序的前提是“保证逻辑不变”。这一点在单线程环境下较容易判断,但是在多线程环境下,代码复杂程度高,编译器在编译阶段对代码执行效果的判断存在困难。 因此在多线程环境下,代码重排序很容易导致优化后和优化前的逻辑不等价。 |
图示演示指令重排序可能出现的问题:
三、关键字 volatile
1)volatile 的作用是什么? | |
<1> | 保证内存可见性。volatile 修饰的变量每次被访问都必须从内存中读取,每次被修改都必须存储到内存中。 |
<2> | 禁止指令重排序。volatile 修饰的变量读写操作的相关指令不允许被重排序。 |
2)内存可见性和指令重排序都是编译器优化,怎么好像都是负作用? |
在大部分场景下,编译器优化都能非常优秀的提高程序的运行效率,只是在多线程编程的部分关键代码中,存在线程不安全的风险。 |
3)volatile 不保证原子性 |
volatile 和 synchronized 有本质的区别,synchronized 保证原子性,而 volatile 保证的是内存可见性。 |
4)合理的使用 volatile 关键字 |
编译器优化就好像一场激烈的风暴,而程序员要做的就是掌控这场风暴,必要时让风暴停一停。 为此,Java 提供了 volatile 关键字供程序员使用。当使用 volatile 关键字时,强制读写内存,禁止指令重排序,程序运行速度变慢,但数据准确性提高,线程变得安全了。 |
代码演示 volatile 的使用效果,沿用上文“内存可见性演示”中的代码:
public class Volatile_Demo0 {//有一个共享变量flag,注意该变量已经被 volatile 修饰;public volatile static int flag = 0;public static void main(String[] args) throws InterruptedException {//创建一个线程,线程中当flag为0时,一直循环判断;Thread t = new Thread(()->{while (flag == 0){}});//启动线程;System.out.println("run开始");t.start();//让main线程休眠两秒后,将flag的值改为1;Thread.sleep(2000);flag = 1;//让main线程等待t线程结束;t.join();System.out.println("run结束");}
}//运行结果:
run开始
run结束与上文“内存可见性演示”中的代码唯一的不同,就是在共享变量 flag 上,使用了 volatile 进行修饰。
但这次的结果是程序正常执行完毕,证明了 volatile 的作用。