volatile原理剖析和实例讲解

news2024/11/25 15:32:59

一、是什么

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变量只能保证可见性,在不符合以下两条规则的运算场景中,我们仍然要通过加锁来保证原子性

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

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

PreparedStatement 相比于 Statement的优点

PreparedStatement 相比于 Statement&#xff0c;有以下几个优点&#xff1a; 1. 预编译&#xff1a;PreparedStatement 对象在执行 SQL 语句之前会进行预编译&#xff0c;这意味着数据库管理系统可以提前解析和编译 SQL 语句&#xff0c;以优化执行计划&#xff0c;从而提高查…

【C语言初阶(15)】操作符1

文章目录 Ⅰ操作符的分类Ⅱ 算术操作符Ⅲ 移位操作符⒈左移操作符⒉右移操作符 Ⅳ 位操作符⒈按位取⒉按位与⒊按位异或⒋按位或⒌位操作符练习题 Ⅴ赋值操作符Ⅵ 单目操作符 Ⅰ操作符的分类 算术操作符移位操作符位操作符赋值操作符单目操作符关系操作符逻辑操作符条件操作符…

【ElasticSearch】ES集群搭建、监控、故障转移

文章目录 1、ES集群介绍2、搭建ES集群3、集群状态监控4、集群职责及脑裂5、分布式新增和查询流程6、ES故障转移 1、ES集群介绍 单机的ES做数据存储与搜索&#xff0c;必然面临两个问题&#xff1a; 海量数据存储问题单点故障问题 因此&#xff0c;考虑使用ES集群&#xff1a…

AIGC浪潮席卷,亚马逊云科技携手海尔设计、Nolibox加速工业设计转型

从机器学习算法到深度学习再到强化学习&#xff0c;AI创新浪潮奔流不息。而AIGC&#xff08;AI-generated Content&#xff0c;人工智能生成内容&#xff09;的到来&#xff0c;更是让AI成为众多企业的得力助手&#xff0c;开拓了文本、图像、音视频等领域的天花板。 在洞悉到…

x86架构ubuntu18下运行mgba模拟器

0. 环境 i7 ubuntu18 1. 准备源码 到https://github.com/mgba-emu/mgba/releases下载源码 mgba-0.10.2.tar.gz tar -zvxf mgba-0.10.2.tar.gz cd mgba0.10.2 2. 编译 mkdir build cd build cmake -DCMAKE_INSTALL_PREFIX/home/xxjianvm/work/mgba-0.10.2/install .. make -j…

PROFINET转TCP/IP网关TCP/IP协议的含义是

大家好&#xff0c;今天要和大家分享一款自主研发的通讯网关&#xff0c;远创智控YC-PN-TCPIP。这款网关可是集多种功能于一身&#xff0c;PROFINET从站功能&#xff0c;让它在通讯领域独领风骚。想知道这款网关如何实现PROFINET和TCP/IP网络的连接吗&#xff1f;一起来看看吧&…

【GeoDa实用技巧100例】004:绘制长沙市宾馆热度图

文章目录 一、加载宾馆分布数据二、绘制热度图三、加载范围数据四、加载底图数据 一、加载宾馆分布数据 加载专栏配套的案例数据data004.rar中的长沙市宾馆酒店.shp&#xff0c;如下图&#xff0c;选择Shapefile格式&#xff1a; 选择长沙市宾馆酒店&#xff0c;加载如下&…

完全平方数(力扣)动态规划 JAVA

给你一个整数 n &#xff0c;返回 和为 n 的完全平方数的最少数量 。 完全平方数 是一个整数&#xff0c;其值等于另一个整数的平方&#xff1b;换句话说&#xff0c;其值等于一个整数自乘的积。例如&#xff0c;1、4、9 和 16 都是完全平方数&#xff0c;而 3 和 11 不是。 示…

【指针进阶】(题目练习)

这篇文章的思维导图在这里&#xff1a;思维导图 一维数组&#xff1a; int a[] {1,2,3,4}; printf("%d\n",sizeof(a)); printf("%d\n",sizeof(a0)); printf("%d\n",sizeof(*a)); printf("%d\n",sizeof(a1)); printf("%d\n"…

Vit 实战营 Class2:图像与Transformer基础

