专栏首页ytaovolatile 手摸手带你解析

volatile 手摸手带你解析

前言

volatile 是 Java 里的一个重要的指令,它是由 Java 虚拟机里提供的一个轻量级的同步机制。一个共享变量声明为 volatile 后,特别是在多线程操作时,正确使用 volatile 变量,就要掌握好其原理。

特性

volatile 具有可见性有序性的特性,同时,对 volatile 修饰的变量进行单个读写操作是具有原子性

这几个特性到底是什么意思呢?

  • 可见性: 当一个线程更新了 volatile 修饰的共享变量,那么任意其他线程都能知道这个变量最后修改的值。简单的说,就是多线程运行时,一个线程修改 volatile 共享变量后,其他线程获取值时,一定都是这个修改后的值。
  • 有序性: 一个线程中的操作,相对于自身,都是有序的,Java 内存模型会限制编译器重排序和处理器重排序。意思就会说 volatile 内存语义单个线程中是串行的语义。
  • 原子性: 多线程操作中,非复合操作单个 volatile 的读写是具有原子性的。

可见性

可见性是在多线程中保证共享变量的数据有效,接下来我们通过有 volatile 修饰的变量和无 volatile 修饰的变量代码的执行结果来做对比分析。

无 volatile 修饰变量

以下是没有 volatile 修饰变量代码,通过创建两个线程,来验证 flag 被其中一个线程修改后的执行情况。

/**
 * Created by YANGTAO on 2020/3/15 0015.
 */
public class ValatileDemo {

    static Boolean flag = true;

