关键字volatile-创新互联

volatile关键字和内存的可见性问题密切相关.

站前网站建设公司创新互联建站,站前网站设计制作,有大型网站制作公司丰富经验。已为站前超过千家提供企业网站建设服务。企业网站搭建\成都外贸网站建设要多少钱,请找那个售后服务好的站前做网站的公司定做!

用一段代码解释什么为内存可见性问题:

class MyCounter {
    public int flag = 0;
}

public class ThreadDemo15 {
    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 scanner = new Scanner(System.in);
            System.out.println("请输入一个整数: ");
            myCounter.flag = scanner.nextInt();
        });

        t1.start();
        t2.start();
    }
}

在这段代码里:

t1要循环快速的重复读取,t2进行修改.

预期:t2把flag改成非0的值之后,t1随之就结束循环.

但是当我们把flag改成非0的值后,t1循环并没有随之结束.

同时可以看到,t2这个线程已经没了,执行完了,但是t1线程依然存在,还在继续循环.

这个情况就叫做"内存可见性问题".

这个代码是不是一个bug?

是,这也是一个线程不安全的问题,一个线程读,一个线程改.

这里使用汇编来理解,大概就是两步操作:

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

2.cmp.把寄存器里的值,和0进行比较,根据比较的结果,来决定下一步往哪个地方来执行.(条件跳转指令).

上述是一个循环,这个循环执行的速度非常快,一秒执行百万次以上.

循环执行这么多次,在t2真正修改之前,load得到的结果都是一样的.另一方面,load操作和cmp操作相比,速度慢很多很多!!!(CPU针对寄存器的操作,要比内存快很多,3-4个数量级;计算机对于内存的操作,比硬盘快3-4个数量级).

由于load执行的速度太慢(相对于cmp操作来说),在加上反复load得到的结果都一样,所以JVM就对此做出了调整,不在真正重复的load,判定不在修改flag的值,所以只load一次就好了.(这也是编译器优化的一种方式).

总结内存可见性问题:一个线程针对一个变量进行读取操作,同时另一个线程针对这个变量进行修改.

此时读取到的值,不一定是修改过后的值.(进行读取操作的线程没有感知到变量的变化).

归根结底是编译器/JVM 在多线程环境下优化时产生了误判.

此时就需要程序员进行手动干预了.可以给flag这个变量加上volatile关键字.意思就是告诉编译器,这个变量是"异变"的,一定要每次都重新读这个变量的内存内容,不能再进行激进的优化了.

volatile关键字只能修饰变量,不能修饰方法.

可不可以修饰方法里的局部变量呢??

不可以 ,方法里的局部变量,只能再你当前线程里面使用.不能多线程同时读取/修改,天然的规避了线程安全问题.(只能再当前方法里使用出了方法变量就没了.方法内部的变量,再栈这样的空间上,每个线程都有自己的栈空间,即使是同一个方法,在多个线程中被调用,这里的局部变量也会处在不同的栈空间中,本质上是不同的变量)


上述的内存可见性问题,是编译器优化的问题,但也不是始终会出现的.(编译器可能存在误判,但也不是100%误判的)

比如将上述代码稍微调整一下

加了sleep控制了速度之后,即使不加volatile,代码也正确了.(刚才错误的优化也消失了)

编译器的优化,很多时候是不可预估的,应用程序这个角度是无法感知的.因此稳妥的做法,就是把改加volatile的地方都加上.


从JMM的角度表述内存可见性问题

JMM:Java Memory Model,Java内存模型.

表述:Java程序里,主内存,每个线程有自己的工作内存(t1和t2的工作内存不是同一个东西).

t1线程进行读取的时候,只是读取了工作内存的值.

t2线程进行修改的时候,先修改工作内存的值,然后再把工作内存的内容同步到主内存中.

但是由于编译器优化,导致t1没有重新的从主内存当中同步数据到工作内存,读到的结果就是修改之前的结果.

上述表述,来自于Java的官方文档.

解读:

主内存:main memory 主存,也就是咱们平时说的的内存.

工作内存:work memory 工作存储区,这个东西并非所说的内存,而是CPU上存储数据的单元(寄存器).

那为什么Java这里,不直接叫做"CPU寄存器",而是专门起了"工作内存"这样的说法?

这里的工作内存,不一定只是CPU的寄存器,还可能包括CPU的缓存cache.

CPU读取寄存器,速度比读取内快很多,因此就会在CPU内部引入缓存cache.

寄存器存储空间小,读写速度快,成本贵.

在内存和寄存器中间搞了个cache,存储空间居中,读写速度居中,成本居中.

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

当CPU要读取一个内存数据的时候,可能直接读内存,也可能是读cache,还能是读寄存器.

引入cache之后,硬件结构就变得复杂了.

工作内存(工作存储区):CPU寄存器+CPU的cache

一方面是为了表述简单,一方面也是为了避免涉及到硬件的细节和差异,Java这里就使用"工作内存"一词一言蔽之了.


你是否还在寻找稳定的海外服务器提供商?创新互联www.cdcxhl.cn海外机房具备T级流量清洗系统配攻击溯源,准确流量调度确保服务器高可用性,企业级服务器适合批量采购,新人活动首月15元起,快前往官网查看详情吧


文章题目:关键字volatile-创新互联
链接地址:http://pwwzsj.com/article/dseggj.html