【尚硅谷周阳--JUC并发编程】【第七章--volatile与JMM】

12 篇文章 0 订阅

一、被volatile修饰的变量有两大特点

1、特点

  1. 可见性
  2. 有序性
    排序要求有时需要禁重排

关于可见性和有序性的介绍查看第六章

2、volatile的内存语义

  1. 当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值立即刷新回主内存中并及时发出通知。
  2. 当读一个volatile变量时,JMM会把该线程对应的本地内存设置为无效,重新回到主内存中读取最新共享变量。
  3. 所以volatile的写内存语义是直接刷新到主内存中,读的内存语义是直接从主内存中读取。

3、volatile凭什么可以保证可见性和有序性

内存屏障Memory Barrier

二、内存屏障(面试重点)

1、是什么?

  1. 内存屏障(也称内存栅栏,屏障指令等,是一类同步屏障指令,是CPU或编译器在对内存随机访问的操作中的一个同步点,使得此点之前的所有读写操作都执行后才开始执行此点之后的操作),避免代码重排序。内存屏障其实就是一种JVM指令,Java内存模型的重排规则会要求Java编译器在生成JVM指令时插入特定的内存屏障指令,通过这些内存屏障指令,volatile实现了Java内存模型中的可见性和有序性(禁重排),但volatile无法保证原子性
  2. 内存屏障之前的所有写操作都要回写到主内存
  3. 内存屏障之后的所有读操作都能获得内存屏障之前的所有写操作的最新结果(实现了可见性)
  4. 写屏障(Store Memory Barrier):告诉处理器在写屏障之前将所有存储在缓存(Store bufferes)中的数据同步到主内存。也就是说当看到Store屏障指令,就必须把该指令之前所有写入指令执行完毕才能继续往下执行。
  5. 读屏障(Load Memory Barrier):处理器在读屏障之后的读操作,都在读屏障之后执行。也就是说在Load屏障指令之后就能够保证后面的读取数据指令一定能够读取到最新的数据。
    在这里插入图片描述
  6. 因此重排序时,不允许把内存屏障之后的指令重排序到内存屏障之前。一句话:对一个volatile变量的写,先行发生于任意后续对这个volatile变量的读,也叫写后读。

2、内存屏障分类

2.1、粗分两种

  1. 读屏障(Load Barrier)
    在读指令之前插入读屏障,让工作内存或CPU高速缓存当中的缓存数据失效,重新回到主内存中获取最新数据
  2. 写屏障(Store Barrier)
    在写指令之后插入写屏障,强制把写缓存冲区的数据刷回到主内存中

2.2、细分四种

  1. 在Unsafe.class中,有三个native方法
    在这里插入图片描述
    调用unsafe类时unsafe.cpp会自动加载以下三个方法
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
  2. 四大屏障分别是什么意思
    在这里插入图片描述

2.3、详细介绍

2.3.1、什么叫保证有序性?

通过内存屏障禁重排

  1. 重排有可能影响程序的执行和实现,因此,我们有时候希望告诉JVM你别“自作聪明”给我重排序,我这里不需要重排序
  2. 对于编译器的重排序,JMM会根据重排序的规则,禁止特定类型的编译器重排序
  3. 对于处理器的重排序,Java编译器在生成指令序列的适当位置,插入内存屏障指令,来禁止特定类型的处理器排序
2.3.2、happens-before之volatile变量规则

在这里插入图片描述

2.3.3、JMM就将内存屏障插入策略分为4种规则
  1. 读屏障
    • 在每个volatile读操作的后面插入一个LoadLoad屏障
    • 在每个volatile读操作的后面插入一个LoadStore屏障
      在这里插入图片描述
  2. 写屏障
    • 在每个volatile写操作的前面插入一个StoreStore屏障
    • 在每个volatile写操作的后面插入一个StoreLoad屏障
      在这里插入图片描述

三、volatile特性

1、保证可见性

1.1、说明

保证不同线程对某个变量完成操作后结果及时可见,即该共享变量一旦改变所有线程立即可见

1.2、案例

  • 不加volatile,没有可见性,程序无法停止
public class VolatileSeeDemo {

    static boolean flag = true;

    public static void main(String[] args) {
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "\t ------come in");
            while (flag) {

            }
            System.out.println(Thread.currentThread().getName() + "\t ------flag被设置为false,程序停止了");
        }, "t1").start();

        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

        flag = false;

        System.out.println(Thread.currentThread().getName() + "\t 修改完成");
    }
}

在这里插入图片描述

  • 加了volatile,保证可见性,程序可以停止
public class VolatileSeeDemo {

    static volatile boolean flag = true;
    public static void main(String[] args) {
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "\t ------come in");
            while (flag) {

            }
            System.out.println(Thread.currentThread().getName() + "\t ------flag被设置为false,程序停止");
        }, "t1").start();

        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

        flag = false;

        System.out.println(Thread.currentThread().getName() + "\t 修改完成flag");
    }
}

