浅谈volatile关键字

news/2024/4/20 3:05:08/文章来源:https://blog.csdn.net/qq_45058208/article/details/129171590

文章目录

    • 1.保证内存可见性
    • 2.可见性验证
    • 3.原子性验证
    • 4.原子性问题解决
    • 5.禁止指令重排序
    • 6.JMM谈谈你的理解
      • 6.1.基本概念
      • 6.2.JMM同步规定
        • 6.2.1.可见性
        • 6.2.2.原子性
        • 6.2.3.有序性
      • 6.3.Volatile针对指令重排做了啥
    • 7.你在哪些地方用过Volatile?

volatile是Java提供的轻量级的同步机制,主要有三个特性:

1.保证内存可见性
2.不保证原子性
3.禁止指令重排序

1.保证内存可见性

volatile是Java提供的轻量级的同步机制,保证了可见性,不保证原子性。了解volatile工作机制,首先要对Java内存模型(JMM)有初步的认识:

  • 每个线程创建时,JVM会为其创建一份私有的工作内存(栈空间),不同线程的工作内存之间不能直接互相访问。JMM规定所有的变量都存在主内存,主内存是共享内存区域,所有线程都可以访问
  • 线程对变量进行读写,会从主内存拷贝一份副本到自己的工作内存,操作完毕后刷新到主内存。所以,线程间的通信要通过主内存来实现。

volatile的作用是:线程对副本变量进行修改后,其他线程能够立刻同步刷新最新的数值。这个就是可见性

在这里插入图片描述

2.可见性验证

我们来看一个例子:

package com.bruceliu.demo15;import java.util.concurrent.TimeUnit;/*** @BelongsProject: Thread0509* @BelongsPackage: com.bruceliu.demo15* @Author: bruceliu* @QQ:1241488705* @CreateTime: 2020-05-13 23:16* @Description: TODO*/
public class VolatileDemo {int x = 0;//注意:这里的b没有被volatile修饰boolean b = false;/*** 写操作*/private void write() {x = 5;b = true;System.out.println("x=>" + x);System.out.println("b =>" + b);}/*** 读操作*/private void read() {//如果b=false的话,就会无限循环,直到b=true才会执行结束,会打印出x的值while (!b) {}System.out.println("x=" + x);}public static void main(String[] args) throws Exception {final VolatileDemo volatileDemo = new VolatileDemo();//线程1执行写操作Thread thread1 = new Thread(new Runnable() {public void run() {volatileDemo.write();}});//线程2执行读操作Thread thread2 = new Thread(new Runnable() {public void run() {volatileDemo.read();}});//我们让线程2的读操作先执行thread2.start();//睡1毫秒,为了保证线程2比线程1先执行TimeUnit.MILLISECONDS.sleep(1);//再让线程1的写操作执行thread1.start();thread1.join();thread2.join();//等待线程1和线程2全部结束后,打印执行结束System.out.println("执行结束");}
}

注意我们的b没有用volatile修饰,我们先启动了线程2的读操作,后启动了线程1的写操作,由于线程1和线程2会保存x和b的副本到自己的工作内存中,线程2执行后,由于他副本b=false,所以会进入到无限循环中,线程1执行后修改的也是自己副本中的b=true,然而线程2无法立即察觉到,所以执行上面代码后,不会打印“执行结束”,因为线程2一直在执行!
在这里插入图片描述

运行之后会一直出于运行状态,并且没有打印“执行结束”

在这里插入图片描述
此时的流程会是这样子

在这里插入图片描述

给b加了volatile关键字修饰后,线程1对b做了修改,然后会立即更新内存中的值,线程2通过嗅探发现自己的副本已经过期了,然后重新从内存中拿到b=true的值,然后跳出while循环,执行结束!

我们知道volatile关键字的作用是保证变量在多线程之间的可见性,它是java.util.concurrent包的核心,没有volatile就没有这么多的并发类给我们使用.

3.原子性验证

看下面一段代码,number变量加了volatile修饰。创建了10个子线程,每个线程循环1000次执行number++。

