volatile原理剖析和实例讲解

一、是什么

volatile是Java的一个关键字,是Java提供的一种轻量级的同步机制,

二、能做什么

保证了不同线程对这个变量进行操作时的可见性,有序性。

三、可见性

可见性主要是指一个线程修改了共享变量的值,另一个线程可以看见。但是每一个线程都是要自己的工作内存,那么要如何实现线程之间的可见内?使用volatile关键字就可以有效的解决可见性问题。下面用一个例子来解释一下线程可见性的问题。

public class VolatileDemo {
    static boolean flag = false;
    public static void main(String[] args) {
        //启用一个线程,如果主线程修改该布尔值为true则退出循环,如果线程中对于修改的值不可间,那么程序会一直在循环中
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "线程启动,falg为" + flag);
            while (!flag) {

            }
            System.out.println(Thread.currentThread().getName() + "退出循环");
        },"t1").start();
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        //main线程把布尔值修改为true
        flag = true;
        System.out.println(Thread.currentThread().getName()+"修改flag为" +flag);
    }
}

从上面的代码可以知道如果t1线程可以知道main线程的修改,那么t1线程中的for循环就可以正常退出,如果main线程的修改t1不可见,那么t1线程的循环就无法退出。如果我们在flag变量添加volatile关键字,如下所示。

public class VolatileDemo {
    //添加volatile关键字
    static volatile boolean flag = false;
    public static void main(String[] args) {
        //启用一个线程,如果主线程修改该布尔值为true则退出循环,如果线程中对于修改的值不可间,那么程序会一直在循环中
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "线程启动,falg为" + flag);
            while (!flag) {

            }
            System.out.println(Thread.currentThread().getName() + "退出循环");
        },"t1").start();
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        //main线程把布尔值修改为true
        flag = true;
        System.out.println(Thread.currentThread().getName()+"修改flag为" +flag);
    }
}

那么没有加volatile关键字线程t1中为何看不到被主线程main修改false的flag的值?

可能原因

  • 主线程修改了flag之后没有将其刷新到主内存所以t1线程看不到。
  • 主线程将flag刷新到了主内存,但是t1一直读取的是自己工作内存中的flag的值,没有去主内存中更新获取flag最新的值。

使用volatile修饰共享变量有以下特点

  • 线程中读取的时候,每次读取都会去主内存中读取共享变量最新的值,然后将其复制到工作内存。
  • 线程中修改了工作内存中变量的副本,修改之后会立即刷新到主内存。

四、有序性

指令重排

为了提高性能,在遵守 as-if-serial 语义(即不管怎么重排序,单线程下程序的执行结果不能被改变。编译器,runtime 和处理器都必须遵守。)的情况下,编译器和处理器常常会对指令做重排序。

一般重排序可以分为如下三种类型:

  • 编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
  • 指令级并行的重排序。现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
  • 内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。

重排序的执行流程为执行流程
在这里插入图片描述

数据依赖性

若两个操作访问同一变量,且这两个操作中有一个为写操作,此两个操作间就存在数据依赖性。

下面用两个案列来说明什么是数据依赖性

public class volatileDemo01 {
    public static void main(String[] args) {
        int a = 1;
        int b = 2;
        int c = a + b;
        System.out.println(c);
    }
}
// 重排后的代码
public class volatileDemo01 {
    public static void main(String[] args) {
        int b = 2;
        int a = 1;
        int c = a + b;
        System.out.println(c);
    }
}

变量a和变量b调换位置,无论怎么调换都不会影响程序的最终结果所以就不存在数据依赖性。

名称代码示例说明
写后读a=1;b=a;写一个变量之后,再读这个位置
写后写a=1;a=2;写一个变量之后,再写这个变量
读后写a=b;b=1;读一个变量之后,再写这个变量

上面三种情况是存在数据依赖关系的,只要重排序两个操作的执行顺序,程序的执行结果就会被改变。

内存屏障

为了实现 volatile 内存语义(即内存可见性),JMM 会限制特定类型的编译器和处理器重排序。为此,JMM 针对编译器制定了 volatile 重排序规则表,如下所示:

是否重排序第二次操作普通读/写第二次操作volatile读第二次操作volatile写
第一次操作普通读/写
第一次操作volatile读
第一次操作volatile写

上面表格的内容可以总结为以下3点

  • 当第一个操作为volatile读时,不论第二个操作是什么,都不能重排序。这个操作保证了volatile读之后的操作不会被重排到volatile之前
  • 当第二个操作为volatile写是,不论第一个操作是什么,都不能重排序。这个操作保证了volatile写之前的操作不会被重排到volatile之后
  • 当第一个操作为volatile写是,第二个操作为volatile读时,不能重排。

