当前位置: 首页 > news >正文

Java并发机制的底层实现原理


Java 代码在编译后会变成 Java 字节码 ,字节码被类加载器加载到 JVM 里, JVM 执行字节码,最终转化为汇编指令在 CPU 上执行。

Java 中使用的并发机制依赖于 JVM 的实现和 CPU 的指令。


1. volatile


1.1 作用

在 Java 中, 如果一个字段被 volatile 修饰,那么 Java 内存模型将确保所有线程看到这个变量的值的一致的。保证了 可见性 和 有序性。


1.2 volatile 是如何保证可见性的呢?

有 volatile 变量修饰符的共享变量的 Java 代码, 在转变为汇编语言时候 会对其加上特殊指令(根据硬件不同有差异,但是效果一致)。有这个特殊指令的汇编代码在多核处理器上运行时会发生两件事:

  1. 将当前处理器缓存行的数据写回到系统内存。
  2. 这个写回内存的操作会使其他缓存了该内存地址的 CPU 里的数据无效。

为了提高处理速度,处理器不直接和内存进行通信,而是先将系统内存的数据读到内部缓存(LI、L2 或其他 )后再进行操作,但操作完不知道何时会写到内存。如果对声明了volatile 的变量进行写操作,JVM 就会向处理器发送一条有 lock 前缀的指令,将这个变量所在缓存行的数据写回到系统内存。

但是,就算写回到内存,如果其他处理器缓存的值还是旧的,再执行计算操作就会有问题。所以,在多处理器下,为了保证各个处理器的缓存是一至的,就会实现缓存一致性协议。每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改后,它就会将当前处理的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存把数据读到处理器缓存里。

下面具体讲解 volatile 的两条实现原则。

1)lock 前缀指令会引起处理器缓存回写到内存。

lock 前缀指令导致在执行指令期间,声言处理器的 LOCK# 信号。在多处理器环境中,LOCK# 信号确保在声言该信号期间,处理器可以独占任何共享内存。但是,在最近的处理器里,LOCK #信号一般不锁总线,而是锁缓存,毕竟锁总线的开销比较大。对于 Intel486 和 Pentium 处理器,在锁操作时,总是在总线上声言LOCK#信号。但在P6和目前的处理器中,如果访问的内存区域已经缓存在处理器内部,则不会声言LOCK#信号。相反,它会锁定这块内存区域的缓存并回写到内存,同时使用缓存一致性机制来确保修改的原子性,此操作被称为“缓存锁定”。缓存一致性机制会阻止同时修改由两个以上处理器缓存的内存区域数据。

2) 一个处理器的缓存回写到内存会导致其他处理器的缓存无效。

IA-32处理器和 Intel64处理器使用 MESI(修改、独占、共享、无效)控制协议去维护内部缓存和其他处理器缓存的一致性。在多核处理器系统中进行操作的时候,IA-32 和Intel 64处理器能嗅探其他处理器访问系统内存和它们的内部缓存。处理器使用嗅探技术保证它的内部缓存、系统内存和甚他处理器的缓存的数据在总线上的一致性。例如,在 Pentium 和 P6 family 处理器中,如果通过嗅探一个处理器来检测其他处理器打算写内存地址,而这个地址当前处于共享状态,那么正在嗅探的处理器将使它的缓存行无效,在下次访问相同内存地址时,强制执行缓存行填充。



2. synchronized


在 Java 中,每一个对象都可以作为锁。synchronized 具体表现形式有以下三种:

  • 修饰普通方法:锁的是当前实例对象。
  • 修饰类方法:锁的锁当前类的 Class 对象。
  • 修饰同步方法:锁的锁 synchronized 括号里配置的对象。

JVM 基于进入和退出 Monitor 对象来实现同步。

3. 原子操作的实现原理

原子操作 意为 “不可中断的一个或一些列操作”。

3.1 术语定义

CPU 术语定义


3.2 处理器如何实现原子操作

最新的处理器能自动保证单个处理器对同一个缓存行里进行的操作是原子的,但是复杂的内存操作处理是不能保证其原子性的。

因此,处理器提供总线锁定和缓存行锁定两个机制来保证复杂内存操作的原子性。

3.2.1 使用总线锁定保证原子性

