Java高并发编程—可见性与有序性原理

news2024/11/13 14:41:31

原子性、可见性和有序性是并发编程所面临的三大问题。 Java通过CAS操作已解决了并发编程中的原子性问题,本章为大家介绍Java如何解决剩余的另外两个问题——可见性和有序性。

CPU物理缓存结构

由于CPU的运算速度比主存(物理内存)的存取速度快很多,为了提高处理速度,现代CPU不直接和主存进行通信,而是在CPU和主存之间设计了多层的高速Cache(高速缓存),越靠近CPU的缓存越快,容量也越小。

每一级高速缓存中所储存的数据都是下一级高速缓存的一部分,越靠近CPU的高速缓存越快,容量也越小。L1高速缓存和L2高速缓存都只能被一个CPU单核使用, L3高速缓存可以被同一个插槽上的CPU内核共享,主存由全部插槽上的所有CPU核共享。 CPU读取数据时,先从L1高速缓存中读取,如果没有命中,再到L2、 L3高速缓存中读取,假如这些高速缓存都没有命中,它就会到主存中读取所需要的数据。

解决缓存一致性

缓存一致性:当多个处理器运算任务都涉及到同一块主内存区域的时候,将可能导致各自的缓存数据不一样。MESI(Modified Exclusive Shared Or Invalid)是一种广泛使用的支持写回策略的缓存一致性协议,CPU 中每个缓存行(caceh line)使用 4 种状态进行标记(使用额外的两位 bit 表示):Modified 被修改的;Exclusive 独享的;Shared 共享的; Invalid 无效的;

并发编程的三大问题

原子性问题

原子操作就是“不可中断的一个或一系列操作”,是指不会被线程调度机制打断的操作。这种操作一旦开始,就一直运行到结束,中间不会有任何线程的切换。 前面讲到的i++操作不是原子性操作,因为操作系统底层有四个原子操作组成:

  1. 从主存中复制i的值并复制到CPU的工作内存中。
  2. CPU读取工作内存中的值,然后执行i++操作,完成后刷新到工作内存。
  3. 将工作内存中的值更新到主存。
可见性问题

一个线程对共享变量的修改,另一个线程能够立刻可见,我们称为该共享变量具备内存可见性。 JMM(Java Memory Model, Java内存模型)规定,所有的变量都存放在公共主内存中,当线程使用变量时会把主存中的变量复制到自己的工作空间(或者叫作私有内存)中,线程对变量的读写操作,是自己工作内存中的变量副本。可见性问题主要的两点是:变量共享、多线程!存在不可见问题的根本原因是由于缓存的存在,线程持有的是共享变量的副本,无法感知其他线程对于共享变量的更改,导致读取的值不是最新的。但是 final 修饰的变量是不可变的,就算有缓存,也不会存在不可见的问题。

main 线程对 run 变量的修改对于 t 线程不可见,导致了 t 线程无法停止:

static boolean run = true;	//添加volatile
public static void main(String[] args) throws InterruptedException {
    Thread t = new Thread(()->{
        while(run){
        // ....
        }
	});
    t.start();
    sleep(1);
    run = false; // 线程t不会如预想的停下来
}

原因:

  • 初始状态, t 线程刚开始从主内存读取了 run 的值到工作内存

  • 因为 t 线程要频繁从主内存中读取 run 的值,JIT 编译器会将 run 的值缓存至自己工作内存中的高速缓存中,减少对主存中 run 的访问,提高效率

  • 1 秒之后,main 线程修改了 run 的值,并同步至主存,而 t 是从自己工作内存中的高速缓存中读取这个变量的值,结果永远是旧值

在这里插入图片描述

**内存屏蔽机制保证可见性:**对于volatile读来说,本身的指令是load,如果需要保证可见性,只要后面的普通读、普通写,不重排到前面就可以了。 对于volatile写来说,本身的指令是store,保证其可见性,需要其前面的普通写、后面的普通读,不可以和自己重排。

有序性问题

所谓的程序的有序性,是指程序执行的顺序按照代码的先后顺序执行。如果程序执行的顺序与代码的先后顺序不同,并导致了错误的结果,即发生了有序性问题。CPU为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。只要两个指令之间不存在“数据依赖”,就可以对这两个指令乱序。 指令重排需要保证As-if-Serrial规则,As-if-Serial规则的具体内容为:不管如何重排序,都必须保证代码在单线程下运行正确。