在这里插入图片描述

1.3、上述代码原理解释

  1. 线程t1中为何看不到被主线程main修改为false的flag值?
    • 问题可能:
      • 主线程修改了flag之后没有将其刷新到主内存,所以t1线程看不到。
      • 主线程将flag刷新到了主内存,但是t1一直读取的是自己工作内存中flag的值,没有去主内存中更新获取flag最新的值。
    • 我们的诉求:
      • 线程中修改了自己工作内存中的副本之后,立即将其刷新到主内存
      • 工作内存中每次读取共享变量时,都去主内存中重新读取,然后拷贝到工作内存。
    • 解决:使用volatile修饰共享变量,就可以达到上面的效果,被volatile修饰的变量有以下特点
      • 线程中读取的时候,每次读取都会去主内存中读取共享变量最新的值,然后将其赋值到工作内存
      • 线程中修改了工作内存中变量的副本,修改之后会立即刷新到主内存。

1.4、volatile变量的读写过程

Java内存模型中定义的8种每个线程自己的工作内存与物理内存之间的原子操作
在这里插入图片描述

  • read:作用于主内存,将变量的值从主内存传输到工作内存,主内存到工作内存
  • load:作用于工作内存,将read从主内存传输的变量值放入工作内存变量副本中,即数据加载
  • use:作用于工作内存,将工作内存变量副本的值传递给执行引擎,每当JVM遇到需要该变量的字节码指令是会执行该操作
  • assign:作用于工作内存,将从执行引擎接收到的值赋值给工作内存变量,每当JVM遇到一个给变量赋值字节码指令时会执行该操作
  • store:作用于工作内存,将赋值完毕的工作变量的值写回给主内存
  • write:作用于主内存,将store传输过来的变量值赋值给主内存中的变量
    由于上述6条只能保证单条指令的原子性,针对多条指令的组合性原子保证,没有大面积加锁,所以,JVM提供了另外两个原子指令
  • lock:作用于主内存,将一个变量标记为一个线程独占的状态,只是写时候加锁,就只是锁了写变量的过程
  • unlock:作用于主内存,把一个处于锁定状态的变量释放,然后才能被其他线程占用

2、没有原子性

volatile变量的复合操作不具有原子性,比如number++

2.1、代码案例及说明

public class VolatileNoAtomicDemo {

    public static void main(String[] args) {
        MyNumber myNumber = new MyNumber();

        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    myNumber.addPlusPlus();
                }
            }, String.valueOf(i)).start();
        }

        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

        System.out.println(myNumber.number);
    }
}


class MyNumber {
    volatile int number;

    public void addPlusPlus() {
        number++;
    }
}

在这里插入图片描述
在这里插入图片描述
(面试题:举例说明volatile没有原子性)即多线程情况下,写操作有可能会被丢失(由于数据加载,数据计算,数据赋值这三个步骤非原子操作,有可能存在已经出现了number++但是主内存中变量又被修改了,导致需要重新加载变量而出现当前number++操作作废),由此可见,volatile不具备原子性,如果需要原子性需要加锁。例如:

class MyNumber {
    volatile int number;

    public synchronized void addPlusPlus() {
        number++;
    }
}

2.2、从i++的字节码角度说明

在这里插入图片描述

2.3、结论

volatile变量不适合参与到依赖当前值的运算
在这里插入图片描述

3、指令禁重排

3.1、说明与案例

  1. 重排序

    • 重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段,有时候会改变程序语句的先后顺序
    • 不存在数据依赖关系,可以重排序,存在数据依赖关系,禁止重排序
    • 但重排序后指令绝对不能改变原有的串行语义!这点在并发设计中必须要重点考虑!
  2. 重排序的分类和执行流程
    重排序分类和执行流程

    • 编译器优化的重排序:编译器在不改变单线程串行语义的前提下,可以重新调整指令的执行顺序
    • 指令级并行的重排序:处理器使用指令级并行技术来将多条指令重叠执行,若不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序
    • 内存系统的重排序:由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是乱序执行
  3. 数据依赖性:若两个操作访问同一变量,且这两个操作中有一个位写操作,此时两操作间就存在数据依赖性

  4. 案例

    • 不存在数据依赖关系,可以重排序
      在这里插入图片描述
    • 若存在数据依赖关系,禁止重排序。编译器和处理器在重排序时,会遵守数据依赖性,不会改变存在依赖关系的两个操作的执行,但不同处理器和不同线程之间的数据依赖性不会被编译器和处理器考虑,其只会作用域单处理器和单线程环境,下面三种情况,只要重排序两个操作的执行顺序,程序的执行结果就会被改变。
      在这里插入图片描述