内存屏障是一组处理器指令,它的作用是禁止指令重排序和解决内存有序性的问题

JMM把内存屏障指令分为四类

  • 在每一个volatile写操作前面插入一个StoreStore屏障:StoreStore屏障可以保证在volatile写之前,其前面的所有普通写操作都已刷新到主内存。

  • 在每一个volatile写操作后面插入一个StoreLoad屏障:StoreLoad屏障的作用是避免volatile写与后面可能有的volatile读/写操作重排序。

  • 在每一个volatile读操作后面插入一个LoadLoad屏障:LoadLoad屏障用来禁止处理器把上面的volatile读与下面的普通读重排序。

  • 在每一个volatile读操作后面插入一个LoadStore屏障:LoadStore屏障用来禁止处理器把上面的volatile读与下面的普通写重排序。

class VolatileTest{
    int i=0;
    // 没有加volatile多线程的情况下会发生指令重排
    boolean flag = false;
    public void set(){
        i=2;
        flag=true;
    }
    public void get(){
        if (flag){
            System.out.println(i);
        }
    }
}

加上volatile该程序就变成线程安全的程序了,我们分析以下这个代码。

class VolatileTest{
    int i=0;
    // 没有加volatile多线程的情况下会发生指令重排
    boolean flag = false;
    public void set(){
        i=2;
        flag=true;
    }
    public void get(){
        if (flag){
            System.out.println(i);
        }
    }
}

在这里插入图片描述

左边是set方法的分析,右边是get方法的分析。因为给flag添加了volatile关键字,所以当对于flag的读写都会添加相应的屏障,在每一个volatile写操作后面都会插入一个StoreLoad屏障,volatile写不能与后面可能有的volatile读/写操作重排序volatile前面插入一个StoreStore屏障,可以保证在volatile写之前,其前面的所有普通写操作都已刷新到主内存。volatile读后面会添加LoadLoad屏障和LoadStore屏障。LoadLoad屏障用来禁止处理器把上面的volatile读与下面的普通读重排序

LoadStore屏障用来禁止处理器把上面的volatile读与下面的普通写重排序

五、无原子性

下面我将用一个例子来说明volatile的无原子性

class Number {
    volatile int num = 0;
    public void add(){
        num++;
    }
}
public class volatileDemo01 {
    public static void main(String[] args) {
        Number number = new Number();
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                for (int j = 0; j < 1000; j++) {
                    number.add();
                }
            }).start();
        }
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println(number.num);
    }
}

上面这段代码如果是线程安全的话就会输出10000,但是由于volatile并不能保证原子性所以程序的输出结果每次基本上都不一样。

在这里插入图片描述

对于volatile变量具备可见性,JVM只是保证从主内存加载到线程工作内存值是最新的,也仅是数据加载时最新的。但是多线程环境下,“数据计算”和“数据赋值”操作可能多次出现,若数据在加载之后,若主内存volatile修饰变量发生修改之后,线程工作内存中的操作将会作废去读内存最新值,操作出现写丢失问题。各线程私有内存和主内存公共内存中变量不同步,进而导致数据不一致。由此可见volatile解决的是变量读时的可见性问题,但无法保证原子性,对于多线程修改主内存共享变量的场景必须使用加锁同步

六、使用

volatile的运用

  • 当读远多于写,结合使用内部锁和volatile变量来减少同步的开销。

    public class UseVolatileDemo {
       private  volatile  int value;
       // 利用volatile保证读取操作的可见性
       public int getValue() {
           return value;
       }
       // 利用synchronized保证复合操作的原子性
       public synchronized  int incrementAndGet() {
           return value++;
       }
    }
    
  • 状态标志,判断业务是否结束。

    public class VolatileDemo {
        static volatile boolean flag = false;
        public static void main(String[] args) {
            //启用一个线程,如果主线程修改该布尔值为true则退出循环,如果线程中对于修改的值不可间,那么程序会一直在循环中
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + "线程启动,falg为" + flag);
                while (!flag) {
    
                }
                System.out.println(Thread.currentThread().getName() + "退出循环");
            },"t1").start();
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            //main线程把布尔值修改为true
            flag = true;
            System.out.println(Thread.currentThread().getName()+"修改flag为" +flag);
        }
    }
    
  • DCL双端锁的发布。

    public class SafeDoubleCheckSingleton {
        private volatile static SafeDoubleCheckSingleton singleton;
        public SafeDoubleCheckSingleton() {
        }
        // 双重锁设计
        public static SafeDoubleCheckSingleton getInstance() {
            if (singleton == null) {
                synchronized (SafeDoubleCheckSingleton.class) {
                    if (singleton == null) {
                        // 利用volatile,禁止“初始化对象(2)”和“设置singleton指向内存空间(3)"的重排序
                        singleton = new SafeDoubleCheckSingleton();
                    }
                }
            }
            return singleton;
        }
    }
    

    如果没有volatile在多线程的环境下该单列模式可能会产生线程安全问题。

