并发编程——5.JMM、可见性和有序性及volatile的底层实现原理

2024-04-09 11:36

本文主要是介绍并发编程——5.JMM、可见性和有序性及volatile的底层实现原理,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

这篇文章我们来讲一下JMM和其相关的内容。

目录

1.JMM模型的介绍

2.volatile的底层原理

3.有序性的介绍

3.1as-if-serial原则

3.2happen-before原则

4.内存屏障

5.小结


1.JMM模型的介绍

首先,我们来看一下JMM模型。

这是一张多核CPU的并发缓存架构图。我们的数据存在主内存RAM中,由于CPU的运算速度非常快,而CPU从主内存中读取数据的速度比较慢(与前者的速度是差几个量级的),所以为了适配这二者的速度差异,我们在CPU中开辟了一块缓存区,空间不大,里面放的是CPU中使用频率较高的数据,CPU从缓存区中读取数据的速度就比从主内存中读取数据的速度要快的多,这样就便于我们CPU的运行。我们的JMM模型就与上面的多核CPU并发缓存架构类似。

Java多线程内存模型(简称JMM)cpu缓存模型类似,是基于cpu缓存模型来建立的,Java线程内存模型是标准化的,屏蔽掉了底层不同计算机的区别。

如下图所示:

下面举例来解释一下:

假设主内存中有一个boolean类型的变量flag,初始值为true,现在我们的线程1要将这个flag改为false,它会先把这个flag复制一份到线程1的工作内存,然后在工作内存中将这个flag改为false。此时线程2和线程3中不一定会感知到这个flag为false。也就是说,我们的线程1将flag改为了false,但是我们的线程2和线程3中的flag还是true。这就是不满足线程的可见性。

下面我们来看一下程序:

解释一下:

首先是定义了一个共享变量initFlag,初始值为false,然后是main方法,里面new了一个线程,线程里面打印一句话,然后是一个死循环,然后线程启动。然后是主线程睡眠2s,然后又new了一个线程,线程里面调用一个方法,方法里面打印一句话,然后修改initFlag的值,然后再打印一句话。正常情况下,initFlag值被修改后,线程1中的死循环会跳出来,会打印success那句话。

但是结果结果显然不是这样的,根据结果我们可以知道,线程2中的所有内容都执行完了,但是线程1中的死循环还没有结束,那句success还没打印出来。那就说明线程2修改后的initFlag值没有被线程1感知到,所以线程1中的死循环没有结束。这就符合我们上面的JMM的讲解了。

那怎么解决呢?给我们的共享变量加一个volatile即可!

如下图所示:

这样问题就解决了

2.volatile的底层原理

上面我们讲了volatile可以解决可见性的问题,下面我们来看一下volatile的底层原理。

在讲volatile的底层原理之前,我们先来了解一下JMM的数据原子操作

如下图所示:

下面通过一个具体的例子来讲解一下

如下图所示(例子是上面initFlag的例子):

首先,主内存中存了 变量initFlag,初始值为false,然后线程1通过总线读取到initFlag,即read操作,然后是load操作,将initFlag写入工作内存中,然后是use操作,对应程序中就是进行判断。同一时刻,线程2也在进行这些操作,不过对应到程序中,线程2的use操作就是改值,然后线程2进行assign赋值操作,将新的initFlag值赋值到线程2的工作内存中的变量中,此时线程2中的initFlag才变为true,然后是store存储操作,即线程2将工作内存中的initFlag值存入主内存中,注意,此时主内存中原本的initFlag值还依然为false,等到最后一步write写入操作,才将主内存中的initFlag值改为true。

但是在线程2进行后面的一系列操作时,线程1中的initFlag值始终为false,并且线程1始终在使用这个initFlag的值,这就是不可见性。

那volatile到底是怎么保证我线程2在修改完initFlag值的同时,我线程1也能感知到并及时修改的呢?

首先,我们来了解两点内容:

然后,我们来看一张图,然后来解释一下:

首先说明一点,这个缓存一致协议是硬件上面的内容。

