【Java多线程】volatile与内存屏障详解

volatile关键字的要求

被volatile关键字修饰的变量,具有如下特性:

1.保证可见性:volatile关键字保证可见性,意味着:

  • 对一个volatile修饰的变量进行读操作的话,总是能读到这个变量的最新的值,也就是这个变量最后被修改的值。
  • 一个线程修改了volatile修饰的变量的值时,那么这个变量的新的值,会立即刷新回到主内存中。
  • 一个线程去读取volatile修饰的变量的值时,该变量在工作内存中的数据无效,需要重新到主内存去读取最新的数据。

2.禁止指令重排,也就说维护happens-before的关系

  • 对volatile变量的写入,不能重排到写入之前的操作之前。
  • 对volatile变量的读取,不能重排到读取操作的后续操作之后。

volatile不保证原子性。

happens-before原则(先行发生原则):

  • 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作。
  • 锁定规则:一个unLock操作先行发生于后面对同一个锁额lock操作。
  • volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作。
  • 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C。
  • 线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作。
  • 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生。
  • 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行。
  • 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始。

volatile和synchronized区别:

  • volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取; synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
  • volatile仅能使用在变量级别;synchronized则可以使用在代码块、方法、和类级别的。
  • volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性。
  • volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
  • volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化。

volatile测试程序

关于synchronized、volatile可以用来测试的程序

public class VolatileStudy {
    //volatile关键字的典型使用场景
    //多个线程间的状态通信,比如:一个线程要等待另一个线程完成一定的工作过后,这个线程才能开始工作
    private int num = 0;
    private volatile boolean needStart = false;
    public void addNum(){
        num++;
    }
    public int getNum(){
        return num;
    }

