[volatile]关键字

66 阅读7分钟

开启掘金成长之旅**!这是我参与「掘金日新计划 · 2 月更文挑战」的第 14天,点击查看活动详情**

编辑

目录

1.内存可见性问题-引入

2.volatile关键字

3.从java内存模型的角度内存可见性问题

1.内存可见性问题-引入

构造一个myCounter类,成员flag,让t1线程中的循环条件为新创建的对象flag,让t2通过输入的整型值,控制flag的值,若非0,则t1循环应该终止!

class myCounter{
    public int flag = 0;
}

public class ThreadDemo16 {
    public static void main(String[] args) {
        myCounter mycounter = new myCounter();
        Thread t1 = new Thread(()->{
           while(mycounter.flag==0){

           }
            System.out.println("t1循环执行结束");
        });
        Thread t2 = new Thread(()->{
            Scanner sc = new Scanner(System.in);
            System.out.println("请输入一个整数:");
            mycounter.flag = sc.nextInt();
        });
        t1.start();
        t2.start();
    }
}

我们先来看两个线程的状态:
​编辑

t1线程是RUNNABLE状态

​编辑

因为还未输入,t2其实还是阻塞状态

当我们输入一个非0值后:

​编辑

​编辑

此时t2线程已经执行结束,t1线程还在RUNNABLE状态!!

可以看到并没有打印"t1循环执行结束".我们预期的是t2改动了flag值,t1就应该结束循环了,但此时t1明显没有结束循环

这个问题就是"内存可见性问题"!因为这里的结果并不是我们预期的,所以也是一个Bug,这也是一个线程安全问题!

​编辑

这里使用汇编指令理解,大致分为两步操作:

1.load,把内存中的flag的值读取到寄存器中

2.cmp,把寄存器的值和0进行比较,根据比较结果,决定下一步的跳转(这两步是一个循环,执行速度极快)循环执行次数这么多,t2真正修改flag之前,load得到的结果都是一样的,另一方面,load的操作和cmp操作相比,速度则非常慢!!(CPU针对寄存器的操作,要比内存操作快很多,快34个数量级,计算机对于内存的操作,比硬盘快34个数量级)

由于load执行的速度相对于cmp比较慢,再加上反复load的结果相同,JVM就做出了一个优化,不再重复load,判定没有人修改flag的值,于是只读一次就好了

不再重复load是编译器的优化 的一种方式,但是实际上是有人修改的,因此由于编译器的判断失误导致出现了bug

内存可见性问题

一个线程针对一个变量进行读取操作,另一个线程针对这个变量进行修改,此时线程读到的值,不一定是修改过后的值!!读线程没有感受到线程的改动!!归根结底还是编译器/JVM在多线程环境下优化时产生了误判!

此时就需要程序员手动干预了

2.volatile关键字

如何手动干预编译器/JVM的优化呢?

此时,给flag变量加上volatile 关键字就可以了,这个单词意思是可变的,容易失去的.它表示这个变量时"可变的",编译器每一次都要重新读取这个变量的内存内容,任意时间这个变量的值可能改变,编译器不能对它进行优化!!!

给flag变量加上volatile 关键字

​编辑

结果

​编辑

优化虽然能让速度提升起来,但是容易引发各种各样的问题!

这个关键字只能修饰变量,

不能修饰方法里的局部变量,局部变量出了方法就没了,只能在线程里边使用,不能多线程之间同时读取/修改,天然的规避了线程安全问题!

每个线程都有自己的"栈空间",方法内部的变量在"栈"这样的内存空间上(栈就是记录方法之间的调用关系),即使是同一个方法不同的线程调用,方法内的局部变量也在不同的栈空间中,本质上还是不同的变量,那么也不会涉及到多个线程读取/修改同一个变量的情况,不会出现内存可见性问题

上面的内存可见性问题也不是始终会出现,就是可能会误判,如果加个sleep,我们看结果

​编辑

结果

​编辑

这里结果正确了,sleep控制了循环的速度,编译器错误的优化也消失了,但是我们不知道编译器什么时候会优化,在应用程序方面无法感知,最稳妥的方法还是加上volatile

3.从java内存模型的角度内存可见性问题

java程序里,内存,每个线程还有自己的"CPU和寄存器"都是不同的,t1线程进行读取的时候,只是读取了t1线程的"CPU 寄存器"的值,t2线程进行修改的时候,先修改的是"CPU 寄存器"中的值,然后再把这个值同步到内存中,但是由于编译器优化,t1没有重新从内存中同步数据到"CPU 寄存器"中,读到的结果就是"修改之前的值"

主内存:main memory 贮存,也叫做内存

工作内存:work memory 工作存储区,不是内存,而是cpu上存储数据的单元(这里是存储器,还有其他东西(cache等))

这里的工作内存还不光指寄存器,还可能是高速缓冲器,cpu读取寄存器,速度比读取内存快太多了,为了减小这个差距,引入了cache,cache是指可以进行高速数据交换的存储器,它先于内存CPU交换数据,因此速率很快

​编辑

缓存的工作原理

当CPU要读取一个数据时,首先从CPU缓存中查找,找到就立即读取并送给CPU处理;没有找到,就从速率相对较慢的内存中读取并送给CPU处理,同时把这个数据所在的数据块调入缓存中,可以使得以后对整块数据的读取都从缓存中进行,不必再调用内存。正是这样的读取机制使CPU读取缓存的命中率非常高(大多数CPU可达90%左右),也就是说CPU下一次要读取的数据90%都在CPU缓存中,只有大约10%需要从内存读取。这大大节省了CPU直接读取内存的时间,也使CPU读取数据时基本无需等待。总的来说,CPU读取数据的顺序是先缓存后内存

相比于cpu和内存,它的存储空间居中,读写速度居中,成本居中,当CPU需要读到一个内存数据的时候,可能直接从内存读,也能从cache中的缓存,也可能读寄存器中的数据

因此硬件结构更复杂了,工作存储区=cpu寄存器+cpu cache,为了表述简单,直接就用"工作内存"代替了!

===