Happens-Before规则与DCL失效原因分析

先行发生是Java内存模型中定义的两项操作之间的偏序关系,如果操作A先行发生于操作B,其实就是说在发生操作B之前,操作A产生的影响能被操作B观察到,“影响”包括修改了内存中共享变量的值/发送了消息/调用了方法等。

Happens-Before规则(先行发生原则)

1. 程序次序规则(Program Order Rule):

在一个线程内,按照程序代码顺序,书写在前面的操作先行发生于书写在后面的操作。如下 value = 4 语句 happens-before flag = true 语句;

 

public class HappensBefore {

    static int value = 0;
    static volatile boolean flag = false;

    public  void write(){
        value = 4;
        flag = true;
    }

    public  void read(){
        if(flag){
            System.out.println(value);
        }
    }
}

2 .管程锁定规则(Monitor Lock Rule):

管程—一种通用的同步语句,在Java中主要指的就是Synchronized。而管程锁定规则指的就是对一个锁的unlock happens-before 后续对这个锁的lock操作。如下,若线程A、B同时访问read方法,当线程A执行完之后,线程B能够获取到线程A对变量的操作。

 

public class HappensBefore {

    int value = 10;

    public void read(){
        synchronized (this){ //此处加锁
            if(value < 100){
                value ++;
            }
        }//此处自动解锁
    }
}

3.volatile变量规则(Volatile Variable Rule):

对一个volatile变量的写操作先行发生于后面对这个变量的读操作,这里的“后面”是指时间上的先后顺序。

4.传递性(Transitivity):

如果操作A先行发生于操作B,操作B先行发生于操作C,那么操作A先行发生于操作C。

第3点以及第4点都采用第一段代码做示例,首先从第一段代码我们可以知道value = 4 happens-before flag = true;然后可以得知对volatile 型变量flag 的写操作 happens-before 对其的读操作。因此根据传递性规则可以得知 value = 4 happens-before flag = ture的读操作。因此当线程B读到了flag=true时,那么线程A对value所做的更改对B线程也是可见的。这得益于jdk1.5之后对volatile关键字的语义增强(可见性,与禁止指令重排序)。

5.线程启动规则(Thread Start Rule):

Thread对象的start()方法先行发生于此线程的每一个动作。它是指主线程 A 启动子线程 B 后,子线程 B 能够看到主线程在启动子线程B前的操作。换句话说就是,如果线程A调用线程B的Start()方法(在线程A中启动线程B),那么该start()操作happens-before于线程B中的任意操作。

 

public class HappensBefore{

    static int value = 10;

    public static void main(String[] args) {

        Thread newThread = new Thread(new Runnable() {
            @Override
            public void run() {
                //线程启动前,所有对共享变量的修改,此处都可见,因此这里的value=66
                System.out.println(value);
            }
        });
        value = 66;
        newThread.start();
    }
}

6. 线程 join() 规则:

指主线程 A 等待子线程 B 完成(主线程 A 通过调用子线B的Join()方法实现),当子线程B完成后(主线程A中Join方法返回),主线程能够看到子线程的操作。也就是如果在线程A中,调用线程B的Join()并成功返回,那么线程B中的任意操作Happens-Before于该Join操作的返回。如下:

 

public class HappensBefore{

    static int value = 10;

