并发场景下的数据可见性问题

Demo:线程1中flag如果为true就循环打印“线程1执行中”的语句,线程2启动后会将flag置为false

  1. public class VolatileTest {
  2. private static boolean flag = true;
  3. public static void main(String[] args) {
  4. new Thread(() -> {
  5. System.out.println("线程1启动 flag:" + flag);
  6. while (flag) {
  7. System.out.println("线程1执行中 flag:" + flag);
  8. }
  9. System.out.println("线程1执行完成 flag:" + flag);
  10. }).start();
  11. new Thread(() -> {
  12. System.out.println("线程2 启动 flag:" + flag);
  13. flag = false;
  14. System.out.println("线程2 修改 flag:" + flag);
  15. }).start();
  16. }
  17. }

运行结果

线程1启动 flag:true 线程1执行中 flag:true 线程1执行中 flag:true 线程1执行中 flag:true 线程1执行中 flag:true 线程1执行中 flag:true

线程1执行中 flag:true

线程2 启动 flag:true

线程2 修改 flag:false

线程1执行中 flag:true

线程1执行完成 flag:false

从运行结果中看出当线程2将flag设置为false之后,线程1中的flag此时依旧为true
此时我们只需要用volatile关键字修饰一下flag变量即可

  1. private volatile static boolean flag = true;

Java内存模型

截屏2020-12-20 上午10.19.04.png

截屏2020-12-19 下午11.22.34.png

每个线程都会有一块自己的独立内存,在线程工作的时候是先将数据从主内存中读取到自己的工作内存中,所以没有及时读取到最新的flag值。上述例子中,虽然线程2已经把flag的值设置为了false,但是由于线程1读的还是自己本地的值,所以为true。想要解决上述问题可以在修饰flag变量时加个volatile关键字

volatile语义

可见性

对被volatile关键字修饰的变量修改时,立即对其他线程可见。例如flag = true,当线程2把它设置为false后,线程1马上就可以读取到最新的flag=false。被volatile修饰的变量,在值被修改后,马上会执行store和write操作,并且过期掉其他所有缓存中的值,让数据重新从主内存load

禁止指令重排

禁止编译器将自认为可以进行重排序的指令进行重排

CPU缓存

截屏2020-12-19 下午11.28.08.png
由于从内存读取数据的速度比从cpu缓存中读取的速度慢很多,所以cpu在工作时,会先将数据从内存中读取到自己的缓存中再进行运算。但是由于缓存的数据所以可能会有数据一致性问题。但是java内存模型在硬件模型层面并不完全等价于cpu的缓存机制。cpu是靠MESI协议来保证缓存数据的一致性的

MESI协议

E(Exclusive)

数据有效,数据和内存中的数据一致,数据值存在于本CPU的cache中
截屏2020-12-20 上午12.13.30.png

S(Shared)

数据有效,数据和内存中的数据一致,数据存在于很多cache中
截屏2020-12-20 上午12.14.54.png

M(Modified)

数据有效,数据被修改了,和内存中的数据不一致,数据值存在于cpu的cache中

截屏2020-12-20 上午12.17.23.png

I(Invalid)

无效状态

截屏2020-12-20 上午12.17.23.png