JVM-volatile的内存语义

本篇内容主要摘自《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的内存语义)