美文网首页
JVM-volatile的内存语义

JVM-volatile的内存语义

作者: Briarbear | 来源:发表于2018-06-04 10:41 被阅读0次

    本篇内容主要摘自《Java并发编程的艺术-方腾飞》

    • 更多相关文章见笔者博客

    1. volatile特性

    • 理解 volatile 特性的一个好方法是把对 volatile 变量的单个读 / 写,看成是使用同一个锁对这些单个读 / 写操作做了同步。下面通过具体的示例来说明,示例代码如下
    class VolatileFeaturesExample {
        volatile long vl = 0L; //使用volatile声明64位的long型变量
     
        public void set(long l) {
            vl = l; //单个volatile变量的写
        }
     
        public void getAndIncrement() {
            vl++; //复合(多个)volatile变量的读/写
        }
     
        public long get() {
            return vl; //单个volatile变量的读
        }
    }
    
    • 假设有多个线程分别调用上面程序的 3 个方法,这个程序在语义上和下面程序等价
    class VolatileFeaturesExample {
        long vl = 0L; // 64 位的 long 型普通变量
        public synchronized void set(long l) { // 对单个的普通变量的写用
            vl = l;
        }
     
        public void getAndIncrement () {// 普通方法调用
            long temp = get();// 调用已同步的读方法
            temp += 1L;// 普通写操作
            set(temp);// 调用已同步的写方法
        }
     
        public synchronized long get() { // 对单个的普通变量的读用同一个锁同步
            return vl;
        }
    }
    
    • 如上面示例程序所示,一个 volatile 变量的单个读 / 写操作,与一个普通变量的读 / 写操作都是使用同一个锁来同步,它们之间的执行效果相同
    • 锁的 happens-before 规则保证释放锁和获取锁的两个线程之间的内存可见性,这意味着对一个 volatile 变量的读,总是能看到(任意线程)对这个 volatile 变量最后的写入
    • 锁的语义决定了临界区代码的执行具有原子性。这意味着,即使是 64 位的 long 型和 double 型变量,只要它是 volatile 变量,对该变量的读 / 写就具有原子性。如果是多个volatile 操作或类似于volatile++ 这种复合操作,这些操作整体上不具有原子性
    • 简而言之,volatile 变量自身具有下列特性
      • 可见性 对一个 volatile 变量的读,总是能看到(任意线程)对这个 volatile 变量最后
        的写入
      • 原子性 对任意单个 volatile 变量的读 / 写具有原子性,但类似于 volatile++ 这种复合操作不具有原子性

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

    • 上面讲的是 volatile 变量自身的特性,对程序员来说,volatile 对线程的内存可见性的影响比 volatile 自身的特性更为重要,也更需要我们去关注

    • 从 JSR-133 开始(即从 JDK5 开始),volatile 变量的写 - 读可以实现线程之间的通信。

    • 从内存语义的角度来说,volatile 的写 - 读与锁的释放 - 获取有相同的内存效果:volatile
      写和锁的释放有相同的内存语义;volatile 读与锁的获取有相同的内存语义

    • 请看下面使用 volatile 变量的示例代码

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

      假设线程 A 执行 writer() 方法之后,线程 B 执行 reader() 方法。根据 happens-before 规
      则,这个过程建立的 happens-before 关系可以分为 3 类:
      1)根据程序次序规则,1 happens-before 2; 3 happens-before 4
      2)根据 volatile 规则,2 happens-before 3
      3)根据 happens-before 的传递性规则,1 happens-before 4

      上述 happens-before 关系的图形化表现形式如下

    image

    在上图中,每一个箭头链接的两个节点,代表了一个 happens-before 关系。黑色箭头表示程序顺序规则;橙色箭头表示 volatile 规则;蓝色箭头表示组合这些规则后提供的 happens-before保证

    这里 A 线程写一个 volatile 变量后,B 线程读同一个 volatile 变量。A 线程在写 volatile 变量之前所有可见的共享变量,在 B 线程读同一个 volatile 变量后,将立即变得对 B 线程可见。


    3. volatile 写 - 读的内存语义

    volatile 写的内存语义如下

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

    以上面示例程序 VolatileExample 为例,假设线程 A 首先执行 writer() 方法,随后线程 B执行 reader() 方法,初始时两个线程的本地内存中的 f lag 和 a 都是初始状态。图 3-17 是线程A 执行 volatile 写后,共享变量的状态示意图

    image

    如上图所示,线程 A 在写 f lag 变量后,本地内存 A 中被线程 A 更新过的两个共享变量的值被刷新到主内存中。此时,本地内存 A 和主内存中的共享变量的值是一致的

    • volatile 读的内存语义如下

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

    图 3-18 为线程 B 读同一个 volatile 变量后,共享变量的状态示意图。

    如图所示,在读 f lag 变量后,本地内存 B 包含的值已经被置为无效。此时,线程 B 必须从主内存中读取共享变量。线程 B 的读取操作将导致本地内存 B 与主内存中的共享变量的值变成一致

    如果我们把 volatile 写和 volatile 读两个步骤综合起来看的话,在读线程 B 读一个volatile 变量后,写线程 A 在写这个 volatile 变量之前所有可见的共享变量的值都将立即变得对读线程 B 可见

    • 下面对 volatile 写和 volatile 读的内存语义做个总结
      • 线程 A 写一个 volatile 变量,实质上是线程 A 向接下来将要读这个 volatile 变量的某个线程发出了(其对共享变量所做修改的)消息
      • 线程 B 读一个 volatile 变量,实质上是线程 B 接收了之前某个线程发出的(在写这个volatile 变量之前对共享变量所做修改的)消息
      • 线程 A 写一个 volatile 变量,随后线程 B 读这个 volatile 变量,这个过程实质上是线程 A 通过主内存向线程 B 发送消息
    image

    相关文章

      网友评论

          本文标题:JVM-volatile的内存语义

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