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

Bianca ·
更新时间:2024-11-11
· 603 次阅读

文章目录引出问题(代码示例)Java内存模型(JMM)问题分析问题处理方案一:加锁方案二:volatile关键字volatile的原理和实现机制volatile与synchronized区别

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

引出问题(代码示例) 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内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。 问题分析

在这里插入图片描述

VolatileThread线程从主内存读取到数据放入其对应的工作内存 将flag的值更改为true,但是这个时候flag的值还没有写会主内存 此时main方法读取到了flag的值为false 当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修饰之后,那么就具备了两层语义: 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。 禁止进行指令重排序。 工作原理:
在这里插入图片描述

VolatileThread线程从主内存读取到数据放入其对应的工作内存

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

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

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

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

volatile的原理和实现机制

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

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

没添加volatile关键字

在这里插入图片描述

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

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

它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成; 它会强制将对缓存的修改操作立即写入主存; 如果是写操作,它会导致其他CPU中对应的缓存行无效。

总结: volatile保证不同线程对共享变量操作的可见性,也就是说一个线程修改了volatile修饰的变量,当修改写回主内存时,另外一个线程立即看到最新的值。但是volatile不保证原子性

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

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


作者:斗士(Carroll)



可见性 面试 面试官 并发 变量

需要 登录 后方可回复, 如果你还没有账号请 注册新账号