    public static void main(String[] args) {
        VolatileStudy volatileStudy = new VolatileStudy();
        new Thread(()->{
            for (int i = 0; i < 5; i++) {
                volatileStudy.addNum();
                System.out.println("now addNum次数==="+i);

                try{
                    Thread.sleep(500L);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            //工作完成后,设置needStart
            volatileStudy.needStart = true;
            System.out.println("已经设置了needstart为true");
        }).start();

        new Thread(()->{
            while(!volatileStudy.needStart){

            }
            System.out.println("now num==="+volatileStudy.getNum());
        }).start();
    }
}

volatile原理

volatile内存语义

volatile写的内存语义:写一个volatile变量时,JMM会把线程对应的工作内存中的共享变量的值刷新到主内存中。

volatile读的内存语义:读一个volatile变量时,JMM会把线程对应的工作内存中共享变量数据设置为无效的,然后会从主内存中去读取共享变量最新的数据。

volatile内存语义的实现

1.字节码层面

它影响的是Class内的Field的flags;

添加了一个 ACC_VOLATILE

JVM在把字节码生成为机器码的时候,发现操作是volatile的变量的话,就会根据JMM要求,在相应的位置去插入内存屏障指令。

2.JMM层面:插入内存屏障

  • volatile写之前的操作,都禁止重排序到volatile之后。
  • volatile读之后的操作,都禁止重排序到volatile之前。
  • volatile写之后的volatile读,禁止重排序。

为了实现volatile内存语义,按如下方式来插入内存屏障:

  • 在每个volatile写操作的前面插入一个StoreStore屏障

    禁止上面的普通写和下面的volatile写操作重排序,前面所有的普通写操作,数据都已经刷新到主内存。

    保证普通写和volatile写禁止重排;volatile写和volatile写禁止重排。

  • 在每个volatile写操作的后面插入一个StoreLoad屏障

    禁止上面的volatile写和下面的volatile读/写或普通写操作重排序,前面所有的volatile写的操作,数据都已经刷新到主内存。

    volatile写操作和普通写操作禁止重排;volatile写和volatile读/写禁止重排。

  • 在每个volatile读操作的后面插入一个LoadLoad屏障

    禁止下面的普通读、volatile读和上面的volatile读重排序。

    volatile读和普通读禁止重排;volatile读和volatile读禁止重排。

  • 在每个volatile读操作的后面插入一个LoadStore屏障

    禁止上面的volatile读和下面的volatile写或普通写重排序。

    volatile读和普通写禁止重排;volatile读和volatile写禁止重排。

JMM采用保守的内存屏障插入策略,始终在每个volatile写后面插入一个StoreLoad屏障。

这样可以保证在任意处理器平台上,volatile语义的正确性。

在这里插入图片描述

为什么没有禁止普通读和volatile写之间的重排序?

volatile写:针对的是volatile的变量,在写的时候,是把volatile变量的值,从工作内存刷新到主内存。

普通读:读的一定不是volatile变量。

普通读不会对volatile变量的值造成影响,也不影响相应的内存,不会去破坏volatile的内存语义。

因此他们之间不需要加屏障指令 3.处理器层面

cpu执行机器码指令的时候,是使用lock前缀指令,来实现volatile的功能的。

Lock指令,相当于内存屏障,功能也类似。

  • 首先对总线/缓存加锁,然后执行后面指令,最后释放锁,同时把高速缓存的数据刷新回到主内存。
  • 在lock锁住总线/缓存的时候,其他的cpu的读写请求就会被阻塞,直到锁释放。Lock过后写操作,会让其他cpu的高速缓存中相应的数据失效。这样后续这些cpu在读取数据的时候就会从主内存加载最新的数据。

加了Lock指令过后的具体表现和JMM添加内存屏障过后一样。

内存屏障

是一种屏障指令,它使得CPU或编译器对屏障指令的前和后所发出的内存操作,执行一个排序的约束。也叫内存栅栏或栅栏指令。

内存屏障的能力:

1.阻止屏障两边的指令重排序

2.写数据的时候加了屏障,强制把写缓冲区的数据刷回到主内存中。

3.读数据的时候,加了屏障,让工作内存/CPU高速缓存当中缓存的数据失效,重新到主内存中获取新的数据。

基本分类:

1.读屏障:Load Barrier:在读指令之前插入读屏障。让工作内存/CPU高速缓存当中缓存的数据失效,重新到主内存中获取新的数据。

2.写屏障:Store Barrier:在写指令之后插入写屏障,强制把写缓冲区刷回到主内存中。

重排序和内存屏障的关系

重排序可能会给程序带来问题,因此有些时候我们希望告诉JVM,这里不需要排序。

JVM本身为了保证可见性:

1.对于编译器的重排序,JMM会根据重排序的规则,禁止特定类型的编译器重排序

2.对于处理器的重排序,java编译器在生成指令序列的适当位置,插入内存屏障指令,来禁止特定类型的处理器排序。

JMM的内存屏障

  • LoadLoad Barriers:Load1;LoadLoad: Load2

    禁止重排序,访问Load2的读取操作一定不会重排到Load1之前。

    保证Load2在读取的时候,自己缓存内相应数据失效,Load2会去主内存中获取到最新的数据。

  • LoadStore Barriers:Load1;LoadStore; Store2

    禁止重排序:一定是Load1读取数据完成后,才能让Store2及其之后的写出操作的数据,被其他线程看到。

  • StoreStore Barriers:Store1;StoreStore; Store2

    禁止重排序:一定是Store1的数据写出到主内存完成后,才能让Store2及其之后的写出操作的数据,被其他线程看到。

    保证Store1指令写出去的数据,会强制被刷新回到主内存中。

  • StoreLoad Barriers:Store1;StoreLoad;Load2

    禁止重排序:一定是Store1的数据写入到主内存完成后,才能让Load2来读取数据。

    同时保证:强制把写缓冲区的数据刷回到主内存中。

    ​ 让工作内存/cpu高速缓存当中缓存的数据失效,重新到主内存中获取新的数据。

为什么说StoreLoadBarriers是最重的?

重:和内存交互次数多,交互延迟较大。

参考学习资料

b站视频内存屏障与volatile相关

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值