Java

深入理解Java内存模型 ch4 volatile

2018-12-10  本文已影响75人  王侦

1.volatile的特性

class VolatileFeaturesExample{
  volatile long vl = 0L;
  
  public void set(long l) {
    vl = l;
  }

  public void getAndIncrement() {
    vl++;
  }

  public long get() {
    return vl;
  }

这个程序在语义上和下面的程序等价:

class VolatileFeaturesExample{
  volatile long vl = 0L;
  
  public synchronized void set(long l) {
    vl = l;
  }

  public void getAndIncrement() {
    long tmp = get();
    tmp += 1L;
    set(tmp);
  }

  public synchronized long get() {
    return vl;
  }

关键:对一个 volatile变量的单个读 /写操作 ,与对一个普通变量的 读/写操作使用同一个锁来步 ,它们之间的执行效果相同 。

总之:

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

从内存语义的角度来说,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()方法。

3.volatile写-读的内存语义

volatile写的内存语义:

volatile读的内存语义:

4.volatile内存语义的实现

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


基于保守策略的JMM内存屏障插入策略:


StoreStore屏障保证上面所有的普通写在volatile写之前刷新到主内存。
StoreLoad屏障是避免volatile写与后面可能有的volatile读/写操作重排序。

为了保证正确实现volatile的内存语义,在每个volatile写的后面或者读前面插入一个StoreLoad屏障。从整体执行效率的角度考虑,选择写后面添加。因为volatile写-读内存语义的常见使用模式是:一个写线程写volatile变量,多个线程读同一个volatile变量。


LoadLoad屏障禁止处理器把上面的volatile读与下面的普通读重排序。LoadStore屏障禁止处理器把上面的volatile读与下面的普通写重排序。

上述 volatile写和volatile读的内存屏障插入策略非常保守。在实际执行时,只要不改变volatile写-读的内存语义,编译器可以根据具体情况省略不必要的屏障。

class VolatileBarrierExample{
  int a;
  volatile int v1 = 1;
  volatile int v2 = 2;

  void readAndWrite() {
    int i = v1;
    int j = v2;
    a = i + j;
    v1 = i + 1;
    v2 = j * 2;
  }
}

内存屏障的插入还可以根据具体的处理器内存模型继续优化。x86处理器除最后StoreLoad屏障外,其它的屏障都会被省略。
因为x86仅会对写-读操作做排序。这意味着x86处理器中,volatile写的开销比volatile读的开销会大很多(因为执行StoreLoad屏障开销会比较大)。


5.总结

由于 volatile仅仅保证对单个volatile变量的读 /写具有原子性, 而锁的 互斥执行特性可以确保对整个临界区代码的执行具有原子性。 在功能上,锁比 volatile更强 大;在可伸缩性和执行能上, volatile更有优势 。

上一篇 下一篇

猜你喜欢

热点阅读