【并发编程】天天说Volatile,你知道其内存语义是什么吗?

推荐阅读



volatile的作用我们都知道,他是在多线程之间实现可见性的保证,那么在JMM中

  • volatile的内存语义以及其实现你真的了解吗?
  • 为什么使用volatile就可以保证内存可见性?
  • 为什么说volatile保证可见性却不能保证原子性?

在之前的 Java的内存模型 的Happen-Before 的几个原则中就说到 ,对于volatile变量的写操作先于发生任意后续这个变量的读操作 ,也就是说对volaitile的写操作可以对任意其他线程的读操作可见。那么本章节就着重了解一些volatile的内存语义与实现原理。在JSR-133 中


1、Volatile 特性

理解volatile特性的一个好方法是把对volatile变量的单个读/写,看成是使用同一个锁对这些单个读/写操作做了同步。下面通过具体的示例来说明,示例代码如下。

class Test{
  volatile  Long count;

  public Long getCount() {return count;  }

  public void setCount(Long count) { this.count = count;  }
  
  public void increment(){ this.count++;  }
}


假设多线程执行程序,那么其执行效果等效于

class Test{
  private   Long count;

  public synchronized Long getCount() {
    return count;
  }

  public synchronized void setCount(Long count) {
    this.count = count;
  }


  public synchronized void increment(){
    this.count++;
  }
}

如上面示例程序所示,一个volatile变量的单个读/写操作,与对一个普通变量的的加锁读/写操作,它们之间的执行效果相同

锁的happens-before规则保证释放锁和获取锁的两个线程之间的内存可见性,这意味着对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入。

2、 volatile 写/读建立的 happens-before关系


从volatile 的内存语义上来说,volatile的写-读与所得释放-获取有相同的效果;volatile 写和锁的释放具有相同的内存语义,volatile读与所得获取有相同的内存语义。




比如下面的程序中

class VolatileExample {
  int a = 0;
  volatile boolean flag = true;
    
  public void writer(){
    a = 1; // 1
    flag = true; // 2
  }
    
    
  public void reader(){
  	if(flag){      // 3
    	int i = a; // 4
    }
  }
}


线程A执行writer() 方法,之后线程B执行reader()方法, 根据As-Serial-if 规则可知 1 happens-before 2 , 3 happens-before 4 ,根据volatile写-读规则可知,2 hapens-beofre 3,所以1 happens-before 4 ,所以可知 根据volatile 规则可可以构建Happens-Before 规则。

3、Volatile 的内存语义与其实现原理

3.1 volatile 写操作与读操作的内存语义

  1. 当写一个Volatile 变量的时候,JMM会把该线程对应的本地内存的共享变量刷新到主内存中. ** 使用上面的代码中示例,当在执行writer() 方法 写了flag变量后,JMM会立即可本地内存的共享变量刷新到主内存中。

  2. 当读取一个volatile变量的时候,JMM会将当前线程的本地内存共享变量设置为无效,线程在接下来的操作,会从主内存中读取共享变量。


所以,volatile 的内存语义就是,在修改一个volatile变量的时候,会将本地内存的共享变量刷新到主内存,然后读取一个volatile变量的时候会将本地内存作废,然后从主内存中读取数据。所以线程1更新volatile变量的值,线程2读取volatile变量,实际上就是线程1向线程2发送消息通知线程2共享变量发生改变。

3.2 volatile 内存语义的实现


在之前的笔记中,笔者就已经提到过,JMM为了实现volatile内存语义使用的就是在指令序列中插入不同的内存屏障指令。JMM 有四种内存屏障指令,用于不同的场景,JMM的场景如下:

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

3.3 在volatile 写操作之前插入StoreStore 屏障


因为在对volatile 写操作之后需要同步刷新到主内存中,此时如果对volatile写操作之前的普通写操作重排序到volatile写操作之后,那么这个普通写的操作呢就不能被刷新到主内存中,所以为了防止出现这种不可见的情况,所以在volatile写操作之前需要插入StoreStore 屏障,防止之前的普通写操作重排序到volatile写操作之后。


3.4 在volatile 写操作之后插入StoreLoad 屏障

StoreLoad 防止volatile写操作后面的可能存在volatile读操作重排序,按照正常的未重排序的操作,首先会执行volatile写操作,然后同步到主内存,然后volatile读操作会从主内存中读取数据,如果出现重排序,可能会出现先执行volatile读操作,在出现volatile 写操作。为了防止这种情况的重排需要插入StoreLoad屏障执行。

因为JMM无法确定volatile写操作后面是否会出现volatile读操作,所以采取了保守策略: 在每个volatile写操作后面或者在每个volatile读操作前面提示添加StoreLoad指令,但是常见的模型是一个线程写volatile,多个线程读volatile, 那么为了性能上的考虑,所以在每个volatile写操作后面添加了StoreLoad指令。

3.5 在volatile 读操作之后添加 LoadLoad 和 LoadStore 屏障


在执行volatile 操作之后,会将本地内存作废,所以在volatile读操作之后的在普通读操作不能排到volatile读操作之前,所以这里需要添加LoadLoad屏蔽。同时如果volatile读操作与下面的普通写操作排序,那么那么有可能会将普通写的操作作废,所以为了禁止这种情况,也需要在volatile读操作之后添加LoadStore屏障。

事实上,在实际执行的时候,只要不修改volatile的内存语义,一些场合下,可以省略部分内存屏障的指令,以提高效率。同时不同的处理器对于内存屏蔽也有不同的松紧度,

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值