使用限制

由于volatile变量只能保证可见性,在不符合以下两条规则的运算场景中,我们仍然要通过加锁来保证原子性

  • 运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值。
  • 变量不需要与其他的状态变量共同参与不变约束。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/37074.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

RPC 框架架构设计

RPC 框架架构设计 RPC 又称远程过程调用&#xff08;Remote Procedure Call&#xff09;&#xff0c;用于解决分布式系统中服务之间的调用问题。通俗地讲&#xff0c;就是开发者能够像调用本地方法一样调用远程的服务。下面我们通过一幅图来说说 RPC 框架的基本架构。 RPC 框架…

Python_与redis数据库交互

目录 redis模块的使用 连接方式 连接池 操作 设置值 获取值 管道 事务 源码等资料获取方法 python可以使用redis模块来跟redis交互 redis模块的使用 安装模块: pip3 install redis 导入模块&#xff1a;import redis 连接方式 严格连接模式&#xff1a;rredis.St…

40.RocketMQ之高频面试题大全

消息中间件如何选型 RabbitMQ erlang开发&#xff0c;对消息堆积的支持并不好&#xff0c;当大量消息积压的时候&#xff0c;会导致 RabbitMQ 的性能急剧下降。每秒钟可以处理几万到十几万条消息。 RocketMQ java开发&#xff0c;面向互联网集群化功能丰富&#xff0c;对在线业…

Win10安全中心怎么关闭?Win10安全中心关闭方法

Win10安全中心怎么关闭&#xff1f;关闭Win10的安全中心可以帮助用户自定义系统的安全和防护设置&#xff0c;但有些用户不知道怎么操作才能关闭安全中心&#xff0c;首先用户需要打开Win10电脑的设置选项&#xff0c;接着打开安全中心&#xff0c;然后关掉安全中心的实时保护、…

SQL力扣练习(六)