**为什么指令重排?**因为有些指令很费时间,在保证单线程下运行正确的前提下,CPU会先去指向后面的指令再回过头来指向费时间的指令,前提不存在指令依赖。另外使用流水线机制需要指令重排。

volatile 的原理

面介绍过,为了解决CPU访问主存时读写性能的短板,在CPU中增加了高速缓存,但这带来了可见性问题。而Java的volatile关键字可以保证共享变量的主存可见性,也就是将共享变量的改动值立即刷新回主存。在正常情况下,系统操作并不会校验共享变量的缓存一致性,只有当共享变量被volatile关键字修饰了,该变量所在的缓存行才被要求进行缓存一致性的校验。 特性:

  • 保证可见性
  • 不保证原子性
  • 保证有序性(避免指令重排)

性能:volatile 修饰的变量进行读操作与普通变量几乎没什么差别,但是写操作相对慢一些,因为需要在本地代码中插入很多内存屏障来保证指令不会发生乱序执行,但是开销比锁要小。

synchronized 无法禁止指令重排和处理器优化,为什么可以保证有序性可见性

  • 加了锁之后,只能有一个线程获得到了锁,获得不到锁的线程就要阻塞,所以同一时间只有一个线程执行,相当于单线程,由于数据依赖性的存在,单线程的指令重排是没有问题的
  • 线程加锁前,将清空工作内存中共享变量的值,使用共享变量时需要从主内存中重新读取最新的值;线程解锁前,必须把共享变量的最新值刷新到主内存中(JMM 内存交互章节有讲)
有序性与内存屏障

内存屏障是一系列的CPU指令,它的作用主要是保证特定操作的执行顺序,保障并发执行的有序性。在编译器和CPU都进行指令的重排优化时,可以通过在指令间插入一个内存屏障指令,告诉编译器和CPU,禁止在内存屏障指令的前(或后)执行指令重排序。

  • 写屏障:在指令后插入写屏障指令,将寄存器、高速缓存中的最新数据更新到主存,让其他线程可见。 并且不能对之前的指令重排。 将写屏障之前的共享值同步到主存中,且之前的指令不能重排。
  • 读屏障:在指令前插入读屏障,让高速缓存中的数据全部失效,强制重新从主存加载最新的数据。并且不能对后续的指令重排。将读屏障之后的共享值从主存中加载最新的数据,且之后的指令不能重排
  • 全屏蔽:是一种全能型的屏障,具备读屏障和写屏障的能力。

**内存屏蔽机制保证可见性与有序性:**对于volatile读来说,本身的指令是load,如果需要保证可见性,只要后面的普通读、普通写,不重排到前面就可以了。 对于volatile写来说,本身的指令是store,保证其可见性,需要其前面的普通写、后面的普通读,不可以和自己重排。

JMM(Java Memory Model,即Java内存模型)

JMM提供了合理的禁用缓存以及禁止重排序的方法,所以其核心的价值在于解决可见性和有序性。 本身是一种抽象的概念,实际上并不存在,物理都是存在内存条中。

  • 主存:主要存储的是Java实例对象,所有线程创建的实例对象都存放在主存中,无论该实例对象是成员变量还是方法中的本地变量(也称局部变量),当然也包括了共享的类信息、常量、静态变量。由于是共享数据区域,因此多个线程对同一个变量进行访问可能会发现线程安全问题。 主内存直接对应于物理硬件的内存
  • 工作内存:主要存储当前方法的所有本地变量信息(工作内存中存储着主存中的变量副本),每个线程只能访问自己的工作内存,即线程中的本地变量对其他线程是不可见的,即使两个线程执行的是同一段代码,它们也会各自在自己的工作内存中创建属于当前线程的本地变量。注意,由于工作内存是每个线程的私有数据,线程间无法相互访问工作内存,因此存储在工作内存的数据不存在线程安全问题。工作内存对应寄存器和高速缓存

JMM 作用:

  • 屏蔽各种硬件和操作系统的内存访问差异,实现让 Java 程序在各种平台下都能达到一致的内存访问效果
  • 规定了线程和内存之间的一些关系

