面试官:怎么解决并发变量下变量不可见性问题?

问题:子线程修改了某个成员变量的值,但是在主线程中读取到的还是之前的值修改后的值无法读取到。

引出问题(代码示例)

public class VolatileThread extends Thread {

    // 定义成员变量
    private boolean flag = false ;
    public boolean isFlag() { return flag;}

    @Override
    public void run() {

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 将flag的值更改为true
        this.flag = true ;
        System.out.println("flag=" + flag);

    }
}

public class volatileTest {// 测试类
    
    public static void main(String[] args) {

        // 创建VolatileThread线程对象
        VolatileThread volatileThread = new VolatileThread() ;
        volatileThread.start();

        // main方法
        while(true) {
        	// 这里读取到了flag值一直是false,虽然线程已经把它的值改成了true。
            if(volatileThread.isFlag()) {
                System.out.println("执行了======main方法");
            }
        }
    }
}

结果:
在这里插入图片描述

  • 可以看到,VolatileThread线程中已经将flag设置为true,但main()主方法中始终没有读到,从而没有打印。

Java内存模型(JMM)

  • 概述:JMM(Java Memory Model)Java内存模型,是java虚拟机规范中所定义的一种内存模型。
  • Java内存模型(Java Memory Model)描述了Java程序中各种变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存和从内存中读取变量这样的底层细节。
  • 所有的共享变量都存储于主内存。这里所说的变量指的是实例变量和类变量。不包含局部变量,因为局部变量是线程私有的,因此不存在竞争问题。每一个线程还存在自己的工作内存,线程的工作内存,保留了被线程使用的变量的工作副本。线程对变量的所有的操作(读,取)都必须在工作内存中完成,而不能直接读写主内存中的变量,不同线程之间也不能直接访问对方工作内存中的变量,线程间变量的值的传递需要通过主内存完成。
    在这里插入图片描述
  • Java语言本身对 原子性可见性以及有序性提供了哪些保证呢?
    • 原子性:在Java中,对基本数据类型的变量的读取和赋值操作是原子性操作,即这些操作是不可被中断的,要么执行,要么不执行。
    • 可见性当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。对于可见性,Java提供了volatile关键字来保证可见性。另外,通过synchronized和Lock也能够保证可见性,synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。
    • 有序性:在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。

问题分析

在这里插入图片描述

  1. VolatileThread线程从主内存读取到数据放入其对应的工作内存
  2. 将flag的值更改为true,但是这个时候flag的值还没有写会主内存
  3. 此时main方法读取到了flag的值为false
  4. 当VolatileThread线程将flag的值写回去后,但是main函数里面的while(true)调用的是系统比较底层的代码,速度快,快到没有时间再去读取主存中的值,
  • 所以while(true)读取到的值一直是false。(如果有一个时刻main线程从主内存中读取到了主内存中flag的最新值,那么if语句就可以执行,main线程何时从主内存中读取最新的值,我们无法控制)

问题处理

方案一:加锁

   // main方法
   while(true) {
       synchronized (volatileThread) {
           if(volatileThread.isFlag()) {
               System.out.println("执行了======main方法");
           }
       }
   }
  • 某一个线程进入synchronized代码块前后,执行过程入如下:
    • a.线程获得锁
    • b.清空工作内存
    • c.从主内存拷贝共享变量最新的值到工作内存成为副本
    • d.执行代码
    • e.将修改后的副本的值刷新回主内存中
    • f.线程释放锁

方案二:volatile关键字

  • 使用volatile关键字:
   private volatile boolean flag ;

volatile关键字的两层语义

  • 一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:
    • 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
    • 禁止进行指令重排序。
  • 工作原理:
    在这里插入图片描述
  1. VolatileThread线程从主内存读取到数据放入其对应的工作内存

  2. 将flag的值更改为true,但是这个时候flag的值还没有写会主内存

  3. 此时main方法main方法读取到了flag的值为false

  4. 当VolatileThread线程将flag的值写回去后,失效其他线程对此变量副本

  5. 再次对flag进行操作的时候线程会从主内存读取最新的值,放入到工作内存中

volatile的原理和实现机制

  • volatile到底如何保证可见性和禁止指令重排序的?

    • 观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令

    • 没添加volatile关键字

    • 在这里插入图片描述

    • 添加volatile关键字在这里插入图片描述

    • lock前缀指令实际上相当于一个内存屏障(也称内存栅栏),内存屏障会提供3个功能

      • 它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
      • 它会强制将对缓存的修改操作立即写入主存;
      • 如果是写操作,它会导致其他CPU中对应的缓存行无效。
  • 总结: volatile保证不同线程对共享变量操作的可见性,也就是说一个线程修改了volatile修饰的变量,当修改写回主内存时,另外一个线程立即看到最新的值。但是volatile不保证原子性

volatile与synchronized区别

  • volatile只能修饰实例变量和类变量,而synchronized可以修饰方法,以及代码块。
  • volatile保证数据的可见性,但是不保证原子性(多线程进行写操作,不保证线程安全);而synchronized是一种排他(互斥)的机制

你知道的越多,你不知道的越多。
有道无术,术尚可求,有术无道,止于术。
如有其它问题,欢迎大家留言,我们一起讨论,一起学习,一起进步

发布了217 篇原创文章 · 获赞 268 · 访问量 4万+

猜你喜欢

转载自blog.csdn.net/qq_40722827/article/details/105329141