它的流程是这样的:当我们的线程2修改了initFlag的值之后,也就是执行了assign赋值操作后,它会瞬间触发后面的store存储和write写入这两个操作,也就是说,当某个CPU修改了工作内存里面的数据后,它会马上就将数据同步到主内存中。而其他的CPU通过总线嗅探机制会感知到自己缓存中数据的变化,然后将自己缓存中的数据判为无效数据,然后再重新从主内存中拿数据。

下面了解一下缓存一致协议(了解即可):

那volatile到底是怎么实现上面的那一套功能的呢?

我们来看下面的这张图:

简单来说就是:volatile的底层实现上会有一个汇编的lock前置指令,而这个汇编的lock前置指令会实现硬件层级的缓存一致协议,而缓存一致协议就是那些巴拉巴拉......的东西了

3.有序性的介绍

下面介绍并发三大特性中的有序性。

如下图所示:

简单来说就是:一般情况下,我们的程序是按照我们所写的每一行代码的顺序来运行的,但是有时候,为了提供程序的运行效率,计算机会将我们所写的代码编译为汇编语言后,改变我们所写代码是顺序,然后再运行,这也叫指令重排序,这就会导致在并发的情况下出现错误。

3.1as-if-serial原则

as-if-serial语义:

as-if-serial语义的意思是:不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变。编译器、runtime和处理器都必须遵守as-if-serial语义。

为了遵守as-if-seriali语义,编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果。但是,如果操作之间不存在数据依赖关系,这些操作就可能被编译器和处理器重排序。

3.2happen-before原则

只靠sychronized和volatle关键字来保证原子性、可见性以及有序性,那么编写并发程序可能会显得十分麻烦,幸运的是,从JDK5开始,Java使用新的JSR-133内存模型,提供了happens-before 原则来辅助保证程序执行的原子性、可见性以及有序性的问题,它是判断数据是否存在竞争、线程是否安全的依据。

happens-before原则内容如下

  1. 程序顺序原则:即在一个线程内必须保证语义串行性,也就是说按照代码顺序执行。
  2. 锁规则:解锁(unlock)操作必然发生在后续的同一个锁的加锁(lock)之前,也就是说,如果对于一个锁解锁后,再加锁,那么加锁的动作必须在解锁动作之后(同一个锁)。
  3.  volatile规则:volatile变量的写,先发生于读,这保证了volatle变量的可见性,简单的理解就是,volatile变量在每次被线程访问时,都强迫从主内存中读该变量的值,而当该变量发生变化时,又会强迫将最新的值刷新到主内存,任何时刻,不同的线程总是能够看到该变量的最新值。
  4. 线程启动规则:线程的start()方法先于它的每一个动作,即如果线程A在执行线程B的start方法之前修改了共享变量的值,那么当线程B执行start方法时,线程A对共享变量的修改对线程B可见
  5. 传递性:A先于B,B先于C那么A必然先于C
  6. 线程终止规则:线程的所有操作先于线程的终结,Thread.join()方法的作用是等待当前执行的线程终止。假设在线程B终止之前,修改了共享变量,线程A从线程B的join方法成功返回后,线程B对共享变量的修改将对线程A可见。
  7. 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测线程是否中断。
  8. 对象终结规则:对象的构造函数执行,结束先于finalize()方法

4.内存屏障

下面讲一下Java语言规范的内存屏障。

什么是内存屏障?

简单来说就是,如果这两行代码之间可能发生指令重排序,但是你不想让他们发生指令重排序,那么你就需要在这两行代码之间加上一行代码来防止它们进行指令重排序。加的这行代码就是内存屏障。

内存屏障是什么样的?

如下图所示:

其中的Load、store是Java内存模型的数据原子性操作。

怎么用这个内存屏障?

这个不用你操心,volatile已经帮你用好了。volatile的底层实现上是会有一个汇编的lock前缀,而这个lock前缀就已经实现了内存屏障。

5.小结

这篇文章我们主要讲了JMM,即Java内存模型,讲了JMM数据的原子性操作,讲了volatile的底层实现原理,讲了缓存一致协议,讲了有序性,讲了有序性的两大规则,讲了内存屏障。

内容很散,需要理解,需要自己把这些散的内容串起来。
 

