首页 > 系统相关 >【Java 并发】【五】volatile怎么通过内存屏障保证可见性和有序性

【Java 并发】【五】volatile怎么通过内存屏障保证可见性和有序性

时间:2023-04-02 15:58:16浏览次数:45  
标签:Java initOK 屏障 volatile 内存 有序性 线程 store

1  前言

这节我们就来看看volatile怎么通过内存屏障保证可见性和有序性。

2  保证可见性

volatile修饰的变量,在每个读操作(load操作)之前都加上Load屏障,强制从主内存读取最新的数据。每次在assign赋值后面,加上Store屏障,强制将数据刷新到主内存。

以volatile int x= 0;线程A、B进行x++的操作来画图给你讲解一下:

如上图所示:
(1)线程A读取i的值遇到Load屏障,需要强制从主存读取得到x = 0; 然后传递给工作线程执行x++操作
(2)cpu执行x++操作得到x = 1,执行assign指令进行赋值;然后遇到Store屏障,需要强制刷新回主内存,此时得到主内存x = 1
(3)然后线程B执行读取i遇到Load屏障,强制从主内存读取,得到最新的值x = 1,然后传给工作线程执行x++操作,得到x = 2,同样在赋值后遇到Store屏障立即将数据刷新回主内存

其实说白了就是通过一个屏障让volatile的变量每次读都读主存,每次修改后立即刷到主存里面。好比线程A修改 i 后立即将值刷到主存里面,后面线程B用到的时候强制从主存读取,这个时候它能看到的值是线程A修改之后的值了。volatile通过内存屏障每次走主存的方式;这样来保障可见性。

3  保证有序性

之前讲过一个有序性问题导致异常的例子,我们回顾下:

线程A的执行代码:

// 步骤1
dataSource = initDataSource();
// 步骤2
httpClient = initHttpClient();
// 步骤3
initOK = true;

线程B的执行代码:

// 步骤4
while(!initOK) {
}
// 步骤5
Object data = dataSource.getData();
// 步骤6
httpClient.request(data);

由于线程A先执行了initOK = true。导致线程B提前跳出了while循环,然后线程B调用dataSource.getData的时候发现dataSource没初始化好,竟然是个坑爹的null,导致代码报错了。

现在我们就来讲讲将initOk用volatile来修饰,是可以做到线程A有序性执行的。好了,废话不多说,我先来上代码:

对于线程A执行的代码,对应的指令是这样的:

