zoukankan      html  css  js  c++  java
  • volatile

    作用原理

    volatile是Java虚拟机提供的轻量级的同步机制。

    两个作用

    • 可见性:保证被volatile修饰的共享变量对所有线程总数可见的,也就是当一个线程修改了一个被volatile修饰共享变量的值,新值总是可以被其他线程立即得知。
    • 有序性:禁止指令重排序优化

    但无法保证原子性

     

    作用实现

    可见性

    package com.gx.demo.bingfa;
    
    public class VolatileTest {
        
        private volatile boolean changeFlag = false;
    
        public void save() {
            this.changeFlag = true;
            System.out.println("线程:" + Thread.currentThread().getName() + " 修改了主存中的共享变量changeFlag");
        }
    
        public void load() {
            while (!changeFlag) {
            }
            System.out.println("线程:" + Thread.currentThread().getName() + " 感知到了changeFlag变量的修改");
        }
    
        public static void main(String[] args) {
            VolatileTest sample = new VolatileTest();
            Thread threadA = new Thread(() -> {
                sample.save();
            },"threadA");
            Thread threadB = new Thread(()-> {
                    sample.load();
                },"threadB");
            threadB.start();
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            threadA.start();
        }
    }
    结果:
    线程:threadA:修改共享变量changeFlag
    线程:threadB 感知到了changeFlag变量的修改

    有序性(禁止指令重排)

    禁止指令重排优化指的是:避免多线程环境下程序出现乱序执行的现象。

     

    内存屏障

    概念

    什么是内存屏障(Memory Barrier)?
    内存屏障(memory barrier)是一个CPU指令。

    它的作用有两个:

    a) 确保一些特定操作执行的顺序

    b) 影响一些数据的可见性(可能是某些指令执行后的结果,保证在内存中可见)。

    编译器和CPU可以在保证输出结果一样的情况下对指令重排序,使性能得到优化。插入一个内存屏障,相当于告诉CPU和编译器先于这个命令的必须先执行,后于这个命令的必须后执行。内存屏障另一个作用是强制更新一次不同CPU的缓存。

    例如,一个写屏障会把这个屏障前写入的数据刷新到缓存,这样任何试图读取该数据的线程将得到最新值,而不用考虑到底是被哪个cpu核心或者哪颗CPU执行的。

     

    硬件层的内存屏障

    Intel硬件提供了一系列的内存屏障,主要有:

    1. lfence,是一种Load Barrier 读屏障
    2. sfence, 是一种Store Barrier 写屏障
    1. mfence, 是一种全能型的屏障,具备ifence和sfence的能力
    2. Lock前缀,Lock不是一种内存屏障,但是它能完成类似内存屏障的功能。Lock会对CPU总线和高速缓存加锁,可以理解为CPU指令级的一种锁。它后面可以跟ADD, ADC, AND, BTC, BTR, BTS, CMPXCHG, CMPXCH8B, DEC, INC, NEG, NOT, OR, SBB, SUB, XOR, XADD, and XCHG等指令。

     

    JVM提供的四类内存屏障

    Java内存屏障主要有Load和Store两类。
    对Load Barrier()来说,在读指令前插入读屏障,可以让高速缓存中的数据失效,重新从主内存加载数据
    对Store Barrier()来说,在写指令之后插入写屏障,能让写入缓存的最新数据写回到主内存

     

    对于Load和Store,在实际使用中,又分为以下四种:

    屏障类型

    指令例子

    说明用途

    LoadLoad

    Load1,Loadload,Load2

    确保Load1所要读入的数据能够在被Load2和后续的load指令访问前读入

    StoreStore

    Store1,StoreStore,Store2

    确保Store1的数据在Store2以及后续Store指令操作相关数据之前对其它处理器可见(例如向主存刷新数据)

    LoadStore

    Load1; LoadStore; Store2

    确保Load1的数据在Store2和后续Store指令被刷新之前读取。在等待Store指令可以越过loads指令的乱序处理器上需要使用LoadStore屏障。

    StoreLoad

    Store1; StoreLoad; Load2

    确保Store1的数据在被Load2和后续的Load指令读取之前对其他处理器可见

    volatile的有序性实现

    JMM针对编译器制定的volatile重排序规则表。

    第一个操作 第二个操作:普通读写 第二个操作:volatile读 第二个操作:volatile写

    普通读写 可以重排 可以重排 不可以重排

    volatile读 不可以重排 不可以重排 不可以重排

    volatile写 可以重排 不可以重排 不可以重排

     

    为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。对于编译器来说,发现一个最优布置来最小化插入屏障的总数几乎不可能。为此,JMM采取保守策略。下面是基于保守策略的JMM内存屏障插入策略。

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

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

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

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

    上述内存屏障插入策略非常保守,但它可以保证在任意处理器平台,任意的程序中都能得到

    正确的volatile内存语义。

     

    有序性示例:

    单例模式中的DCL(double check lock)。

    private static MyTest instance;
    public static MyTest getInstance(){
        if(instance == null){
            synchronized (MyTest.class){
                if(instance == null){
                    instance = new MyTest();
                }
            }
        }
        return instance;
    }
    instance = new MyTest();

    在内存中具体分为几个步骤实现

    1.给对象分配内存空间

    2.初始化对象 init()

    3.给变量分配内存地址

    步骤2、3可能会发生重排序,所以当一条线程访问instance不为null时,由于instance实例未必已初始化完成,也就造成了线程安全问题。

    解决办法:

    private volatile static MyTest instance;//禁止指令的重排
     

    原子性

    volatile无法保证原子性。

    static volatile int i = 0;
    public static void caculate(){
        i++;
    }

    i++分为:先去读取i的值,然后再+1写入一个新的值,两个步骤完成,本身不具备原子性。

    假如在第一步完成之后,第二步执行之前时,有线程在此时读取了i在内存中的值,那么这个线程会和开始那个线程相当于要对i执行一样的操作,i结果都是1。也就造成线程安全失败了。

    解决办法:对执行方法添加synchronized,但是synchronized一样具备了可见性,可以不用volatile修饰了。

    我始终记住:青春是美丽的东西,而且对我来说,它永远是鼓舞的源泉。——(现代)巴金
  • 相关阅读:
    11.22
    python之字典(dict)
    Version Control
    java_实现一个类只能声明一个对象
    javase-位运算符
    javase-整数变量的交换
    URI和URL的关系与区别
    http解析
    函数式语言
    MyIASM和Innodb引擎详解
  • 原文地址:https://www.cnblogs.com/flyinglion/p/15088062.html
Copyright © 2011-2022 走看看