这篇关于并发编程——5.JMM、可见性和有序性及volatile的底层实现原理的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/888041

相关文章

HTML5实现的移动端购物车自动结算功能示例代码

《HTML5实现的移动端购物车自动结算功能示例代码》本文介绍HTML5实现移动端购物车自动结算,通过WebStorage、事件监听、DOM操作等技术,确保实时更新与数据同步,优化性能及无障碍性,提升用... 目录1. 移动端购物车自动结算概述2. 数据存储与状态保存机制2.1 浏览器端的数据存储方式2.1.

基于 HTML5 Canvas 实现图片旋转与下载功能(完整代码展示)

《基于HTML5Canvas实现图片旋转与下载功能(完整代码展示)》本文将深入剖析一段基于HTML5Canvas的代码,该代码实现了图片的旋转(90度和180度)以及旋转后图片的下载... 目录一、引言二、html 结构分析三、css 样式分析四、JavaScript 功能实现一、引言在 Web 开发中,

Spring @Scheduled注解及工作原理

《Spring@Scheduled注解及工作原理》Spring的@Scheduled注解用于标记定时任务,无需额外库,需配置@EnableScheduling,设置fixedRate、fixedDe... 目录1.@Scheduled注解定义2.配置 @Scheduled2.1 开启定时任务支持2.2 创建

SpringBoot中使用Flux实现流式返回的方法小结

《SpringBoot中使用Flux实现流式返回的方法小结》文章介绍流式返回(StreamingResponse)在SpringBoot中通过Flux实现,优势包括提升用户体验、降低内存消耗、支持长连... 目录背景流式返回的核心概念与优势1. 提升用户体验2. 降低内存消耗3. 支持长连接与实时通信在Sp

Conda虚拟环境的复制和迁移的四种方法实现

《Conda虚拟环境的复制和迁移的四种方法实现》本文主要介绍了Conda虚拟环境的复制和迁移的四种方法实现,包括requirements.txt,environment.yml,conda-pack,... 目录在本机复制Conda虚拟环境相同操作系统之间复制环境方法一:requirements.txt方法

Spring Boot 实现 IP 限流的原理、实践与利弊解析

《SpringBoot实现IP限流的原理、实践与利弊解析》在SpringBoot中实现IP限流是一种简单而有效的方式来保障系统的稳定性和可用性,本文给大家介绍SpringBoot实现IP限... 目录一、引言二、IP 限流原理2.1 令牌桶算法2.2 漏桶算法三、使用场景3.1 防止恶意攻击3.2 控制资源

springboot下载接口限速功能实现

《springboot下载接口限速功能实现》通过Redis统计并发数动态调整每个用户带宽,核心逻辑为每秒读取并发送限定数据量,防止单用户占用过多资源,确保整体下载均衡且高效,本文给大家介绍spring... 目录 一、整体目标 二、涉及的主要类/方法✅ 三、核心流程图解(简化) 四、关键代码详解1️⃣ 设置

Nginx 配置跨域的实现及常见问题解决

《Nginx配置跨域的实现及常见问题解决》本文主要介绍了Nginx配置跨域的实现及常见问题解决,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来... 目录1. 跨域1.1 同源策略1.2 跨域资源共享(CORS)2. Nginx 配置跨域的场景2.1

Python中提取文件名扩展名的多种方法实现

《Python中提取文件名扩展名的多种方法实现》在Python编程中,经常会遇到需要从文件名中提取扩展名的场景,Python提供了多种方法来实现这一功能,不同方法适用于不同的场景和需求,包括os.pa... 目录技术背景实现步骤方法一:使用os.path.splitext方法二:使用pathlib模块方法三

CSS实现元素撑满剩余空间的五种方法

《CSS实现元素撑满剩余空间的五种方法》在日常开发中,我们经常需要让某个元素占据容器的剩余空间,本文将介绍5种不同的方法来实现这个需求,并分析各种方法的优缺点,感兴趣的朋友一起看看吧... css实现元素撑满剩余空间的5种方法 在日常开发中,我们经常需要让某个元素占据容器的剩余空间。这是一个常见的布局需求