关于我们

质量为本、客户为根、勇于拼搏、务实创新

< 返回新闻公共列表

volatile 解析

发布时间:2020-03-25 00:00:00

前言

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,在某些解决问题上能获取更佳的性能。


相关阅读

云计算的三个优点选取小程序服务商时的注意事项不同数据库的不同区别什么是VPS主机它的优势是什么?你知道5G的优点和缺点吗?云南三级分销商城开发的目的和对企业的价值云南网站优化的3个方面和网页的优化我们晋级拉!!!云南网站链接维护的具体方法云南网站应该如何做优化云南网站维护的主要内容云南云服务器和虚拟主机的操作区别云服务器部署和注意点云南服务网器托管应该选择怎样的机房云南虚拟建设网站主机的优点和缺点云服务器和物理服务器的区别云服务器有哪些优势C# List用法 List介绍C#和Java有什么不同PHP的优点和缺点智慧新餐饮和传统餐饮的区别云数据库对比传统数据库有哪些优点裸金属服务器是什么它的作用是什么白盒测试的特点js中添加scriptjs中[]、{}、()的用法和区别php 字符串的整型转换ipa如何安装到苹果手机邓白氏码是什么?iOS开发者账号到期续费教程在C#中有哪些引用类型和值类型小程序搭建时需要准备些什么云数据库对比传统数据库的优势什么样的企业适合SEMjs中的常见错误C#数据类型转换字符串与数值之间的转换C#的几种循环遍历方式物联网是什么它和互联网又有什么区别?云南网站建设时应该注意些什么云计算是什么?它能干些什么?展望未来5G会给我们的生活带来哪些影响C#常见的几种报错类别C#的学习流程有哪些dedecms 绑定二级域名的正确方法SEM是什么它和SEO之间是什么关系?云南新餐饮料模式是如何运行的?云南网站建设初期应该注意哪些问题云南网站建设中原创文章对网站起什么作用Javascript中如何中断forEach循环云南零售小程序前景怎么样?如何优化WEB应用数据库访问慢的问题?Javascript中用let和var声明变量的区别是什么redis 的主从复制和哨兵?递归和迭代有何区别?<input> 标签的 readonly 属性怎么用?云南网站建设中网页版商城如何保证网站安全云南企业官网如何推广云南企业网站建设为什么把文章类型的栏目排在前面云南餐饮小程序能带给餐饮业什么?云南网站建设需要注意什么地方云南云服务器配置如何选择合适的云南网站建设和网站设计有何区别?云南网站建设—企业官网的好处在哪?云南做小程序的优势在哪?为什么建议做小程序?网站为什么要配置ssl证书?网站为什么要做seo,做seo有什么好处?微信小程序_企业付款到零钱 API 开发如何利用缓存提高asp.net网站访问速度中小型企业如何选择服务器配置?如何优化中小型企业网站C#.net做网站后台 需要记录日志基于asp.net企业门户网站设计霓裳民族服装seo优化分析建议Javascript的函数封装认识及使用Ajax请求中data与后端的交互有哪几种方法JavaScript如何实现组合模式???SQL之子查询的基本用法有哪些?什么是静态测试、动态测试、黑盒测试、白盒测试、α测试 β测试?C# 引入委托的目的是什么c# 委托的本质是什么C# DataGridView添加新行的2个方法C#支付宝扫码支付代码完整版c# MVC 微信支付教程系列之扫码支付代码实例Redis基础通用操作指令有哪些?String、StringBuffer和StringBuilder的各自用法常见的一些Dos命令有哪些?如何在C#中复制一个Windows窗体类前端js中的运算符的种类,=、==与===的有何区别?网站前端怎么设置页面禁止转载?解决XSS脚本攻击恶意代码的方法你知道?javascript 之 apply()、call() 探索net core实现跨域ASP.NET Core程序部署到LinuxASP.NET Core使用AutoMapper.NET Core:处理全局异常.NET Core在ASP.NET Core WebApi中使用CookieASP.NET Core:ASP.NET Core中使用NLog记录日志如何重启iis服务器SQL Server代理服务无法启动怎么办js 判断手机版与PC版自适应跳转升级版 PC版手机版界面对应跳转
/template/Home/Zkeys/PC/Static