java并发之volatile

java并发之volatile

作用:保证变量的可见性

可见性问题的由来:

由于CPU比内存以及磁盘的速度快得多,CPU操作一个变量需要等到内存、磁盘读取成功。这时的CPU会死等在这里,极大的影响了CPU利用率。为了解决这个问题、硬件工程师先是增加了高速缓存,事先让一些常用变量加载到高速缓存中,这样CPU就不需要等待内存加载成功,这样提高了CPU的利用率,但是体验还是不行。为了提升CPU的利用率,发明了线程,在CPU死等的时候去运行其他的内容,极大地利用了CPU资源。但是工程师没有满足,为了得到更快的运行效率,提出了多核CPU。本来在单核CPU的多线程环境中没有可见性问题,但是随着CPU从单核变为多核就出现了可见性问题。

可见性问题:

有一个场景,有两个CPU核心,每个CPU核心有一个缓存。主内存保存有count=0;有两个线程对count进行循环加一操作。

当第一个线程得到CPU0的时间片时,对count进行加操作。即从0开始加。

而此时第二个线程恰好得到CPU1的时间片,也会进行count的加操作。也会从缓存里面读出值,此时CPU1缓存中的值还是0,并不是CPU0中已经计算到的值。就会进行从0开始加的操作。这个就是可见性问题。

其原因是因为不同CPU的缓存中的数据没有共享。、

解决可见性问题

因为CPU的缓存数据没有共享,才导致可见性问题,解决方法当然是让数据共享,即让不同CPU的缓存一致。可以通过缓存锁的的方式来保证缓存一致。即让CPU读写是遵循缓存一致性协议,比如MESI协议。该协议规定了缓存的四种状态。其中M、S、E状态下CPU可以从缓存中读取数据,当缓存处于I状态,CPU只能从主内存中读取数据。这样就可以保证缓存一致性。大致思路是如果一个CPU修改了值,他会告知其他CPU他缓存中的值现在已经无效了,需要从主内存里面重新加载,其他的CPU在接受到这个信息之后,会返回一个ACK给修改值的CPU。CPU接收到值之后才进行将数据同步到主内存,再接着运行其他指令,当然这个过程是阻塞的。为了解决阻塞问题,提高CPU的利用率,引入了一个称作StoreBuffer的东西,即CPU在通知其他CPU之后,就将该数据保存在StoreBuffer,自己去执行其他代码去了,不阻塞在这里。等到ACK返回之后,再将StoreBuffer中的数据同步到缓存以及主内存中。

重排序问题

但是,通过该方式还有一个问题没有解决,就是指令重排序的问题。上面讲到,为了最大化利用CPU资源,CPU死等其他CPU的ACK,它会先运行其他指令。这样就会打乱一些变量之间的逻辑关系。举个简单的例子。见下面代码。

package com.test.demo;

public class VolatileTest {

    static  boolean is_Finish =false;
    static  int i =0;
    
    public static void main(String[] args) {
        
        Thread thread = new Thread(() -> {
            i = 10;
            is_Finish = true;
        });

        Thread thread1 = new Thread(() -> {
            if (is_Finish) {
                if (i == 10) {
                    System.out.println("true");
                }else{
                    System.out.println("false");
                }
            }
        });
        
        thread.start();
        thread1.start();

    }
}

上面代码,怎么看就该输出true,但是按照原来的思路来看会有过一个问题!

即当is_Finish请求的ACK比i的ACK要返回的快,该程序是会输出false的。is_Finish已经通过MESI协议了,i还没有通过,此时i还是0;

这就又有了一个新的问题,指令重排序。

为了解决重排序,设置了内存屏障,CPU在处理有内存屏障的数据时就不考虑CPU的利用率问题了,就会阻塞在这里,不去接下去运行其他指令,一定要等待ACK响应才会打开。

JMM是什么?

Java语言时一个跨平台的语言,不同的平台的硬件系统不一样,就导致了设置内存屏障方式的不同,JAVA为了解决这个问题,将对内存以及高速缓存的读写操作进行了抽象,这个抽象的读写过程就是JMM。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0tpneiS9-1619163985688)(C:\Users\15292\AppData\Roaming\Typora\typora-user-images\image-20210423152833258.png)]

这是Java层面的读写操作,我们通过这个读写操作去设置内存屏障,再去调用不同硬件系统的内存屏障的API,就达到了写一个JAVA程序,适应不同的硬件系统。

Java语言设置内存屏障解决可见性以及重排序的方式

volatile、synchronized、final以及happens-before规则

happens-before就是表示一个代码的顺序关系

1 happens-before 2,代表1一定比2先执行。

JMM中建立happens-before规则的方式

程序顺序规则

监视器锁的规则

volatile变量规则

传递性

start()规则

线程终止原则

线程中断规则

join()规则

对象终结规则

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值