package com.bruce.demo8;import java.util.concurrent.TimeUnit;/*** @BelongsProject: SingleTon-2020* @BelongsPackage: com.bruce.demo8* @CreateTime: 2020-12-10 23:08* @Description: TODO*/
public class Demo8 {static class MyTest {public volatile int number = 0;public void incr(){number++;}}public static void main(String[] args) {MyTest myTest = new MyTest();for (int i = 1; i <= 10; i++){new Thread(() -> {for (int j = 1; j <= 1000; j++){myTest.incr();}}, "Thread"+String.valueOf(i)).start();}try {TimeUnit.SECONDS.sleep(3);} catch (InterruptedException e) {e.printStackTrace();}//等线程执行结束了,输出number值System.out.println("当前number:" + myTest.number);}
}

按理说number最终应该是10000,但是这边执行后,结果如下:
在这里插入图片描述

4.原子性问题解决

方法一:使用 synchronized 关键字

//给函数增加synchronized修饰,相当于加锁了public synchronized void incr(){number++;}

结果如下:
在这里插入图片描述
方法二:使用AtomicInteger

public class Demo8 {static class MyTest {public volatile AtomicInteger number = new AtomicInteger();public void incr(){number.getAndIncrement();}}public static void main(String[] args) {MyTest myTest = new MyTest();for (int i = 1; i <= 10; i++){new Thread(() -> {for (int j = 1; j <= 1000; j++){myTest.incr();}}, "Thread"+String.valueOf(i)).start();}try {TimeUnit.SECONDS.sleep(3);} catch (InterruptedException e) {e.printStackTrace();}//等线程执行结束了,输出number值System.out.println("当前number:" + myTest.number);}
}

5.禁止指令重排序

体现了JMM的有序性

6.JMM谈谈你的理解

6.1.基本概念

JMM 本身是一种抽象的概念并不是真实存在,它描述的是一组规定或则规范,通过这组规范定义了程序中的访问方式。

6.2.JMM同步规定

6.2.1.可见性

可见性:一个线程对某一共享变量修改之后,另一个线程要立即获取到修改后的结果。

  • 线程解锁前,必须把共享变量的值刷新回主内存
  • 线程加锁前,必须读取主内存的最新值到自己的工作内存
  • 加锁解锁是同一把锁

由于 JVM 运行程序的实体是线程,而每个线程创建时 JVM 都会为其创建一个工作内存,工作内存是每个线程的私有数据区域,而 Java 内存模型中规定所有变量的储存在主内存,主内存是共享内存区域,所有的线程都可以访问,但线程对变量的操作(读取赋值等)必须都工作内存进行看。

首先要将变量从主内存拷贝的自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,工作内存中存储着主内存中的变量副本拷贝,前面说过,工作内存是每个线程的私有数据区域,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成。

内存模型图
在这里插入图片描述

6.2.2.原子性

原子性: 不可分割,完整性,也就是说某个线程正在做某个具体业务时,中间不可以被加塞或者被分割,需要具体完成,要么同时成功,要么同时失败。

数据库也经常提到事务具备原子性!
在这里插入图片描述

在putfield时,其他线程挂起,没有及时得到主内存的值改变消息

n++在多线程下是非线程安全的,如何不加synchronized解决?