Java内存模型的规定如下:1)所有变量存储在主存中。2)每个线程都有自己的工作内存,且对变量的操作都是在工作内存中进行的。3)不同线程之间无法直接访问彼此工作内存中的变量,要想访问只能通过主存来传递。
在这里插入图片描述

JMM将所有的变量都存放在公共主存中,当线程使用变量时,会把公共主存中的变量复制到自己的工作内存(或者叫作私有内存)中,线程对变量的读写操作是自己的工作内存中的变量副本。因此, JMM模型也需要解决代码重排序和缓存可见性问题。 JMM提供了一套自己的方案去禁用缓存以及禁止重排序来解决这些可见性和有序性问题。 JMM提供的方案包括大家都很熟悉的volatile、synchronized、 final等。 JMM主存与工作内存之间的交互协议的8个操作如下:

JMM 如何解决有序性问题

JMM提供了自己的内存屏障指令,要求JVM编译器实现这些指令,禁止特定类型的编译器和处理器重排序。

Happens-Before 规则介绍

  • 程序顺序执行规则( as-if-serial规则):在同一个线程中,有依赖关系的操作按照先后顺序,前一个操作必须先行发生于后面一个操作 。
  • 对volatile(修饰的)变量的写操作必须先行发生于对volatile变量的读操作。
  • 传递性规则:如果A操作先行发生于B操作,而B操作又先行发生于C操作,那么A操作先行发生于C操作。
  • 监视锁规则:解锁操作先行发生于后续对这个监视锁的加锁操作 。
  • join规则:如果线程A执行了B.join()操作并成功返回,那么线程B中的任意操作先行发生于线程A所执行的ThreadB.join()操作。

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

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

相关文章

Android系统原理性问题分析 - 多路并发情况下的C/S模型

声明 在Android系统中经常会遇到一些系统原理性的问题,在此专栏中集中来讨论下。Android系统中很多地方都采用了I/O多路复用的机制,为了引出I/O多路复用机制,先来分析多路并发情况下的C/S模型。此篇参考一些博客和书籍,代码基于A…

C++条件变量condition_variable

一、问题 假设没有条件变量,对于一个生产者消费者问题,消费线程在得知队列中没有产品时,将阻塞自己。生产者线程可以给队列中放入产品,但是没有办法激活消费者线程,而消费者线程处于阻塞状态也没有办法自己激活自己。…

RocketMQ 领域模型概述

本文为您介绍 Apache RocketMQ 的领域模型。 Apache RocketMQ 是一款典型的分布式架构下的中间件产品,使用异步通信方式和发布订阅的消息传输模型。通信方式和传输模型的具体说明,请参见下文通信方式介绍和消息传输模型介绍。 Apache RocketMQ 产品具备…

IOS开发指南之自定义TableViewCell使用

演示效果: 1.自定义TableViewCell创建 File->new->File... 在iOS模板中选择Empty来创建一个空的XIB文件,然后点击下一步 输入XIB文件名Cell,然后点击Create创建 创建XIB文件成功后如下: 同时按钮Shift+command+L弹出库,然后输入 table筛选,选择Table View Cell 拖到下…

一文通透spring的初始化

简述 今天重点分析ApplicationContext初始化时做的事情,我们都只到spring是个IOC和AOP容器,那再我们new一个ApplicationContext,spring内部都做了什么?怎么实现的IOC和AOP? 比如说下面这段代码 Configuration Compon…

计组 第二章 数据的表示与运算 2.1 数制与编码 知识点整理

