并发编程——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

相关文章

SpringBoot集成redisson实现延时队列教程

《SpringBoot集成redisson实现延时队列教程》文章介绍了使用Redisson实现延迟队列的完整步骤,包括依赖导入、Redis配置、工具类封装、业务枚举定义、执行器实现、Bean创建、消费... 目录1、先给项目导入Redisson依赖2、配置redis3、创建 RedissonConfig 配

Python的Darts库实现时间序列预测

《Python的Darts库实现时间序列预测》Darts一个集统计、机器学习与深度学习模型于一体的Python时间序列预测库,本文主要介绍了Python的Darts库实现时间序列预测,感兴趣的可以了解... 目录目录一、什么是 Darts?二、安装与基本配置安装 Darts导入基础模块三、时间序列数据结构与

Python使用FastAPI实现大文件分片上传与断点续传功能

《Python使用FastAPI实现大文件分片上传与断点续传功能》大文件直传常遇到超时、网络抖动失败、失败后只能重传的问题,分片上传+断点续传可以把大文件拆成若干小块逐个上传,并在中断后从已完成分片继... 目录一、接口设计二、服务端实现(FastAPI)2.1 运行环境2.2 目录结构建议2.3 serv

C#实现千万数据秒级导入的代码

《C#实现千万数据秒级导入的代码》在实际开发中excel导入很常见,现代社会中很容易遇到大数据处理业务,所以本文我就给大家分享一下千万数据秒级导入怎么实现,文中有详细的代码示例供大家参考,需要的朋友可... 目录前言一、数据存储二、处理逻辑优化前代码处理逻辑优化后的代码总结前言在实际开发中excel导入很

SpringBoot+RustFS 实现文件切片极速上传的实例代码

《SpringBoot+RustFS实现文件切片极速上传的实例代码》本文介绍利用SpringBoot和RustFS构建高性能文件切片上传系统,实现大文件秒传、断点续传和分片上传等功能,具有一定的参考... 目录一、为什么选择 RustFS + SpringBoot?二、环境准备与部署2.1 安装 RustF

Nginx部署HTTP/3的实现步骤

《Nginx部署HTTP/3的实现步骤》本文介绍了在Nginx中部署HTTP/3的详细步骤,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学... 目录前提条件第一步:安装必要的依赖库第二步:获取并构建 BoringSSL第三步:获取 Nginx

MySQL的JDBC编程详解

《MySQL的JDBC编程详解》:本文主要介绍MySQL的JDBC编程,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录前言一、前置知识1. 引入依赖2. 认识 url二、JDBC 操作流程1. JDBC 的写操作2. JDBC 的读操作总结前言本文介绍了mysq

MyBatis Plus实现时间字段自动填充的完整方案

《MyBatisPlus实现时间字段自动填充的完整方案》在日常开发中,我们经常需要记录数据的创建时间和更新时间,传统的做法是在每次插入或更新操作时手动设置这些时间字段,这种方式不仅繁琐,还容易遗漏,... 目录前言解决目标技术栈实现步骤1. 实体类注解配置2. 创建元数据处理器3. 服务层代码优化填充机制详

Python实现Excel批量样式修改器(附完整代码)

《Python实现Excel批量样式修改器(附完整代码)》这篇文章主要为大家详细介绍了如何使用Python实现一个Excel批量样式修改器,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一... 目录前言功能特性核心功能界面特性系统要求安装说明使用指南基本操作流程高级功能技术实现核心技术栈关键函

Java实现字节字符转bcd编码

《Java实现字节字符转bcd编码》BCD是一种将十进制数字编码为二进制的表示方式,常用于数字显示和存储,本文将介绍如何在Java中实现字节字符转BCD码的过程,需要的小伙伴可以了解下... 目录前言BCD码是什么Java实现字节转bcd编码方法补充总结前言BCD码(Binary-Coded Decima