美文网首页Java
深入理解Java内存模型 ch4 volatile

深入理解Java内存模型 ch4 volatile

作者: 王侦 | 来源:发表于2018-12-10 20:36 被阅读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变量的单个读 /写操作 ,与对一个普通变量的 读/写操作使用同一个锁来步 ,它们之间的执行效果相同 。

    总之:

    • 可见性: 对一个 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

        本文链接:https://www.haomeiwen.com/subject/brvdhqtx.html