文章目录 数组图像&#xff1a;图像与像素图像分类&#xff1a;机器如何学习&#xff1f;NMT&#xff1a;Neuron Machine TranslationTransformerVision Transformer代码实战 数组图像&#xff1a;图像与像素 什么是数字图像&#xff1f;在计算机图像的图像格式。每一个点叫pix…

全国节能宣传周丨物通博联智慧能源解决方案助力节能降碳

今年7月10日至16日&#xff0c;为全国第33个节能宣传周。今年全国节能宣传周活动主题是“节能降碳&#xff0c;你我同行”。 全国节能宣传周活动是在1990年国务院第六次节能办公会议上确定的活动周&#xff0c;开展该活动是实施全面节约战略、开展节能降碳宣传教育、推动形成绿…

ros系统生成kinova双臂机器人moveit配置包方法,详细过程,亲测有效!

环境&#xff1a;ubuntu18.04 ros&#xff1a;melodic 此博客前提你已经安装了moveit&#xff0c;如果未安装&#xff0c;可参考链接 一、启动MoveIt Setup Assistant roslaunch moveit_setup_assistant setup_assistant.launch点击&#xff1a;Create New MoveIt Configurat…

听产品大佬谈大语言模型的商业化价值

今年 3 月以来&#xff0c;全球各大厂商陆续发布大语言模型&#xff0c;无数人欢欣鼓舞&#xff0c;庆祝沉寂了几年的 AI 领域重新焕发生机。 然而热闹过后&#xff0c;一个现实的问题摆在面前&#xff1a;大语言模型的商业化价值该如何挖掘&#xff1f; 来自美洽的资深产品经…

CVE-2021-41773

CVE-2021-41773 Apache Httpd Server 路径穿越漏洞 Httpd&#xff08;即 HTTP Daemon &#xff0c;超文本传输协议守护程序的简称&#xff09;是一款运行于网页服务器后台&#xff0c;等待传入服务器请求的软件。HTTP 守护程序能自动回应服务器请求&#xff0c;并使用 HTTP 协…

fastadmin调试前端js

当键盘按键抬起后打印id #username&#xff0c;点击箭头的除缓存 隐藏前端昵称 设置为 style"sidplay:none" 添加管理员组别的时候把此处的 NULL改为 1 就可以默认选择第一组别

springboot生鲜交易系统

生鲜交易管理方面的任务繁琐,以至于交易市场每年都在生鲜交易管理这方面投入较多的精力却效果甚微,生鲜交易系统的目标就是为了能够缓解生鲜交易管理工作方面面临的压力,让生鲜交易管理方面的工作变得更加高效准确。 本项目在开发和设计过程中涉及到原理和技术有: B/S、java技…

css 绘制直角梯形 和 平行四边形

<div:class"{isFirst: index 0,tab_item: index ! 0,}":style"{width: item?.label?.length > 4 ? 206px : 137px,}"><div>{{ item.label }}</div></div> isFirst是直角梯形 tab_item是平行四边形 直角梯形 .isFirstcur…

计算机系统结构与操作系统实验三(5)-中断

&#x1f4cd;实验要求 将中断处理程序放到C里编写 &#x1f4cd;实验过程 鉴于代码相比之前&#xff0c;多出了很多配套的固定代码&#xff0c;因此代码量和代码中来突增&#xff0c;本文就不再一个一个贴代码了&#xff0c;需要的直接去看我上传在csdn的代码吧(文末有链接…

音视频绕不开的话题之WebRTC

什么是WebRTC&#xff1f; 闲来无事&#xff0c;我们今天探讨下音视频绕不开的一个话题&#xff1a;WebRTC。WebRTC之于音视频行业&#xff0c;无异于FFMpeg&#xff0c;可以说WebRTC的开源&#xff0c;让音视频行业大跨步进入发展快车道。 WebRTC是一个支持实时音视频通信的开…

mac系统终端运行python文件

1.在要运行的python文件首行里添加python解释器路径代码 代码&#xff1a; #!/usr/bin/env python3 注意:上面代码中的python3是对应当前的python版本 例子&#xff1a; 2.复制要运行的python文件位置&#xff0c;在终端使用代码进入要运行的文件位置 终端运行代码&…