深入理解Java内存模型 ch4 volatile

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变量的单个读 /写操作 ,与对一个普通变量的 读/写操作使用同一个锁来步 ,它们之间的执行效果相同 。

总之:

  • 可见性: 对一个 volatile变量的读,总是能看到 (任意线程)对这个 volatile变量最后的写入。
  • 原子性: 对任意单个volatile变量的读 /写具有原子性, 写具有原子性, 但类似于 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()方法。

  • 根据程序次序规则:
    1 happens-before 2
    3 happens-before 4
  • 根据volatile规则
    2 happens-before 3

3.volatile写-读的内存语义

volatile写的内存语义:

  • 当写一个volatile变量时, JMM会把该线程对应的本地内存中的共享变量值刷新到主内存。

volatile读的内存语义:

  • 当读一个 volatile变量时, JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。

4.volatile内存语义的实现

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


  • (写会将本地内存刷新到主内存)当第二个操作是volatile写时,不管第一个操作什么都能重排序。 这个 规则确保volatile写之前的操作不会被编译器重排序到 volatile写之后。
  • (会清空本地内存,从主存读取)当第一个操作是 volatile读时,不管第二个操作是什么都能重排序。 这个 规则确保volatile读之后的操作不会被编译器重排序到volatile读之前。
  • 当第一个操作是volatile写,第二个操作是volatile读时,不能重排序。

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

  • 在每个 volatile写操作的前面插入一个 StoreStore屏障
  • 在每个 volatile写操作的后面插入一个StoreLoad屏障
  • 在每个 volatile读操作的后面插入一个 LoadLoad屏障
  • 在每个volatile读操作的后面插入一个 LoadStore屏障

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更有优势 。

你可能感兴趣的:(深入理解Java内存模型 ch4 volatile)