  • 使用原子类(java.util.concurrent.atomic)
  • 为什么使用了原子类可以解决?原理是什么?CAS

6.2.3.有序性

计算机在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排,一般分一下3种:

源代码->编译器优化的重排->指令并行的重排->内存系统的重排->最终执行的指令

单线程环境里面确保程序最终执行结果和代码顺序执行的结果一致。

处理器在进行重排序时必须考虑指令之间的数据依赖性。

多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测。

指令重排 - example 1

public void mySort() {int x = 11;   // 1int y = 12;   // 2x = x + 5;  // 3y = x * x;  // 4}

按照正常单线程环境,执行顺序是 1 2 3 4
但是在多线程环境下,可能出现以下的顺序:

2 1 3 41 3 2 4

上述的过程就可以当做是指令的重排,即内部执行顺序,和我们的代码顺序不一样。但是指令重排也是有限制的,即不会出现下面的顺序

4 3 2 1

因为处理器在进行重排时候,必须考虑到指令之间的数据依赖性

因为步骤 4:需要依赖于 y的申明,以及x的申明,故因为存在数据依赖,无法首先执行

例子
int a,b,x,y = 0

线程1线程2
x = a;y = b;
b = 1;a = 2;
x = 0; y = 0

因为上面的代码,不存在数据的依赖性,因此编译器可能对数据进行重排

线程1线程2
b = 1;a = 2;
x = a;y = b;
x = 2; y = 1

这样造成的结果,和最开始的就不一致了,这就是导致重排后,结果和最开始的不一样,因此为了防止这种结果出现,volatile就规定禁止指令重排,为了保证数据的一致性

指令重排 - example 2
比如下面这段代码

public class ResortSeqDemo {int a= 0;boolean flag = false;public void method01() {a = 1;flag = true;}public void method02() {if(flag) {a = a + 5;System.out.println("reValue:" + a);}}
}

我们按照正常的顺序,分别调用method01() 和 method02() 那么,最终输出就是 a = 6

但是如果在多线程环境下,因为方法1 和 方法2,他们之间不能存在数据依赖的问题,因此原先的顺序可能是

a = 1;
flag = true;a = a + 5;
System.out.println("reValue:" + a);

但是在经过编译器,指令,或者内存的重排后,可能会出现这样的情况

flag = true;a = a + 5;
System.out.println("reValue:" + a);a = 1;

也就是先执行 flag = true后,另外一个线程马上调用方法2,满足 flag的判断,最终让a + 5,结果为5,这样同样出现了数据不一致的问题

为什么会出现这个结果:多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测。

这样就需要通过volatile来修饰,来禁止指令重排序保证线程安全性

6.3.Volatile针对指令重排做了啥

Volatile实现禁止指令重排优化,从而避免了多线程环境下程序出现乱序执行的现象
首先了解一个概念,内存屏障(Memory Barrier)又称内存栅栏,是一个CPU指令,它的作用有两个:

  • 保证特定操作的顺序
  • 保证某些变量的内存可见性(利用该特性实现volatile的内存可见性)

由于编译器和处理器都能执行指令重排的优化,如果在指令间插入一条Memory Barrier则会告诉编译器和CPU,不管什么指令都不能和这条Memory Barrier指令重排序,也就是说 通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化。 内存屏障另外一个作用是刷新出各种CPU的缓存数,因此任何CPU上的线程都能读取到这些数据的最新版本。
在这里插入图片描述
也就是过在Volatile的写 和 读的时候,加入屏障,防止出现指令重排的

7.你在哪些地方用过Volatile?

工作内存与主内存同步延迟现象导致的可见性问题

  • 可通过synchronized或volatile关键字解决,他们都可以使一个线程修改后的变量立即对其它线程可见,对于指令重排导致的可见性问题和有序性问题
  • 可以使用volatile关键字解决,因为volatile关键字的另一个作用就是禁止重排序优化

举例:

public class LazySafe {private LazySafe(){}//对象加上了volatile关键字是为了保证变量的可见性,防止指令重排序//第二个线程拿到的可能是半实列化的对象,所以要加volatile防止指令重排序private volatile static LazySafe lazySafe;public static  LazySafe getInstance(){if(lazySafe==null){//双重判定synchronized (LazySafe.class){if(lazySafe==null){lazySafe=new LazySafe(); //不是原子性的!}}}return lazySafe;}
}

DCL(双端检锁)机制不一定是线程安全的,原因是有指令重排的存在,加入volatile可以禁止指令重排

原因是在某一个线程执行到第一次检测的时候,读取到 instance 不为null,instance的引用对象可能没有完成实例化。因为 instance = new SingletonDemo();可以分为以下三步进行完成:

memory = allocate(); // 1、分配对象内存空间
instance(memory); // 2、初始化对象
instance = memory; // 3、设置instance指向刚刚分配的内存地址,此时instance != null

但是我们通过上面的三个步骤,能够发现,步骤2 和 步骤3之间不存在 数据依赖关系,而且无论重排前 还是重排后,程序的执行结果在单线程中并没有改变,因此这种重排优化是允许的。

memory = allocate(); // 1、分配对象内存空间
instance = memory; // 3、设置instance指向刚刚分配的内存地址,此时instance != null,但是对象还没有初始化完成
instance(memory); // 2、初始化对象

这样就会造成什么问题呢?

也就是当我们执行到重排后的步骤2,试图获取instance的时候,会得到null,因为对象的初始化还没有完成,而是在重排后的步骤3才完成,因此执行单例模式的代码时候,就会重新在创建一个instance实例

指令重排只会保证串行语义的执行一致性(单线程),但并不会关系多线程间的语义一致性

所以当一条线程访问instance不为null时,由于instance实例未必已初始化完成,这就造成了线程安全的问题

所以需要引入volatile,来保证出现指令重排的问题,从而保证单例模式的线程安全性!

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

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

相关文章

【华为OD机试模拟题】用 C++ 实现 - 求字符串中所有整数的最小和

最近更新的博客 华为OD机试 - 入栈出栈(C++) | 附带编码思路 【2023】 华为OD机试 - 箱子之形摆放(C++) | 附带编码思路 【2023】 华为OD机试 - 简易内存池 2(C++) | 附带编码思路 【2023】 华为OD机试 - 第 N 个排列(C++) | 附带编码思路 【2023】 华为OD机试 - 考古…

【Git】Git的分支操作

目录 4、 Git 分支操作 4.1 什么是分支 4.2 分支的好处 4.3 分支的操作 4、 Git 分支操作 4.1 什么是分支 在版本控制过程中&#xff0c; 同时推进多个任务&#xff0c; 为每个任务&#xff0c; 我们就可以创建每个任务的单独分支。 使用分支意味着程序员可以把自己的工作…

postgres 源码解析50 LWLock轻量锁--1

简介 postgres LWLock&#xff08;轻量级锁&#xff09;是由SpinLock实现&#xff0c;主要提供对共享存储器的数据结构的互斥访问。LWLock有两种锁模式&#xff0c;一种为排他模式&#xff0c;另一种是共享模式&#xff0c;如果想要读取共享内存中的内容&#xff0c;需要在读取…

面试之设计模式(简单工厂模式)

案例 在面试时&#xff0c;面试官让你通过面对对象语言&#xff0c;用Java实现计算器控制台程序&#xff0c;要求输入两个数和运算符号&#xff0c;得出结果。大家可能想到是如下&#xff1a; public static void main(String[] args) {Scanner scanner new Scanner(System.…

BERT模型系列大全解读

前言 本文讲解的BERT系列模型主要是自编码语言模型-AE LM&#xff08;AutoEncoder Language Model&#xff09;&#xff1a;通过在输入X中随机掩码&#xff08;mask&#xff09;一部分单词&#xff0c;然后预训练的主要任务之一就是根据上下文单词来预测这些单词&#xff0c;从…

F.pad() 函数

F.pad() 对tensor 进行扩充的函数。 torch.nn.functional.pad (input, pad, mode‘constant’, value0) input&#xff1a;需要扩充的 tensor&#xff0c;可以是图像数据&#xff0c;亦或是特征矩阵数据&#xff1b;pad&#xff1a;扩充维度&#xff0c;预先定义某维度上的扩充…

到了35岁,软件测试职业发展之困惑如何解?

35岁&#xff0c;从工作时间看&#xff0c;工作超过10年&#xff0c;过了7年之痒&#xff0c;多数IT人都已经跳槽几次。 35岁&#xff0c;发展比较好的软件测试人&#xff0c;已经在管理岗位&#xff08;测试经理甚至测试总监&#xff09;或已经成为测试专家或测试架构师。发展…

Head First设计模式---4.工厂方法模式

2.1工厂方法模式 亦称&#xff1a; 虚拟构造函数、Virtual Constructor、Factory Method 工厂方法模式是一种创建型设计模式&#xff0c; 其在父类中提供一个创建对象的方法&#xff0c; 允许子类决定实例化对象的类型。 [外链图片转存失败,源站可能有防盗链机制,建议将图片…

掌握MySQL分库分表(七)广播表、绑定表实战,水平分库+分表实现及之后的查询和删除操作

文章目录什么是广播表广播表实战数据库配置表Java配置实体类配置文件测试广播表水平分库分表配置文件运行测试什么是绑定表&#xff1f;绑定表实战配置数据库配置Java实体类配置文件运行测试水平分库分表后的查询和删除操作查询操作什么是广播表 指所有的分片数据源中都存在的…

2023该好好赚钱了,推荐三个下班就能做的副业

在过去的两年里&#xff0c;越来越多的同事选择辞职创业。许多人通过互联网红利赚到了他们的第一桶金。随着短视频的兴起&#xff0c;越来越多的人吹嘘自己年收入百万&#xff0c;导致很多刚进入职场的年轻人逐渐迷失自我&#xff0c;认为钱特别容易赚。但事实上&#xff0c;80…

Docker启动RabbitMQ,实现生产者与消费者

目录 一、Docker拉取镜像并启动RabbitMQ 二、Hello World &#xff08;一&#xff09;依赖导入 &#xff08;二&#xff09;消息生产者 &#xff08;三&#xff09;消息消费者 三、实现轮训分发消息 &#xff08;一&#xff09;抽取工具类 &#xff08;二&#xff09;启…

零基础机器学习做游戏辅助第十四课--原神自动钓鱼(四)yolov5目标检测

一、yolo介绍 目标检测有两种实现,一种是one-stage,另一种是two-stage,它们的区别如名称所体现的,two-stage有一个region proposal过程,可以理解为网络会先生成目标候选区域,然后把所有的区域放进分类器分类,而one-stage会先把图片分割成一个个的image patch,然后每个im…

【微信小程序】--JSON 配置文件作用(三)

&#x1f48c; 所属专栏&#xff1a;【微信小程序开发教程】 &#x1f600; 作  者&#xff1a;我是夜阑的狗&#x1f436; &#x1f680; 个人简介&#xff1a;一个正在努力学技术的CV工程师&#xff0c;专注基础和实战分享 &#xff0c;欢迎咨询&#xff01; &#…

二叉树、二叉搜索树、二叉树的最近祖先、二叉树的层序遍历【零神基础精讲】

来源0x3f&#xff1a;https://space.bilibili.com/206214 文章目录二叉树[104. 二叉树的最大深度](https://leetcode.cn/problems/maximum-depth-of-binary-tree/)[111. 二叉树的最小深度](https://leetcode.cn/problems/minimum-depth-of-binary-tree/)[129. 求根节点到叶节点…

黑马 Vue 快速入门 笔记

黑马 Vue 快速入门 笔记0 VUE相关了解0.1 概述0.2 MVVM0.3 JavaScript框架0.4 七大属性0.5 el:挂载点1 VUE基础1.0 第一个vue代码&#xff1a;Hello&#xff0c;vue1.1 v-bind 设置元素的属性 简写 &#xff1a;1.2 v-if &#xff0c; v-else &#xff0c; v-else-ifv-if , v-e…

XC7K70T-1FBG676C应用XC7K70T-L2FBG484E Kintex-7, FPGA 规格参数

概述Kintex-7 FPGA为快速增长应用和无线通信提供最优性价比和低功耗。Kintex-7 FPGA允许设计人员构建卓越带宽和12位数字可编程模拟&#xff0c;同时满足成本和功耗要求。144GMACS数字信号处理器 (DSP) 的独特功耗使得多功能Kintex-7器件成为便携式超声波设备和下一代通信等应用…

文案女王彭芳如何转变为“百万发售系统”创始人?我们来探个究竟!

智多星老师 她的输出跟智多星老师几乎毫无二致&#xff0c;是抄袭还是纯属巧合呢&#xff1f; 你们问的这个问题我也想知道&#xff0c;为了了解真相&#xff0c;我让我的一个学生把那个叫“彭芳老师”的视频给我看&#xff0c;当看到她的简介时&#xff0c;我非常震惊&#…

Elasticsearch在Linux中的单节点部署和集群部署

目录一、Elasticsearch简介二、Linux单节点部署1、软件下载解压2、创建用户3、修改配置文件4、切换到刚刚创建的用户启动软件5、测试三、Linux集群配置1、拷贝文件2、修改配置文件3、分别修改文件所有者4、启动三个软件5、测试四、问题总结1、在elasticsearch启动时如果报错内存…

numpy的常见数据类型

常见数据类型介绍Python 原生的数据类型相对较少&#xff0c; bool、int、float、str等。这在不需要关心数据在计算机中表示的所有方式的应用中是方便的。然而&#xff0c;对于科学计算&#xff0c;通常需要更多的控制。为了加以区分 numpy 在这些类型名称末尾都加了“_”。类型…

ES mapping 详解

nested 类型&#xff1f;&#xff1f;&#xff1f; _all _routing; ES-mapping Elasticsearch根据业务创建映射mapping结构分析&#xff1a;keyword和text&#xff08;一&#xff09;_elasticsearch keyword mapping_周全全的博客-CSDN博客 0.Mapping样例 {"mapping…