    public static void main(String[] args) {

        // A 线程,判断其他线程修改 flag 之后,数据是否对本线程有效
        new Thread(() -> {
            while (flag) {

            }
            System.out.printf("********** %s 线程执行结束!**********", Thread.currentThread().getName());
        }, "A").start();



        // B 线程,修改 flag 值
        new Thread(() -> {
            try {
                // 避免 B 线程比 A 线程先运行修改 flag 值
                TimeUnit.SECONDS.sleep(1);
                flag = false;
                // 如果 flag 值修改后,让 B 线程先打印信息
                TimeUnit.SECONDS.sleep(2);

                System.out.printf("********** %s 线程执行结束!**********", Thread.currentThread().getName());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "B").start();

    }
}

上面代码中,当 flag 初始值 true,被 B 线程修改为 false。如果修改后的值对 A 线程有效,那么正常情况下 A 线程会先于 B 线程结束。执行结果如下:

执行结果是:当 B 线程执行结束后, flag=false并未对 A 线程生效,A 线程死循环。

volatile 修饰变量

在上述代码中,当我们把 flag 使用 volatile 修饰:

/**
 * Created by YANGTAO on 2020/3/15 0015.
 */
public class ValatileDemo {

    static volatile Boolean flag = true;

    public static void main(String[] args) {

        // A 线程,判断其他线程修改 flag 之后,数据是否对本线程有效
        new Thread(() -> {
            while (flag) {

            }
            System.out.printf("********** %s 线程执行结束!**********", Thread.currentThread().getName());
        }, "A").start();

        
        // B 线程,修改 flag 值
        new Thread(() -> {
            try {
                // 避免 B 线程比 A 线程先运行修改 flag 值
                TimeUnit.SECONDS.sleep(1);
                flag = false;
                // 如果 flag 值修改后,让 B 线程先打印信息
                TimeUnit.SECONDS.sleep(2);

                System.out.printf("********** %s 线程执行结束!**********", Thread.currentThread().getName());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "B").start();

    }
}

执行结果:

B 线程修改 flag 值后,对 A 线程数据有效,A 线程跳出循环,执行完成。所以 volatile 修饰的变量,有新值写入后,对其他线程来说,数据是有效的,能被其他线程读到。

主内存和工作内存

上面代码中的变量加了 volatile 修饰,为什么就能被其他线程读取到,这就涉及到 Java 内存模型规定的变量访问规则。

  • 主内存:主内存是机器硬件的内存,主要对应Java 堆中的对象实例数据部分。
  • 工作内存:每个线程都有自己的工作内存,对应虚拟机栈中的部分区域,线程对变量的读/写操作都必须在工作内存中进行,不能直接读写主内存的变量。

上面 无volatile修饰变量部分的代码执行示意图如下:

当 A 线程读取到 flag 的初始值为 true,进行 while 循环操作,B 线程将工作内存 B 里的 flag 更新为 false,然后将值发送到主内存进行更新。随后,由于此时的 A 线程不会主动刷新主内存中的值到工作内存 A 中,所以线程 A 所取得 flag 值一直都是 true,A 线程也就为死循环不会停止下来。

上面 volatile修饰变量部分的代码执行示意图如下:

当 B 线程更新 volatile 修饰的变量时,会向 A 线程通过线程之间的通信发送通知(JDK5 或更高版本),并且将工作内存 B 中更新的值同步到主内存中。A 线程接收到通知后,不会再读取工作内存 A 中的值,会将主内存的变量通过主内存和工作内存之间的交互协议,拷贝到工作内存 A 中,这时读取的值就是线程 A 更新后的值 flag=false。整个变量值得传递过程中,线程之间不能直接访问自身以外的工作内存,必须通过主内存作为中转站传递变量值。在这传递过程中是存在拷贝操作的,但是对象的引用,虚拟机不会整个对象进行拷贝,会存在线程访问的字段拷贝。

有序性

volatile 包含禁止指令重排的语义,Java 内存模型会限制编译器重排序和处理器重排序,简而言之就是单个线程内表现为串行语义。那什么是重排序?重排序的目的是编译器和处理器为了优化程序性能而对指令序列进行重排序,但在单线程和单处理器中,重排序不会改变有数据依赖关系的两个操作顺序。比如:

/**
 * Created by YANGTAO on 2020/3/15 0015.
 */
public class ReorderDemo {

    static int a = 0;

    static int b = 0;

    public static void main(String[] args) {
        a = 2;
        b = 3;
    }
}


// 重排序后:

public class ReorderDemo {

    static int a = 0;

    static int b = 0;

    public static void main(String[] args) {
        b = 3;  // a 和 b 重排序后,调换了位置
        a = 2;
    }
}

但是如果在单核处理器和单线程中数据之间存在依赖关系则不会进行重排序,比如:

/**
 * Created by YANGTAO on 2020/3/15 0015.
 */
public class ReorderDemo {

    static int a = 0;

    static int b = 0;

    public static void main(String[] args) {
        a = 2;
        b = a;
    }
}

// 由于 a 和 b 存在数据依赖关系,则不会进行重排序

volatile 实现特有的内存语义,Java 内存模型定义以下规则(表格中的 No 代表不可以重排序):

Java 内存模型在指令序列中插入内存屏障来处理 volatile 重排序规则,策略如下:

  • volatile 写操作前插入一个 StoreStore 屏障
  • volatile 写操作后插入一个 StoreLoad 屏障
  • volatile 读操作后插入一个 LoadLoad 屏障
  • volatile 读操作后插入一个 LoadStore 屏障

该四种屏障意义:

  • StoreStore:在该屏障后的写操作执行之前,保证该屏障前的写操作已刷新到主内存。
  • StoreLoad:在该屏障后的读取操作执行之前,保证该屏障前的写操作已刷新到主内存。
  • LoadLoad:在该屏障后的读取操作执行之前,保证该屏障前的读操作已读取完毕。
  • LoadStore:在该屏障后的写操作执行之前,保证该屏障前的读操作已读取完毕。

原子性

前面有提到 volatile 的原子性是相对于单个 volatile 变量的读/写具有,比如下面代码:

/**
 * Created by YANGTAO on 2020/3/15 0015.
 */
public class AtomicDemo {

    static volatile int num = 0;

    public static void main(String[] args) throws InterruptedException {

        final CountDownLatch latch = new CountDownLatch(10);
        for (int i = 0; i < 10; i++) {  // 创建 10 个线程
            new Thread(() -> {
                for (int j = 0; j < 1000; j++) {    // 每个线程累加 1000
                    num ++;
                }
                latch.countDown();
            }, String.valueOf(i+1)).start();
        }

        latch.await();

        // 所有线程累加计算的数据
        System.out.printf("num: %d", num);
    }
}

上面代码中,如果 volatile 修饰 num,在 num++ 运算中能持有原子性,那么根据以上数量的累加,最后应该是 num:10000。代码执行结果:

结果与我们预计数据的相差挺多,虽然 volatile 变量在更新值的时候回通知其他线程刷新主内存中最新数据,但这只能保证其基本类型变量读/写的原子操作(如:num = 2)。由于 num++是属于一个非原子操作的复合操作,所以不能保证其原子性。

使用场景

  1. volatile 变量最后的运算结果不依赖变量的当前值,也就是前面提到的直接赋值变量的原子操作,比如:保存数据遍历的特定条件的一个值。
  2. 可以进行状态标记,比如:是否初始化,是否停止等等。

总结

volatile 是一个简单又轻量级的同步机制,但在使用过程中,局限性比较大,要想使用好它,必须了解其原理及本质,所以在使用过程中遇到的问题,相比于其他同步机制来说,更容易出现问题。但使用好 volatile,在某些解决问题上能获取更佳的性能。

关注【ytao】,更多原创好文

本文分享自微信公众号 - ytao(ytao-blog),作者:ytao

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2020-03-16

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Java 多线程中使用 JDK 自带工具类实现计数器

    在实际开发过程中,经常遇到需要多线程并行的业务,最后需要进行将各个线程完成的任务进行汇总,但主线程一般会早于子线程结束,如果要想等各个子线程完成后再继续运行主线...

    ytao
  • Java 线程基础,从这篇开始

    一个独立运行的程序是一个进程,一个进程中可以包含一个或多个线程,每个线程都有属于自己的一些属性,如堆栈,计数器等等。同时,一个线程在一个时间点上只能运行在一个 ...

    ytao
  • Java 线程通信之 wait/notify 机制

    Java 线程通信是将多个独立的线程个体进行关联处理,使得线程与线程之间能进行相互通信。比如线程 A 修改了对象的值,然后通知给线程 B,使线程 B 能够知道线...

    ytao
  • 你真的会用volatile吗

    volatile的概念volatile详解什么时候需要使用volatilevolatile在标准库里的应用volatile会降低程序执行的效率volatile不...

    用户7634691
  • Java并发编程之volatile关键字解析

    文章来源:http://www.cnblogs.com/dolphin0520/p/3920373.html

    java思维导图
  • Java并发编程之volatile关键字解析

    volatile这个关键字可能很多朋友都听说过,或许也都用过。在Java 5之前,它是一个备受争议的关键字,因为在程序中使用它往往会导致出人意料的结果。在Jav...

    哲洛不闹
  • 厕读:每日一题,面试无忧

    9. volatile关键字是否能保证线程安全?() 答案:不能 解析:volatile关键字用在多线程同步中,可保证读取的可见性,JVM只是保证从主内存加载到...

    ImportSource
  • 谈谈面试官最爱的volatile关键字

    在Java相关的岗位面试中,很多面试官都喜欢考察面试者对Java并发的了解程度,而以volatile关键字作为一个小的切入点,往往可以一问到底,把Java内存模...

    美的让人心动
  • 万字长文说透 volatile 的原理和面试知识点!

    volatile 是一种轻量且在有限的条件下线程安全技术,它保证修饰的变量的可见性和有序性,但非原子性。相对于 synchronize 高效,而常常跟 sync...

    业余草
  • 面试官最爱的volatile关键字

    美的让人心动

扫码关注云+社区

领取腾讯云代金券