Java源码分析系列笔记-3.volatile

目录

  • [1. 是什么](#1. 是什么)
  • [2. 什么情况 volatile 比 synchronized 更合适](#2. 什么情况 volatile 比 synchronized 更合适)
    • [2.1. 例子](#2.1. 例子)
    • [2.2. 无法停止的原因分析](#2.2. 无法停止的原因分析)
    • [2.3. 解决方法](#2.3. 解决方法)
    • [2.4. volatile vs synchronized](#2.4. volatile vs synchronized)
  • [3. 汇编源码实验](#3. 汇编源码实验)
    • [3.1. 下载编译 hsdis-amd64.dll](#3.1. 下载编译 hsdis-amd64.dll)
    • [3.2. 放入 JRE bin 目录下](#3.2. 放入 JRE bin 目录下)
    • [3.3. 对比实验](#3.3. 对比实验)
    • [3.4. 加上 jvm 参数运行](#3.4. 加上 jvm 参数运行)
    • [3.5. 输出结果对比](#3.5. 输出结果对比)
  • [4. 根据实验结果分析原理](#4. 根据实验结果分析原理)
    • [4.1. 可见性](#4.1. 可见性)
    • [4.2. 有序性](#4.2. 有序性)
  • [5. 参考](#5. 参考)

1. 是什么

Java 的轻量级锁,主要保证了有序性、可见性和一定的原子性

  • 轻量级
    相比于 synchronized,volatile 不会引起上下文切换(不会造成线程阻塞)
  • 原子性
    对任意单个 volatile 变量的读/写具有原子性,但类似于 volatile++这种复合操作不具有原子性
  • 可见性
    volatile 写会把数据同时写入主内存,并让其他线程对这个数据的工作内存失效,这样其他线程读的时候就需要去主内存中读取
  • 有序性
    对一个 volatile 变量的读,总是能看到(任意线程)对这个 volatile 变量最后的写入。

2. 什么情况 volatile 比 synchronized 更合适

2.1. 例子

如下程序。thread1 并不会停止

java 复制代码
public class VolatileTest
{
    private static boolean isRunning = true;

    public static void main(String[] args) throws InterruptedException
    {
        Thread thread1 = new Thread(()->{
            System.out.println("thread1 is running");
            while (isRunning)
            {

            }
            System.out.println("thread1 will be stopped");
        });
        thread1.start();

        Thread.sleep(1000);

        Thread thread2 = new Thread(()->{
            System.out.println("thread2 is running");

            isRunning = false;

            System.out.println("thread2 change isRunning flag");
        });
        thread2.start();

        thread1.join();
        thread2.join();
    }
}

2.2. 无法停止的原因分析

  1. Thread1 从主内存把 isRunning 这个变量加载到工作内存中,值为 true 所以一直运行
  2. Thread2 从主内存把 isRunning 这个变量加载到工作内存中,值为 true 改为 false,写回工作内存,再写回主内存
  3. Thread1 一直从工作内存中读取这个变量,一直为 true,所以还是无法停止运行

2.3. 解决方法

将 isRunning 使用 volatile 修饰

java 复制代码
public class VolatileTest
{
    private static volatile boolean isRunning = true;

    public static void main(String[] args) throws InterruptedException
    {
        Thread thread1 = new Thread(()->{
            System.out.println("thread1 is running");
            while (isRunning)
            {

            }
            System.out.println("thread1 will be stopped");
        });
        thread1.start();

        Thread.sleep(1000);

        Thread thread2 = new Thread(()->{
            System.out.println("thread2 is running");

            isRunning = false;

            System.out.println("thread2 change isRunning flag");
        });
        thread2.start();

        thread1.join();
        thread2.join();
    }
}

2.4. volatile vs synchronized

volatile synchronized
内存模型三性 可见性、有序性 可见性、有序性、原子性
是否造成线程阻塞【重量级别】 不会
应用范围 变量级别 变量、方法、类级别

3. 汇编源码实验

3.1. 下载编译 hsdis-amd64.dll

参考How to build hsdis-amd64.dll and hsdis-i386.dll on Windows或者hsdis-amd64.7z

3.2. 放入 JRE bin 目录下

3.3. 对比实验

  • 有 volatile
java 复制代码
public class TestVolatile
{
    private static volatile int i = 0;
    public static void main(String[] args)
    {
        test();
    }

    private static void test()
    {
        i++;
    }
}
  • 没有 volatile
java 复制代码
public class TestVolatile
{
    private static int i = 0;
    public static void main(String[] args)
    {
        test();
    }

    private static void test()
    {
        i++;
    }
}

3.4. 加上 jvm 参数运行

jvm 复制代码
-server -Xcomp -XX:+UnlockDiagnosticVMOptions -XX:-Inline -XX:CompileCommand=print,*TestVolatile.test

使用 IDEA 的话如下图:

3.5. 输出结果对比

结果如附件:

4. 根据实验结果分析原理

从汇编语言层面看,有 volatile 的结果比没有 volatile 的多了一个指令:lock addl $0x0,(%rsp) ,这条指令起到内存屏障的作用

  1. 禁止屏障两边的指令重排序

  2. 强制把写缓冲区/高速缓存中的脏数据等写回主内存,让缓存中相应的数据失效

4.1. 可见性

根据内存屏障的作用 2 可以实现可见性,表现如下

  • volatile 写会把数据同时写入主内存,并让其他线程对这个数据的工作内存失效
  • 其他线程 volatile 读的时候就需要去主内存中读取

4.2. 有序性

根据内存屏障的作用 1 可以实现有序性,表现如下

在 volatile 写之前插入释放屏障 【LoadStore+StoreStore】使得该屏障之前的任何读写操作都先于这个 volatile 写被提交;

在 volatile 读之后插入获取屏障【LoadLoad+LoadStore】使得这个 volatile 读操作先于该屏障之后的任何读写操作被提交。

5. 参考