// 步骤1
对应上面dataSource = initDataSource(); 对应指令 store datasource指令
// 步骤2
对应上面httpClient = initHttpClient(); 对应指令 store http指令
// 步骤3
StoreStore屏障 (注意:在store initOK前面加了一个StoreStore屏障
initOK = true; 对应指令 store initOk = true指令
StoreLoad 屏障 (注意:在store initOK后面加了一个StoreLoad屏障)

其中会在store initOk = true 前面加一道StoreStore内存屏障,在其后面加一道StoreLoad内存屏障,再结合上一篇我们讲过内存屏障如何禁止指令重排序的那个图:

所以通过volatile修饰initOK,加了屏障之后;store initOK = true这一条指令是不能跳到store dataSource、store http前面去的,所以必须先执行完前面的执行之后,才能执行store initOK = true。
这样对于线程B来说,它看到线程A就是资源初始化完成之后,才将initOK表示设置为true的,这样它看到线程A的执行就是有序的。
这个volatile写的时候前面加StoreStore屏障、写的后面加StoreLoad屏障来禁止重排序的,就是通过加了屏障,store initOK = true 指令不能跟前面的store指令进行交换。所以它就自然得等前面的store指令执行完了之后,才执行store initOK = true的,然后在线程B那一侧看到的initOK = true的时候,发现资源以及初始化好了,自然就不会报错了。

4  volatile为啥不能保证原子性

按照惯例,我们来画张图,还是以 x++ 的那个例子为例,volatile  int x = 0,假如两个线程A、线程B同时对 x 进行 ++ 操作如下:

上图存在一种情况就是,线程A、线程B如果几乎同时读取 x = 0 到自己的工作内存中。
线程A执行 x++ 结果后将 x = 1 赋值给工作内存;但是这个时候还没来的将最新的结果刷新回主内存的时候,线程B就使用读取主内存的旧值 x = 0 ,然后执行use指令将 x = 0的值传递给线程B去进行操作了。即使这个时候线程A立即将 x = 1刷入主内存,那也晚了;线程B已经使用旧值 x = 0进行操作了,像这种情况计算结果就不对了。

如果要保证原子性的话,落到底层实际还是需要进行加锁的,需要保证任意时刻只能有一个线程能执行成功。

比如在硬件层次或者对总线进行加锁,使得某一时刻只能有一个线程能执行x++操作,这样才能是不被中断的,才是原子性的。
现在现在这种情况,相当于就是两个线程同时进行了 x++操作,线程A的 x++操作还没结束;线程B的 i++操作就也同时进行着,这种情况不是原子的。

如果要保证原子性的话,同一时刻只能有一个线程或者CPU能够执行成功,底层是需要对硬件进行加锁的,只有某个CPU或者线程锁定了,享有独占的权限,那么它的操作才能是不被其它CPU或者线程打断的。

5  小结

这节我们简单看了内存屏障是如何保证有序性和可见性的,以及原子性不可保证的原因,有理解不对的地方欢迎指正哈。

标签:Java,initOK,屏障,volatile,内存,有序性,线程,store
From: https://www.cnblogs.com/kukuxjx/p/17280550.html

相关文章

  • Java学习笔记(十四) maven1
    Maven介绍Maven是apache旗下的一个开源项目,是一款用于管理和构建java项目的工具,基于项目对象模型(POM)的概念,通过一小段信息来管理项目的构建Apache软件基金会,成立于1999年7月,是目前世界上最大的最受欢迎的开源软件基金会,也是一个专门为支持开源项目而生的非营利性......
  • Java学习笔记(十三) 前端基础2
    Ajax介绍概念:AsynchronousJavaScriptAndXML,异步的JavaScript和XML作用:数据交换:通过Ajax可以给服务器发送请求,并获取服务器响应的数据异步交互:可以在不重新加载整个页面的情况下,与服务器交换数据并更新部分网页的技术,如:搜索联想,用户名是否可用的校验等......
  • Java 函数式编程
    概述背景函数式编程的理论基础是阿隆佐·丘奇(AlonzoChurch)于1930年代提出的λ演算(LambdaCalculus)。λ演算是一种形式系统,用于研究函数定义、函数应用和递归。它为计算理论和计算机科学的发展奠定了基础。随着Haskell(1990年)和Erlang(1986年)等新一代函数式编程语言的诞生,......
  • Java学习笔记(十二) 前端基础1
    Web前端基础初识web前端网页由哪些部分组成?文字图片音频视频超链接等我们看到的网页,背后的本质是什么?程序员写的前端代码前端的代码是如何转换成用户眼中的网页的?通过浏览器转化(解析和渲染)成用户看到的网页浏览器中对代码进行解析渲染的部分,称为浏......
  • RxJava在业务系统中的实践
    在java的世界里由于大多数接口和API都是阻塞式的交互,进而影响到很多童靴的编程思想和编程习惯。因而,有一些专家讲java的编程模型是阻塞式模型(与Node.js区别大),不是没有道理的。从高性能的视角看,任何阻塞点都可能导致性能的退步。而响应式编程其天然就是非阻塞的,当数据准备完成后自动......
  • 面试题45(Java)-把数组排成最小的数(中等)
    题目:输入一个非负整数数组,把数组里所有数字拼接起来排成一个数,打印能拼接出的所有数字中最小的一个。示例1:输入:[10,2]输出:"102"示例 2:输入:[3,30,34,5,9]输出:"3033459"提示:0<nums.length<=100说明:输出结果可能非常大,所以你需要返回一个字符串而不......
  • Java基础语法
    用户交互Scanner实验importjava.util.Scanner;publicclassDome01{publicstaticvoidmain(String[]args){Scannerscanner=newScanner(System.in);System.out.println("使用Next方式接受");if(scanner.hasNext()){......
  • java——spring boot集成kafka——spring boot集成kafka
    引入依赖:  <dependency><groupId>org.springframework.kafka</groupId><artifactId>spring-kafka</artifactId></dependency>          编写配置文件:    erver:port:8080spring:kafka:bootstrap-se......
  • Java IO面试题
    JavaIO概览JavaIO流的40多个类都是从如下4个抽象类基类中派生出来的。InputStream/Reader:所有的输入流的基类,前者是字节输入流,后者是字符输入流。OutputStream/Writer:所有输出流的基类,前者是字节输出流,后者是字符输出流。InputStream字节输入流InputStream用于......
  • Java:分数运算(类与对象)
    题目内容:设计一个表示分数的类Fraction。这个类用两个int类型的变量分别表示分子和分母。这个类的构造函数是:Fraction(inta,intb)    构造一个a/b的分数。这个类要提供以下的功能:doubletoDouble();    将分数转换为doubleFractionplus(Fractionr);    将自己的分......