    public static void main(String[] args) {

        Thread newThread = new Thread(new Runnable() {
            @Override
            public void run() {
                //此处对共享变量进行修改
                value = 666;
            }
        });

        newThread.start();
        try {
            newThread.join();
            System.out.println(value);//子线程对共享变量的修改在子线程调用Join之后皆可见,此处value=666
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

DCL(双锁检查机制)实现单例模式失效分析

先看一段双锁检查实现单例模式的代码:

 

//Double-check-lock 单例模式失效原因详解
public class Singleton {
    
    static Singleton instance;// 使用volatile可以禁止指令的重排序
    
    static Singleton getInstance(){
        if(instance == null){
            synchronized (Singleton.class){
                if (instance == null){
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

假设此时同时有线程A、以及线程B同时调用getInstance()方法来获取Singleton的实例。这段代码看起来不存在什么问题,但是并不完美,缺陷就在进行 instance = new Singleton()操作的时候。一般情况下我们所理解的new操作会进行如下的操作:

  1. 分配一块内存M
  2. 在内存M上初始化Singleton对象
  3. 将M的地址值赋给instance

然后经过编译器优化(指令重排序)的操作如下:

  1. 分配一块内存M
  2. 将M的地址值赋给instance
  3. 在内存M上初始化Singleton对象

 

 

具体示意图如下:

DCL.png

假设当线程A调用getInstance()方法时 instance == null 此时就会执行new Singleton()操作,此时的指令是已经经过编译器优化的了,假设当执行到
Instance = M 即将M的地址值赋给 instance的时候,线程B获得了执行权,当线程B再去调用getInstance()方法时instance != null 此时线程B就有可能会获得未经过初始化的Singleton对象,从而导致NullPointException异常的发生。关于这种情况的解决办法,可以使用Volatile关键字来进行规避,采用Volatile关键字之后,计算机对Instance对象进行操作时,会加入内存屏障从而禁止编译器对相应的指令进行重排序。



作者:LeonardoEzio
链接:https://www.jianshu.com/p/8446a398ca68
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

<think>好的,用户想了解Java内存屏障的相关知识,特别是在并发编程中的作用,尤其是和volatile关键字的关联。之前我已经回答过关于volatile的详解,现在需要更深入地探讨内存屏障。首先,我需要回顾之前的回答和相关引用,确保信息一致且补充新的细节。 用户提供的引用中提到了内存屏障如何保障可见性和有序性,以及不同平台的内存模型差异。引用[1]到[5]详细说明了内存屏障的作用、类型以及JVM如何利用它们实现volatile的特性。用户可能希望了解内存屏障的具体类型、工作原理,以及它们如何Java内存模型(JMM)结合使用。 接下来,我需要组织信息结构。可能的分点包括内存屏障的定义、类型、在JMM中的实现、volatile的关系、实际应用案例以及注意事项。需要确保每个部分都引用适当的文献支持,例如引用[2]解释内存屏障的基本作用,引用[4]说明JMM如何统一不同平台,引用[5]关于内存屏障的插入规则。 另外,用户可能对内存屏障如何具体禁止重排序和保证可见性感兴趣。需要详细解释LoadLoad、StoreStore等屏障的作用,并结合volatile变量的读写操作示例。例如,在volatile写操作后插入StoreLoad屏障,确保写入立即对其他线程可见。 还要注意用户可能的疑问点,比如内存屏障的性能影响,以及其他同步机制(如synchronized)的对比。引用[5]提到synchronized也能获得可见性,但性能较差,这部分需要强调推荐使用volatile的原因。 最后,生成相关问题时要围绕内存屏障和volatile的深入应用,例如不同屏障类型的区别,volatilehappens-before的关系,以及实际调试中的注意事项。确保问题能够引导用户进一步学习相关知识点。</think>### Java内存屏障详解(并发编程视角) #### 一、核心定义作用 内存屏障(Memory Barrier)是JVM插入的特殊指令,用于控制**指令重排序**和**内存可见性**。主要实现两个目标: 1. 禁止编译器/处理器对特定指令进行重排序(保障有序性) 2. 强制刷新CPU缓存,保证多线程数据一致性(保障可见性)[^2][^3][^4] #### 二、JMM定义的四种内存屏障 | 屏障类型 | 作用场景 | 具体效果 | 对应volatile操作 | |-------------------|-----------------------------|------------------------------------------|-----------------| | LoadLoad屏障 | load1; LoadLoad; load2 | 确保load1先于load2执行 | volatile读之后 | | StoreStore屏障 | store1; StoreStore; store2 | 确保store1的结果对其他处理器可见先于store2 | volatile写之前 | | LoadStore屏障 | load; LoadStore; store | 禁止load后续store重排序 | volatile读之后 | | StoreLoad屏障 | store; StoreLoad; load | 强制刷写缓存到内存,并重新加载最新值(代价最大) | volatile写之后 | > 示例:volatile变量的写操作会插入`StoreStore`(禁止前面普通写volatile写重排)和`StoreLoad`屏障(保证写操作立刻全局可见)[^4][^5] #### 三、内存屏障在volatile中的实现 ```java public class MemoryBarrierDemo { volatile int value = 0; // 触发JVM插入内存屏障 void writer() { // 普通写操作 int a = 1; // StoreStore屏障(禁止a=1value=2重排序) value = 2; // volatile写 // StoreLoad屏障(强制刷新到主内存) } void reader() { // LoadLoad屏障(保证先读取value) int r1 = value; // volatile读 // LoadStore屏障(禁止后续普通写读操作重排序) int b = r1 + 1; } } ``` 实现机制: 1. **写屏障序列**:`普通写 -> StoreStore屏障 -> volatile写 -> StoreLoad屏障` 2. **读屏障序列**:`volatile读 -> LoadLoad屏障 + LoadStore屏障 -> 普通读写`[^3][^5] #### 四、并发编程中的关键作用 1. **可见性保障** 通过强制缓存失效机制,确保: - volatile写操作前的所有变量修改**立即同步到主内存** - volatile读操作后的所有变量读取**必须从主内存重新加载** (对应happens-before原则中的volatile规则)[^1][^3] 2. **禁止重排序** 典型案例:DCL单例模式中的对象初始化 ```java instance = new Singleton(); // 实际包含三个步骤: // 1. 分配内存空间(普通写) // 2. 初始化对象(构造函数调用) // 3. 将引用指向内存地址(volatile写) ``` 若无内存屏障,步骤2和3可能被重排序,导致其他线程获取未初始化的对象。内存屏障禁止了这种危险的重排[^2][^5] #### 五、性能影响实测对比 | 操作类型 | 典型耗时(x86架构) | 主要成本来源 | |-------------------|--------------------|------------------------| | 普通变量读写 | 1-3 ns | 寄存器/L1缓存访问 | | volatile变量读写 | 5-10 ns | 内存屏障指令执行 | | synchronized块进入 | 20-30 ns | 锁竞争上下文切换 | > 建议:仅在需要跨线程可见性时使用volatile,避免滥用[^5] #### 六、其他机制的对比 | 特性 | volatile | synchronized | Atomic变量 | |--------------------|------------------------|----------------------|---------------------| | 原子性保障 | 仅单次读/写 | 代码块级 | CAS操作保证原子性 | | 可见性实现 | 内存屏障 | 管程退出时刷新内存 | 底层使用volatile | | 指令重排控制 | 读/写双向屏障 | 单方向内存语义 | 依赖volatile特性 | | 适用场景 | 状态标志、单次写入 | 复杂操作同步 | 计数器等复合操作 | #### 七、调试技巧 1. 使用`-XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly`查看JVM插入的内存屏障指令 2. 通过`juc`包的`VarHandle`可手动插入内存屏障: ```java VarHandle.fullFence(); // 插入全能屏障(StoreLoad) ``` --相关问题--: 1. StoreLoad屏障为什么性能消耗最大? 2. 如何手动插入特定类型的内存屏障? 3. volatile的happens-before规则具体包含哪些内容? 4. 内存屏障在ARM架构x86架构的实现差异? 5. 为什么说synchronized的可见性实现不如volatile严格?
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值