2.1 数制与编码 二进制转八进制:3位一组,高位补0 二进制转十六进制:4位一组,高位补0 任意进制转十进制(按权展开法):数码与权值相乘,再相加 十进制转化为任意进制数(基…

全面接入:ChatGPT杀进10个商业应用,让AI替你打工

ChatGPT狂飙160天,世界已经不是两个月前的样子。 新建了一个网站 https://ai.weoknow.com/ 每天给大家更新可用的国内可用chatGPT资源 ChatGPT API已开放60多天。世界已经不是两个月前的样子了。 微软联合创始人比尔盖茨(BillGates)将GPT称…

一、预约挂号详情

文章目录 一、预约挂号详情1、需求分析 2、api接口2.1 添加service接口2.2 添加service接口实现2.2.1 在ScheduleServiceImpl类实现接口2.2.2 在获取科室信息 2.3 添加controller方法 3、前端3.1封装api请求3.2 页面展示 二、预约确认1、api接口1.1 添加service接口1.2 添加con…

FastRcnn理论合集

FastRcnn理论合集 Rcnn 论文原著 Rich feature hierarchies for accurate object detection and semantic segmentation R-CNN可以说是利用深度学习进行目标检测的开山之作。作者Ross Girshick多次在PASCAL VOC的目标检测竞赛中折桂,曾在2010年带领团队获得终身成就…

【P29】JMeter IF 控制器(If Controller)

文章目录 一、IF 控制器(If Controller)参数说明二、测试计划设计2.1、groovy 写法2.2、javaScript 写法2.3、jexl3 写法 一、IF 控制器(If Controller)参数说明 可以控制其下面的子/后代元素是否执行;如果为 true 则…

CSDN问答机器人

文章目录 前言一、背景二、总体流程三、构建知识库四、粗排五、精排六、Prompt总结相关博客 前言 先看结果: 已经连续很多周获得了第二名(万年老二), 上周终于拿了一回第一, 希望继续保持. 😁 这是今天的榜单, 采纳的数量相对较少, 之前基本上维持在100 重点说明…

数字韧性助力金融科技行稳致远 同创永益亮相2023双态IT武汉樱花论坛

2023年4月7日,由ITSS数据中心运营管理组DCMG指导,双态IT论坛主办的以“分布式架构和云原生时代的运维软件进化”为主题的“双态IT武汉樱花论坛”在武汉成功举办,共有来自银行、保险、证券等行业用户及企业代表近百人参会。 云原生时代下&am…

【Linux】-yum的使用

💖作者:小树苗渴望变成参天大树 ❤️‍🩹作者宣言:认真写好每一篇博客 💨作者gitee:gitee 💞作者专栏:C语言,数据结构初阶,Linux,C 如 果 你 喜 欢 作 者 的 文 章 ,就 给 作 者 点…

某生存游戏玩家属性值分析

0x01 背景 这是一款丧尸生存的多人沙盒类游戏,角色在废墟之城内不断的搜刮,强化自己的装备和建造设施来抵御丧尸的无休止攻击,记住,这是一款团队合作游戏,你面对的丧尸随时可能夺走你的性命! 0x02 玩家结…

【C++ Primer Plus】基础知识

C站的小伙伴们,大家好呀!我最近开始阅读学习《C Primer Plus》这本书,在这里和大家一起分享。 下面是本书的第二章《开始学习C》 开始学习C 进入Cmain()函数C注释C预处理器和iostream文件头文件名名称空间使用count进行…

【Linux专区】 Linux is not unix | Linux发展史 | Linux应用现状

💞💞欢迎来到 Claffic 的博客💞💞 👉 专栏:《Linux专区》👈 前言: 上次提前带大家搭建了Linux的环境,其实之前应该还有一步的,就是向大家介绍Linux发展史&…

HNU-电路与电子学-小班3

第三次讨论 1 、直接用晶体管而不是逻辑门实现异或门,并解释这个电路是如何工作的。 (6个 MOS 管构成) 2 、通信双方约定采用 7 位海明码进行数据传输。请为发送方设计海明码校验位 生成电路,采用功能块和逻辑门为接收方设计海…

SCMA基本原理介绍

SCMA: Sparse Code Multiple Access SCMA基本原理 我们考虑一个同步(synchronous)的SCMA系统, 含1个基站(Base Station, BS); J J J个用户(so called layers);K个OFDM…

算法练习-2:送外卖

n 个小区排成一列,编号为从 0 到 n-1 。一开始,美团外卖员在第0号小区,目标为位于第 n-1 个小区的配送站。 给定两个整数数列 a[0]~a[n-1] 和 b[0]~b[n-1] ,在每个小区 i 里你有两种选择: 1) 选择a:向前 a[…

shiro环境搭建

源码部署 这种方法相对复杂,如果不需要分析源码直接用docker就行 前置条件:Maven Ideal Tomcat 下载方式1:https://codeload.github.com/apache/shiro/zip/shiro-root-1.2.4,然后将文件夹导入ideal下载方式2:将shiro…