内存可见性问题

目录

1.什么是内存可见性问题

2.内存可见性问题是怎么发生的

3.解决方法:volatile

4.volatile使用的注意事项

5.内存可见性问题的延伸

缓存(cache)


1.什么是内存可见性问题

首先来看一段代码

class Counter{
    public int flag = 0;
}
public class VolatileDemo1 {
    public static void main(String[] args) {
        Counter counter = new Counter();
        Thread t1 = new Thread(() -> {
            while(counter.flag == 0) {
                //循环里面不进行任何操作
            }
            System.out.println("t1 循环结束");
        });


        Thread t2 = new Thread(() -> {
            Scanner scanner = new Scanner(System.in);
            System.out.print("请输入flag: ");
            counter.flag = scanner.nextInt();
        });
        t1.start();
        t2.start();
        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

这段代码一共创建了两个线程,其中t1线程去判断flag的值(默认为0),如果不为0则跳出循环(循环里面不执行任何操作)当flag不为0时,提示t1线程结束,t2线程则是输入一个值,赋给flag。

按照我们的逻辑,当t2线程输入一个不为0的数字时,t1线程会打印“t1 循环结束”,那么我们来看一下结果,如下:

 可以看到,我们输入1,赋值给flag,但是t1循环却没有对此做出相应的操作,这就是出现了内存可见性问题。

2.内存可见性问题是怎么发生的

首先针对上面的例子,我们做一些分析。

t1线程中有一个循环,循环条件是判断flag这个变量是否为0,循环体为空

t2线程是输入一个数字赋值给flag

按照逻辑当t2输入数字不为0,那么t1循环结束,那么为什么当t2输入了一个不为0的数字时,t1循环仍然没有结束呢?

可以肯定的是:t2中的输入和赋值操作都是没有问题的,那么问题的所在就一个在t1的身上。

那么我们对t1中的执行语句做一些分析:

t1线程中储粮打印操作,唯一可以被执行的计算循环的判断条件 counter.flag == 0 。

这条语句我们可以把它拆分成两条指令:

一条是从内存中获取flag的值--load

一条是将这个值和0进行比较--cmp

按理来说,如果每次进入循环条件判断的时候,都对flag的值进行获取,那么结果就不会出现死循环的现象,而此时出现了死循环,那么就说明对flag的获取出现了问题。

t1中的这个循环是空体,这个循环在执行时的速度极快,1秒钟可以执行上百万次,而执行了这么多次load的获取结果都是一样的。另一方面,load的执行速度相比于cmp慢了太多了。此时JVM就做出来一个非常大胆的决定--不再真正的去重复load了,因为判定好像没人去修改flag的值,所以干脆就只获取一次就好了,此时就出现了前面运行的情况了。

上述的这种情况是编译器优化的一种方式,而内存可见性问题归根结底就是编译器/JVM在多线程环境下优化时产生了误判,此时就需要我们去手动干预,让编译器不要瞎搞,而这个操作结束在变量前面加上 volatile 关键字。

3.解决方法:volatile

继续挪用上面的代码,并且给flag这个变量加上volatile

class Counter{
    volatile public int flag = 0;
}
public class VolatileDemo1 {
    public static void main(String[] args) {
        Counter counter = new Counter();
        Thread t1 = new Thread(() -> {
            while(counter.flag == 0) {
                //循环里面不进行任何操作
            }
            System.out.println("t1 循环结束");
        });


        Thread t2 = new Thread(() -> {
            Scanner scanner = new Scanner(System.in);
            System.out.print("请输入flag: ");
            counter.flag = scanner.nextInt();
        });
        t1.start();
        t2.start();
        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

此时再去运行可以看到

加了volatile之后,代码的运行情况就符合我们的预期了。

当然,代JVM并不是任何时候都会出现优化误判的情况,比如下面的代码

class Counter{
    public int flag = 0;
}
public class VolatileDemo1 {
    public static void main(String[] args) {
        Counter counter = new Counter();


        //编译器不是任何时候都会进行优化或者优化出错 如下,即使没有 volatile 也可以正常运行
        Thread t1 = new Thread(() -> {
            while(counter.flag == 0) {
                //循环里面不进行任何操作
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("t1 循环结束");
        });
        Thread t2 = new Thread(() -> {
            Scanner scanner = new Scanner(System.in);
            System.out.print("请输入flag: ");
            counter.flag = scanner.nextInt();
        });
        t1.start();
        t2.start();
        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }
}

我们在循环体中加入了sleep,此时代码中没有加 volatile 但是代码也可以正常运行,但是这开发中,对于这种不确定的情况,还是加上volatile更加稳妥。

4.volatile使用的注意事项

volatile 只可以对变量进行修饰,不可以对方法进行修饰。

volatile 不可以对方法中的局部变量进行修饰。

volatile 不保证原子性,若想保证原子性要使用 synchronized 

5.内存可见性问题的延伸

关于内存可见性问题,还可以从JMM(Java Memory Modle java内存模型)的角度去重新表述

Java程序里除了主内存,每个线程还有自己的“工作内存”

t1线程进行读取的时候只是读取了它工作内存的数据

t2线程进行修改的时候,先修改工作内存的数据,然后再把工作内存的数据同步到主内存中,但是由于编译器优化,导致t1没有重新从主内存中同步数据到它的工作内存中,所以读到的结果就是错误的结果。(主内存和工作内存这样的表述来自于Java文档)

上面的主内存既可以理解为前面说的内存;

而工作内存可以理解为工作存储区,也就是CPU上存储数据的单元(寄存器)以及缓存。

缓存(cache)

CPU中的寄存器存储的空间小,读写速度快,成本高;

内存的存储空间大,读写速度慢,成本低(相对于寄存器来说)

缓存就是他俩的中间值,缓存存储空间居中,读写速度居中,成本居中

当cpu在读取一个数据的时候,可能是直接读取内存,也可能是读取缓存,还可能是读取寄存器

前面说的工作内存,之所以将寄存器和缓存都包含进去,一方面是因为描述简单,另一方面,无论是缓存还是寄存器都不会对我们得到的结论产生影响。

  • 9
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 7
    评论
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

追梦不止~

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值