ConcurrentProgramming:volatile/构造方法溢出/禁止重排序

ConcurrentProgramming:volatile/构造方法溢出/禁止重排序


关键词

  • 内存可见性原理(直接操作主存)
  • 禁止指令重排原理(内存屏障,最终目的:直接操作主内存
    对volatile写操作:会在写操作后,加入一条store屏障指令,将工作内存中的共享变量值刷新回主内存
    对volatile读操作:会在读操作签,加入一条load屏障指令从主内存读取共享变量
  • 实践场景
    64位写入的原子性、构造方法原子性
    单例模式(DCL-Double Check Lock 双端检锁机制 )

一、64位写入的原子性(Half Write)

如,对于一个long型变量的赋值和取值操作而言,在多线程场景下,线程A调用set(100),线程B调用get(),在某些场景下,返回值可能不是100。

public class MyClass {
    
  private long a = 0;
    
  // 线程A调用set(100)
  public void set(long a) {
    this.a = a;
  }
 
  // 线程B调用get(),返回值一定是100吗?
  public long get() {
    return this.a;
  }
    
}

因为JVM的规范并没有要求64位的long或者double的写入是原子的。在32位的机器上,一个64位变量的写入可能被拆分成两个32位的写操作来执行。这样一来,读取的线程就可能读到“一半的值”。解决办法也很简单,在long前面加上volatile关键字

二、 重排序:DCL问题

单例模式的线程安全的写法不止一种,常用写法为DCL(Double Checking Locking),如下所示:

public class Singleton {
    
  private static Singleton instance;
    
  public static Singleton getInstance() {
      
    if (instance == null) {
        
      synchronized(Singleton.class) {
          
        if (instance == null) {
          // 此处代码有问题
          instance = new Singleton();
       }
          
     }
        
   }
      
    return instance;
 }
    
}

上述的 instance = new Singleton(); 代码有问题:其底层会分为三个操作:

  1. 分配一块内存。
  2. 在内存上初始化成员变量。
  3. 把instance引用指向内存。
对象的构造并不是“原子的”

在这三个操作中,操作2和操作3可能重排序,即先把instance指向内存,再初始化成员变量,因为二者并没有先后的依赖关系。此时,另外一个线程可能拿到一个未完全初始化的对象。这时,直接访问里面的成员变量,就可能出错。这就是典型的“构造方法溢出”问题。

解决办法也很简单,就是为instance变量加上volatile修饰

volatile的三重功效:
  • 64位写入的原子性
  • 内存可见性
  • 禁止重排序

三、 volatile实现原理

由于不同的CPU架构的缓存体系不一样,重排序的策略不一样,所提供的内存屏障指令也就有差异。

这里只探讨为了实现volatile关键字的语义的一种参考做法:

  1. 在volatile写操作的前面插入一个StoreStore屏障。保证volatile写操作不会和之前的写操作重排序。
  2. 在volatile写操作的后面插入一个StoreLoad屏障。保证volatile写操作不会和之后的读操作重排序。
  3. 在volatile读操作的后面插入一个LoadLoad屏障+LoadStore屏障。保证volatile读操作不会和之后的读操作、写操作重排序。

具体到x86平台上,其实不会有LoadLoad、LoadStore和StoreStore重排序,只有StoreLoad一种重排序(内存屏障),也就是只需要在volatile写操作后面加上StoreLoad屏障。

四、JSR-133对volatile语义的增强

在JSR -133之前的旧内存模型中,一个64位long/ double型变量的读/ 写操作可以被拆分为两个32位的读/写操作来执行。从JSR -133内存模型开始 (即从JDK5开始),仅仅只允许把一个64位long/ double型变量的写操作拆分为两个32位的写操作来执行,任意的读操作在JSR -133中都必须具有原子性(即 任意读操作必须要在单个读事务中执行)。

这也正体现了Java对happen-before规则的严格遵守。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

穿城大饼

你的鼓励将是我最大的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值