zl程序教程

您现在的位置是:首页 >  其它

当前栏目

volatile 关键字

关键字 volatile
2023-09-27 14:28:31 时间

请添加图片描述

✨个人主页:bit me👇
✨当前专栏:Java EE初阶👇
✨每日一语:人是怎么颓废的?假装试的努力,持久性的幻想,语言上的巨人,行动上的矮子。

⏰一. volatile

写一串代码:

  • 创建两个线程 t1 和 t2
  • t1 中包含一个循环, 这个循环以 flag == 0 为循环条件.
  • t2 中从键盘读入一个整数, 并把这个整数赋值给 flag.
  • 预期当用户输入非 0 的值的时候, t1 线程结束
public class Demo16 {
    //写一个内部类,此时这个内部类就能处在 Demo16 的内部,和 Demo14 中 Counter 类不是一个作用域
    static class Counter{
        public int flag = 0;
    }
    public static void main(String[] args) {
        Counter counter = new Counter();

        Thread t1 = new Thread(()->{
           while (counter.flag == 0){
               //执行循环,但是此处循环啥都不做
           }
            System.out.println("t1 结束");
        });
        t1.start();

        Thread t2 = new Thread(()->{
           //让用户输入一个数字,赋值给 flag
            Scanner scanner = new Scanner(System.in);
            System.out.println("请输入一个整数:");
            counter.flag = scanner.nextInt();
        });
        t2.start();
    }
}

运行结果:

在这里插入图片描述

当我们输入了一个 1 之后,t1 线程并没有结束!!这里就涉及到了我们的内存可见性问题(内存改了,线程没看见或者说线程没能及时读取到更新后的数据)

t1 做的工作:LOAD 读内存的数据到 CPU 寄存器,TEST:检查 CPU 寄存器的值是否和预期的一样,这两个动作反复的进行,频繁的进行,读内存比读 CPU 寄存器慢几千上万倍,意味着当前的 t1 主要操作就是慢在 LOAD 上,编译器每次 TEST 的时候都看着 LOAD 的结果都没啥变化,就直接进行了优化,那就是相当于只从内存中读取一次数据然后在寄存器里进行反复 TEST。

上述操作,编译器里的优化我们不好干预,于是对上述场景编译器可能知道自己出现误判,给我们提供了一个关键字 volatile。

volatile 关键字是要写到要修改的变量上的

volatile public int flag = 0;

volatile 操作相当于显式的禁止了编译器进行上述优化,是给这个对应的变量加上了 "内存屏障" (特殊的二进制指令),JVM 在读取这个变量的时候,因为内存屏障的存在,就知道要每次都重新读取这个内存的内容,而不是频繁进行草率的优化。(频繁读取内存,速度是慢了,但是数据算对了)

  • 去除 volatile,改变循环里的条件,看看效果
while (counter.flag == 0){
    try {
        Thread.sleep(100);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

运行结果:

在这里插入图片描述

发现程序又没有在此处做优化,编译器的优化是根据代码实际情况来的,上个代码是循环为空所以转速极快,导致读取内存次数频繁,触发了优化,此处加了 sleep ,转速慢下来了,读取次数减少,没有触发优化了,由于不知道编译器何时该优化何时不该优化,所以必要的时候还是得加上 volatile。

在这里插入图片描述

代码在写入 volatile 修饰的变量的时候:

  • 改变线程工作内存中volatile变量副本的值
  • 将改变后的副本的值从工作内存刷新到主内存

代码在读取 volatile 修饰的变量的时候:

  • 主内存中读取volatile变量的最新值到线程的工作内存
  • 工作内存中读取volatile变量的副本

线程优化之后,主要在操作工作内存,没有及时读取主内存,导致出现误判

工作内存:不是真正的内存,其实就是 CPU 寄存器(可能还是加上 CPU 缓存)。

主内存:真正的内存。

上述过程中,Java单独起了个名字 :JMM(Java Memory Model)

volatile 和 synchronized 有着本质的区别. synchronized 能够保证原子性, volatile 保证的是内存可见性.

  • volatile 解决的是一个线程读,一个线程写的问题(禁止指令重排序)
  • synchronized 解决的是两个线程写的问题