如果多个处理器同时对共享变量进行读改写操作(i++就是经典的读改与操作),那么共享变量就会被多个处理器同时操作,这样读改写操作就不是原子的,操作完之后共享变量的值会和期望的不一致。

原因可能是多个处理器同时从各自的缓存中读取变量 i, 分别进行加 1 操作,然后分别写人系统内存中。那么,想要保证读改写共享变量的操作是原子的,就必须保证 CPU1 读改写共享变量的时候,CPU2 不能操作缓存该共享变量内存地址的缓存。

处理器使用总线锁定来解决这个问题。所谓总线锁定就是使用处理器提供的一个 LOCK# 信号,当一个处理器在总线上输出此信号时,其他处理器的请求将被阻塞住,该处理器可以独占共享内存。

3.2.2 使用缓存行锁定保证原子性

在同一时刻,我们保证对某个内存地址的操作是原子性即可,但总线锁定把 CPU 和内存之间的通信锁住了,这使得锁定期间,其他处理器不能操作其他内存地址的数据,所以总线锁定的开销比较大,目前处理器在某些场合下使用缓存锁定代替总线锁定来进行优化。

频繁使用的内存会缓存在处理器的 L1 ,L2 和 L3 高速缓存里,那么原子操作就可以直接在处理器内部缓存中进行,并不需要声明总线锁定,在目前的处理器中可以使用 “缓存锁定” 的方式来实现复杂的原子性。

所谓"缓存锁定"是指内存区域如果被缓存在处理器的缓存行中,并且在 Lock 操作期间被锁定,那么当它执行锁操作回写到内存时,处理器不在总线上声言 LOCK# 信号,而是修改内部的内存地址,并允许它的缓存一致性机制来保证操作的原子性。缓存一致性机制会阻止同时修改由两个以上处理器缓存的内存区域数据。当其他处理器回写已被锁定的缓存行的数据时,会使缓存行无效。

两种情况下,处理器不会使用缓存行锁定:1️⃣ 当操作的数据没有缓存在同一缓存行时;2️⃣ 有些处理器不支持缓存行;


3.2.3 Java 如何实现原子操作

在 Java 中可以通过 循环 CAS 和 锁 的方式来实现原子操作。



http://www.xdnf.cn/news/1474471.html

相关文章:

  • 程序化广告快速上手:零基础入门第一课
  • 洛谷 P1591 阶乘数码-普及-
  • PyTorch生成式人工智能——深度分层变分自编码器(NVAE)详解与实现
  • 贪心算法应用:基因编辑靶点选择问题详解
  • 【C++】类和对象(三)
  • Git reset 回退版本
  • stunnel实现TCP双向认证加密
  • Custom SRP - Complex Maps
  • 顺丰,途虎养车,优博讯,得物,作业帮,途游游戏,三七互娱,汤臣倍健,游卡,快手26届秋招内推
  • JVM如何排查OOM
  • 01.单例模式基类模块
  • 微信小程序携带token跳转h5, h5再返回微信小程序
  • Knative Serving:ABP 应用的 scale-to-zero 与并发模型
  • 【Python 】入门:安装教程+入门语法
  • 使用 C# .NETCore 实现MongoDB
  • OpenAI新论文:Why Language Models Hallucinate
  • 【黑客技术零基础入门】2W字零基础小白黑客学习路线,知识体系(附学习路线图)
  • 【C++】C++11的可变参数模板、emplace接口、类的新功能
  • 《云原生微服务治理进阶:隐性风险根除与全链路能力构建》
  • 旧电脑改造服务器1:启动盘制作
  • Element-Plus
  • Nestjs框架: 基于权限的精细化权限控制方案与 CASL 在 Node.js 中的应用实践
  • 【Mysql-installer-community-8.0.26.0】Mysql 社区版(8.0.26.0) 在Window 系统的默认安装配置
  • Nikto 漏洞扫描工具使用指南
  • 管家婆辉煌系列软件多仓库出库操作指南
  • Kubernetes (k8s)
  • MySQL连接字符串中的安全与性能参数详解
  • Monorepo 是什么?如何使用并写自己的第三方库
  • 聊聊OAuth2.0和OIDC
  • 音转文模型对比FunASR与Faster_whisper