目录 1. 部门工资前三高的所有员工(185) 题解一(dense_rank()窗口函数&#xff09; 题解二&#xff08;自定义函数&#xff09; 2.删除重复的电子邮箱(196) 题解一 题解二&#xff08;官方解析&#xff09; 3.上升的温度(197) 解法一&#xff08;DATEDIFF()&#xff09;…

非主流币波段策略

数量技术宅团队在CSDN学院推出了量化投资系列课程 欢迎有兴趣系统学习量化投资的同学&#xff0c;点击下方链接报名&#xff1a; 量化投资速成营&#xff08;入门课程&#xff09; Python股票量化投资 Python期货量化投资 Python数字货币量化投资 C语言CTP期货交易系统开…

探究Vue源码:mustache模板引擎(5) 对比rollup与webpack,在本地搭建webpack环境

好 从本文开始 我们就来手写一下mustache这个库 他是模板引擎的一个祖先 将模板字符串编译成一个dom字符串 就是它的思想&#xff0c;这也是一个具有跨时代意义的思想 这里的话 我们还是搭一个 webpack 的项目环境 这里值得一提的是 mustache 他官方是通过rollup来进行打包的 …

【Docker】Docker的部署含服务和应用、多租环境、Linux内核的详细介绍

前言 Docker 是一个开源的应用容器引擎&#xff0c;让开发者可以打包他们的应用以及依赖包到一个可移植的容器中,然后发布到任何流行的Linux或Windows操作系统的机器上,也可以实现虚拟化,容器是完全使用沙箱机制,相互之间不会有任何接口。 &#x1f4d5;作者简介&#xff1a;热…

速通matplotlib库

速通matplotlib库 前言 ​ 最近在复习之前学习过的知识点&#xff0c;因此想到把学过的总结一下&#xff0c;方便后面再次复习&#xff0c;所以有了这个系列。 说明 ​ 由于标题写的是“速通”&#xff0c;因此我的想法是可以让大家看完这篇文章&#xff0c;可以上手matplotlib…

爬虫正常用哪种代理比较好?

目录 爬虫使用哪种代理IP 爬虫使用代理IP时需要考虑什么 爬虫怎么使用代理IP 爬虫使用代理IP示例代码 爬虫使用哪种代理IP 在使用代理IP进行爬虫时&#xff0c;以下几种类型的代理被认为是比较好的选择&#xff1a; 1. 高匿名代理&#xff1a;高匿名代理隐藏了真实的IP地址…

简要介绍 | 边缘计算:原理,研究现状与未来展望

注1&#xff1a;本文系“简要介绍”系列之一&#xff0c;仅从概念上对边缘计算进行非常简要的介绍&#xff0c;不适合用于深入和详细的了解。 边缘计算&#xff1a;原理&#xff0c;研究现状与未来展望 What is Edge Computing? | Moving Intelligence to the Edge 一、背景介…

漏刻有时数据可视化Echarts组件开发(27):端午地图粽情之你的家乡吃甜还是吃咸?

端午地图粽情之你的家乡吃甜还是吃咸&#xff1f; 前言Echarts创意来源Echarts核心代码1.引入外部文件2.构建HTML容器3.Echarts组件开发预置各省数据初始化DOM配置选项geo组件series组件自适应浏览器完整option选项配置代码 前言 中国各地对粽子的口味偏好存在一定的差异&…

【openGauss数据库】---设置开机自启动openGauss数据库服务

【openGauss数据库】---设置开机自启动openGauss数据库服务 &#x1f53b; 一、openGauss 自定义服务的配置文件了解&#x1f53b; 二、设置openGauss 开机自启动&#x1f53b; 三、总结—温故知新 &#x1f448;【上一篇】 &#x1f496;The Begin&#x1f496; 点点关注&am…

css animation 鼠标移入暂停会抖动

如图 实现一个赞助商横向滚动列表墙&#xff0c; 上下两排向右滚动&#xff0c;中间向左滚动&#xff0c;鼠标移入暂停当前行。 实现&#xff1a; // 使用animation.moving {animation: move 20s linear infinite; }keyframes move {0% {}100% {transform: translateX(-50%);…

【深入浅出 Spring Security(十三)】使用 JWT 进行前后端分离认证(附源码)

使用 JWT 进行前后端分离认证 一、JWT 的简单介绍二、使用 JWT 进行安全认证后端结合SpringSecurity实现前端Vue3结合Pinia、Axios实现测试结果 一、JWT 的简单介绍 JWT 全称 Java web Token&#xff0c;在此所讲述的是 JWT 用于身份认证&#xff0c;用服务器端生成的JWT去替代…

CV多模态和AIGC的原理解析:从CLIP、BLIP到Stable Diffusion、Midjourney

前言 终于开写本CV多模态系列的核心主题&#xff1a;stable diffusion相关的了&#xff0c;为何执着于想写这个stable diffusion呢&#xff0c;源于三点 去年stable diffusion和midjourney很火的时候&#xff0c;就想写&#xff0c;因为经常被刷屏&#xff0c;但那会时间错不…

入门车载以太网

前言 近些年来,随着为了让汽车更加安全、智能、环保等,一系列的高级辅助驾驶功能喷涌而出。未来满足这些需求,就对传统的电子电器架构带来了严峻的考验,需要越来越多的电子部件参与信息交互,导致对网络传输速率,稳定性,负载率等方面都提出了更为严格的挑战。 除此以外…

34.RocketMQ之Broker端消息存储流程详解

highlight: arduino-light Broker消息存储概要设计 RocketMQ主要存储的文件包括Commitlog文件&#xff0c;ConsumeQueue文件&#xff0c;IndexFile文件。 RMQ把所有主题的消息存储在同一个文件中&#xff0c;确保消息发送时顺序写文件。 为了提高消费效率引入了ConsumeQueue消息…

云原生TDengine-v3.0部署手册

云原生TDengine-v3.0部署手册 一、管理namespace1.1 创建namespace1.2 namespaces列表 二、配置3份yaml文件2.1 tdengine3-storage-class.yaml2.2 taosd-service.yaml2.3 taosd-tdengine.yaml 三、服务部署3.1 部署StorageClass3.2 部署Service3.3 部署StatefulSet3.4 查看启动…

Flask新手教程

Flask简介 Flask是一个轻量级的可定制框架&#xff0c;使用Python语言编写&#xff0c;较其他同类型框架更为灵活、轻便、安全且容易上手。 Flask 可以很好地结合MVC模式进行开发&#xff0c;开发人员分工合作&#xff0c;小型团队在短时间内就可以完成功能丰富的中小型网站或…