3.2、四大屏障的插入情况

  1. 在每一个volatile写操作前面插入一个StoreStore屏障。StoreStore屏障可以保证在volatile写之前,其前面的所有普通写操作都已经刷新到主内存
  2. 每一个volatile写操作后面插入一个StoreLoad屏障。StoreLoad屏障的作用是避免volatile写与后面可能有的volatile读/写操作重排序
  3. 每一个volatile读操作后面插入一个LoadLoad屏障。LoadLoad屏障用来禁止处理器把上面的volatile读与下面的普通读重排序
  4. 每一个volatile读操作后面插入一个LoadStore屏障。LoadStore屏障用来禁止处理器把上面的volatile读与下面的普通写重排修

3.3、案例

public class VolatileTest {

    int i = 0;
    boolean flag = false;

    public void write() {
        i = 2;
        flag = true;
    }

    public void read() {
        if (flag) {
            System.out.println("flag = " + flag + " and  ---i = " + i);
        } else {
            System.out.println("flag = " + flag);
        }
    }
}

在这里插入图片描述

四、如何正确使用volatile

1、单一赋值

  1. 单一赋值可以,但是含复合运算赋值不可以(i++之类)
    • volatile int a = 10;
    • volatile boolean flag = false;

2、状态标识,判断业务是否结束

在这里插入图片描述

3. 开销较低的读,写锁策略

在这里插入图片描述

4. DCL(double-checked locking)双端锁的发布

public class SafeDoubleCheckSingleton {

    private static SafeDoubleCheckSingleton singleton;

    // 私有化构造方法
    private SafeDoubleCheckSingleton() {

    }
    // 双重锁设计
    public static SafeDoubleCheckSingleton getInstance() {
        if (singleton == null) {
            // 1、多线程并发创建对象时,会通过加锁保证只有一个线程能创建对象
            synchronized (SafeDoubleCheckSingleton.class) {
                if (singleton == null) {
                    singleton = new SafeDoubleCheckSingleton();
                }
            }
        }
        // 2、对象创建完毕,执行getInstance()将不需要获取锁,直接返回创建对象
        return singleton;
    }

}
  1. 单线程环境下(或者说正常情况下),在“问题代码处”,或执行如下操作,保证能获取到已完成初始化的实例
    单线程处理流程
  2. 多线程情况下,存在指令重排序
    多线程下指令重排
    在Java中,对象的创建和初始化并不是原子操作,可能会被编译器和处理器进行指令重排优化,这就可能导致在一个线程执行对象初始化的过程中,另一个线程已经获取了对尚未完全初始化的对象的引用,导致访问到未初始化完成的对象。
    多线程情况下创建对象指令重排
  3. 解决方案
    单例模式配合双端锁使用案例

五、本章总结

1、volatile可见性

volatile可见性

2、volatile没有原子性

3、volatile禁重排

  1. 写指令
    • volatile写操作之前会加StoreStore屏障,禁止上面的普通写和下面的volatile写操作重排序,前面所有的普通写操作,数据都已经刷新到主内存,普通写和volatile写禁止重排;volatile写和volatile写禁止重排
    • volatile写操作之后会加StoreLoad屏障,禁止上面的volatile写和下面volatile读/写或普通读写操作重排序,前面volatile写的操作,数据都已经刷新到主内存;volatile写和普通写禁止重排;volatile写和volatile读/写禁止重排。
  2. 读指令
    • volatile读操作之后加LoadLoad屏障,禁止下面的普通读、volatile读和上面的volatile读重排序;volatile读和普通读禁止重排,volatile读和voliate读禁止重排
    • volatile读操作之后加LoadStore屏障,禁止上面的voliate读和下面的volatile写或普通写重排序

4、为什么加上volatile关键字底层就加入了内存屏障?

# 命令
javap -v
# 或者
javap -c

volatile内存屏障底层实现

5、什么是内存屏障

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

6、内存屏障能干嘛?

  1. 阻止屏障两边的指令重排序
  2. 写数据时加入屏障,强制将线程私有工作内存的数据刷回主物理内存
  3. 读数据时加入屏障,线程私有工作内存的数据失效,重新到主物理内存中获取最新数据

7、内存屏障四大指令

  1. 在每一个volatile写操作前面插入一个StoreStore屏障
  2. 在每一个volatile写操作后面插入一个StoreLoad屏障
  3. 在每一个volatile读操作后面插入一个LoadLoad屏障
  4. 在每一个volatile读操作后面插入一个LoadStore屏障

8、3句话总结

  1. volatile写之前的操作,都禁止重排序volatile之后
  2. volatile读之后的操作,都禁止重排序到volatile之前
  3. volatile写之后volatile读,禁止重排序
  • 32
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值