理解 JVM:Java 内存模型(二)——volatile

概述

java 内存模型的核心是围绕着在并发过程中如何处理原子性、可见性、有序性这3个特性来展开的,它们是多线程编程的核心。

  • 原子性(Atomicity):是指一个操作是不可中断的,即使是多个线程同时执行的情况下,一个操作一旦开始,就不会被其它线程干扰。对于基本类型的读写操作基本都具有原子性的(在32位操作系统中 long 和 double 类型数据的读写不是原子性的,因为它们有64位)。
  • 可见性(Visibility):是指在多线程环境下,当一个线程修改了某一个共享变量的值,其它线程能够立刻知道这个修改。
  • 有序性(Ordering):是指程序的执行顺序是按照代码的先后顺序执行的;对于这句话如果在单线程中所有的操作都是有序的,但是在多线程环境下,一个线程的操作相对于另外一个线程的操作是无序的。

先行发生原则(happens-before)

先行发生是 Java 内存模型中定义的两个操作之间的偏序关系,这些先行关系无需任何同步器的协助就已经存在,可以在编码中直接使用。Java 内存模型对这些关系作了如下规则:

  • 程序次序规则(Program Order Rule):在一个线程内,程序安装代码顺序执行。即所谓的“线程内表现为串行的语义(Within-Thread As-If-Serial Semantics)”。
  • 管程锁定规则(Monitor Lock Rule):一个 unlock 操作先行发生于后面对同一个锁的 lock 操作。
  • volatile 变量规则(Volatile Variable Rule):对于一个 volatile 变量的写操作先行发生于此线程的每一个动作。
  • **线程启动规则(Thread Start Rule):**Thread 对象的 start() 方法先行发生于此线程的每一个动作。
  • 线程终止规则(Thread Termination Rule):线程中的所有操作先行发生于对此线程的终止检测。
  • 线程中断规则(Thread Interruption Rule):对线程 interrupt() 方法的调用先行发生于被中断线程的代码检测到中断事件的发生。
  • 对象终结规则(Finalizer Rule):一个对象的初始化完成先行发生于它的 finalizer() 方法。
  • 传递性(Transitivity):如果操作 A 先行发生于操作 B,操作 B 先行发生于 操作 C,可以推断出 操作 A 先行发生于操作 C。

关键字 volatile

volatile 修饰的变量保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。因为当对普通变量进行读写的时候,每个线程先从内存拷贝变量到CPU缓存中。如果计算机有多个CPU,每个线程可能在不同的CPU上被处理,这意味着每个线程可以拷贝到不同的CPU cache中。而volatile修饰的变量,JVM保证了每次读变量都从内存中读,跳过CPU cache这一步。volatile修饰的变量禁止进行指令重排序,所以能在一定程度上保证有序性。只能保证该变量所在的语句还是原来的位置,并不能保证该语句之前或之后的语句是否被打乱。

volatile 的特性

  1. 当一个变量被 volatile 修饰之后,能保证此变量对所有线程的可见性,即当一个线程修改了这个变量的值,新值对其它线程是立即可见的。
  2. 被 volatile 修饰的变量通过查询内存屏障来禁止指令重排序,所以能在一定程度上保证有序性。
  3. 对任意单个 volatile 变量的读/写具有原子性,但类似于 volatile++ 这种复合操作不具有原子性。 例如:

package com.pdh.test;

/** * volatile 复合操作测试 * * @author pengdh * @date 2017/11/12 */
public class VolatileDemo {
    // 申明 volatile 变量
    private static volatile int i = 0;
    // 计数
    private static final int COUNT = 10;

    /** * 对 volatile 变量复合运算 */
    private static void increase() {
        i++;
    }

    public static void main(String[] args) {
        // 启动 10 个线程分别对 i 进行 10000 次计算,正常情况结果为 100000 
        for (int j = 0; j < COUNT; j++) {
            new Thread(() -> {
                for (int k = 0; k < 10000; k++) {
                    increase();
                }
            }).start();
        }
        // 等待所有累加线程全部执行结束,这里不同 ide 中线程存活数不一样,
        // 该示例代码在 idea 中运行,会多出一个 Monitor Ctrl-Break 线程,故条件是 > 2,
        // 如果在 Eclipse 中条件应为 > 1
        while (Thread.activeCount() > 2) {
            Thread.yield();
        }
        System.out.println(i);
    }
}

如上代码正常运行结果应该打印100000,但实际结果基本得不到正确结果。这说明了 volatile 变量的复合运算并不具有原子性,想要得到正确结果,需要对 volatile 变量运算操作加锁或者加上同步块。


package com.pdh.test;

/** * volatile 复合操作测试 * * @author pengdh * @date 2017/11/12 */
public class VolatileDemo {
    // 申明 volatile 变量
    private static volatile int i = 0;
    // 计数
    private static final int COUNT = 10;

    /** * 对 volatile 变量复合运算,使用 synchronized 同步 */
    private static synchronized void increase() {
        i++;
    }

    public static void main(String[] args) {
        // 启动 10 个线程分别对 i 进行 10000 次计算,正常情况结果为 100000
        for (int j = 0; j < COUNT; j++) {
            new Thread(() -> {
                for (int k = 0; k < 10000; k++) {
                    increase();
                }
            }).start();
        }
        // 等待所有累加线程全部执行结束,这里不同 ide 中线程存活数不一样,
        // 该示例代码在 idea 中运行,会多出一个 Monitor Ctrl-Break 线程,故条件是 > 2,
        // 如果在 Eclipse 中条件应为 > 1
        while (Thread.activeCount() > 2) {
            Thread.yield();
        }
        System.out.println(i);
    }
}

volatile 适合场景

volatile适用于不需要保证原子性,但却需要保证可见性的场景。一种典型的使用场景是用它修饰用于停止线程的状态标记,如:

package com.pdh.test;


/** * volatile 复合操作测试 * * @author pengdh * @date 2017/11/12 */
public class VolatileDemo {
    // 申明 volatile 变量
    private volatile boolean flag = false;
    // 计数
    private static final int COUNT = 10;

    /** * 使用 volatile 变量作为线程结束标志 */
    private void start() {
        new Thread(() -> {
            while (!flag) {
                System.out.println("Thread is running");
            }
        }).start();
    }

    private void shutdown() {
        flag = true;
        System.out.println("Thread is stop");
    }

    public static void main(String[] args) throws InterruptedException {
        VolatileDemo demo = new VolatileDemo();
        demo.start();
        Thread.sleep(2000);
        demo.shutdown();
    }
}

使用 volatile 的意义

在只需保证可见性的情况下,volatile 的同步机制性能要优于锁。

参考文献

  • 深入理解 Java 虚拟机

《理解 JVM:Java 内存模型(二)——volatile》

欢迎扫一扫关注 程序猿pdh 公众号!

    原文作者:java内存模型
    原文地址: https://blog.csdn.net/bhuds/article/details/78509830
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