热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

JVM级别内存屏障如何禁止指令重排序的

承接上文证明CPU指令是乱序执行的当多个cpu访问同一份数据的时候怎么保证数据的一致性?在最底层级别的控制有好多种:第一种叫关中断,就是访

承接上文证明CPU指令是乱序执行的

当多个cpu访问同一份数据的时候怎么保证数据的一致性?

在最底层级别的控制有好多种:

第一种叫关中断,就是访问任何数据的时候必须有一个中断信号量的存在。很多传统的cpu就是靠它实现的,从内存读东西的时候实际上是通过中断响应去读的,比如访问这块内存的时候把中断给关了,任何读也好,写也好,io操作中断全不响应,就没有人能打断你,因为把能打断我的所有的指令都给关了,只要没人打断你,就一定能保证数据的一致性,这也涉及到芯片中断的内容;

第二种就是CPU缓存一致性;

第三种就是系统屏障:cpu访问内存数据,把数据线锁住,只允许一颗cpu传数据,其他cpu就不能传,同一时刻只有一个cpu可以访问这个数据,







怎么防止指令重排序?

第一种情况是禁止编译器乱序,有的代码在编译的过程中就直接乱序了;

第二种是使用内存屏障阻止指令乱序执行。

随便写2个指令,在编译器编译的时候就可能会产生乱序,只要前后没有依赖关系就有可能产生,这是在编译阶段;有前后关联关系的是不能够随便换顺序的,比如x=1和y=x+1;没有关联关系的话,就有可能换顺序,比如x=1和y=2。

c语言的volatile,底层是通过这条指令禁止编译器将前后2个指令换顺序执行的。







a=1和b=d这两个指令在编译器编译的时候不能换顺序执行,这种被称为内存屏障。

volatile是指令级别的屏障 ,它也是一种特殊的指令;

不同的cpu,内存屏障的指令是不一样的,因特尔的内存屏障有lfence(读屏障)、sfence(写屏障)、mfence(不管读还是写都不能越过)。

内存屏障指令有很多种,不同的cpu是不同的指令,使用内存屏障来阻止指令的乱序执行。

除了lfence、mfence、sfence这些内存屏障指令之外的其他有哪些内存屏障?

在jvm中也存在内存屏障,jvm不是一台实体的机器,不像intel一样具备一个物理的cpu,jvm只是一个逻辑概念,jvm内存屏障一共规定了4类,所有实现jvm规范的虚拟机必须实现四个屏障:














读读指令中间加一个LoadLoad(LL屏障),读读就不能换顺序;

写写指令中间加一个SS屏障,写写就不能换顺序;

读写指令中间加一个LS屏障,读写指令不能换顺序。

规定了相邻的2个操作不能换顺序,就相当于一个屏障。







在cpu的基础之上构建了操作系统os来管理cpu;

jvm在os系统看来只是一个很普通的程序而已,jvm在自己的程序里规定了好多屏障,最终这些屏障的实现也得靠cpu和os提供的能力。

不能将jvm级别的内存屏障和系统级别的内存屏障混在一起。

jvm是c++写的,在里面规定了自己写的内存屏障,jvm想实现内存屏障最终还得映射成cpu的内存屏障。

volatile的实现细节

volatile两大作用:

第一个是保障可见性:一个cpu改了的内容另外一个cpu马上可见;

第二个作用是禁止指令重排序:比如new对象时候的三个指令不会换顺序执行。

volatile其实是一个普通的关键字,无非是修饰了某块内存和某块变量。







在jvm层做一个特殊的操作,jvm规定volatile所修饰的变量,对这块内存做写操作的时候,在它前面必须加一个屏障SS,后面加个屏障叫SL,前面的所有写操作必须先执行完,然后再往里面写,必须等我写完,别人才能读。







必须等我读完别人才能读,必须等我读完别人才能写。

volatile修饰的内存,对它的任何访问全都换不了顺序,







这个只是jvm自己规定的,最终一定要体现在cpu级别的,cpu级别怎么实现的?

不同的jvm有不同的实现,最流行的jvm hotspot oracle所提供的,hotspot volatile底层到底怎么实现的?

java写的volatile,jvm编译执行,java是解释执行的,所以要想了解volatile怎么实现的,得去读hotspot解释器的代码,看是怎么解释完成的?

二进制码解释器的实现类是bytecodeInterpreter.cpp,可以看到是怎么解释运行volatile的,














可以看到最终的实现是一条汇编指令,不是lfence 、mfence、sfence。

两条指令可以乱序执行,多线程的情况会读到中间状态的各种各样的情形,所以必须得实现一种机制,不让两条指令乱序执行,最底层cpu级别实现了指令级别的机制,编译器级别也实现了禁止编译器优化的指令,jvm级别也实现了自己的逻辑操作,但是jvm级别的指令最终要落到cpu级别,cpu级别最终是怎么落上去的?

is_MP方法是判断是否为多个处理器(或多个cpu),如果是的话就执行lock addl,l是lense的意思,rsp或esp是寄存器指令 ,把某个寄存器的值加上一个0,为什么lock指令可以实现禁止指令重排序?







每种cpu最终的对于屏障的实现应该对应特定的指令,比如lfense、mfense指令,但是hotspot偷了个懒,cpu通过执行lock指令来实现:当cpu或线程访问某个内存的时候,会锁住总线,不允许其他cpu去读或去写,必须等我读完写完,其他才可以继续,这样就不会乱序了。

lock主要用于在多处理器中执行指令时对共享内存的独占使用,将当前处理器对应的缓存刷新到内存并使其他处理器对应的缓存失效,其他处理器得重新读,另外还提供了禁止指令重排序即无法越过内存屏障的作用。

凡是在lock前后加任何指令都不能越过,因为它是一个全屏障。







对某个寄存器加个0,这个操作跟没有是一样的,没有任何作用,简单称之为空指令,

为什么有一个空指令存在,因lock指令在锁总线的时候,这条指令后面必须跟一条指令,后面指令不能为空,所以后面得跟一条指令,但是后面跟的又不能有任何作用,如果有任何作用,中间改了别的值,也不对,所以设计了这么一个指令,往某个寄存器上加了个0,跟没有操作一样,主要为了迎合lock指令的参数要求即后面必须跟一个指令,其实只要有一个lock指令就足够了。

虽然lock后面加了个空操作,但是lock起着锁总线的作用。

因特尔cpu设计的禁止重排序指令是mfense和lfense,其实也没有想到hotspot会有lock这样的操作,作为hotspot来讲,不同的cpu应该做对应的优化,不应该为了偷懒就直接使用了一条lock指令。

lock其实是很多底层的实现,比如synchronized本身也是用lock来实现的,volatile也是。

那lock到底是什么?

lock并不能简单的认为是锁住总线,想了解lock指令就相当于你要了解cpu级别的并发控制到底有哪些种,cpu级别的对于多线程的内存并发控制:

第一个叫关中断;

第二种是缓存一致性协议;

第三种是系统屏障,系统屏障本身第一个级别是编译级别的屏障,第二个级别是指令级别的屏障;

第四种是总线和缓存锁lock cmpxchg memory或lock addl,这条指令要么缓存锁,要么总线锁,所以它未必一定是总线锁,想在cpu级别控制整个并发,只有这四种 。

在操作系统在这四种最基本的操作之上会提供一系列的内核级别的api,让你调用api去实现各种各样的锁。

各种各样的锁包括哪些东西?

从linux内核的角度大概是包括这些内容:信号量和P-V原语(也就是+-的操作),还有一个是互斥。

在这些个api的基础之上,还有互斥量MUTEX、自旋锁CAS。

在自旋锁的基础上还会有读写锁、中断控制、内核抢占,SEQ锁、序列锁、RCU锁。

在多cpu访问下必须要考虑访问同一个数据会出现数据不一致的问题,cpu级别提供了4种控制的方式 ,在这4种控制方式的基础之上,不同的操作系统提供了一系列内核级别的api,在api的基础之上提供了一系列的锁来做并发控制。

在这些内核基础之上,才完成了jvm级别的锁控制,jvm级别除了原来的synchronized之外,还有juc级别的锁,比如cas级别的automic开头的原子类的操作。

在juc里面,在所有底层的基础之上,才会诞生了java这一系列的锁以及自己实现锁所需要的最原始的零件。

要了解jvm锁,建议从底层开始了解,所有上层的东西就是对最底层的一个封装而已。

jvm封装了pthread和kthread,这种是linux内核级别的api,它封装了cpu级别的4种方式,提供的一系列的同步机制。





推荐阅读
  • 面试题总结_2019年全网最热门的123个Java并发面试题总结
    面试题总结_2019年全网最热门的123个Java并发面试题总结 ... [详细]
  • 2020年9月15日,Oracle正式发布了最新的JDK 15版本。本次更新带来了许多新特性,包括隐藏类、EdDSA签名算法、模式匹配、记录类、封闭类和文本块等。 ... [详细]
  • 本文详细介绍了Java反射机制的基本概念、获取Class对象的方法、反射的主要功能及其在实际开发中的应用。通过具体示例,帮助读者更好地理解和使用Java反射。 ... [详细]
  • 零拷贝技术是提高I/O性能的重要手段,常用于Java NIO、Netty、Kafka等框架中。本文将详细解析零拷贝技术的原理及其应用。 ... [详细]
  • 在多线程并发环境中,普通变量的操作往往是线程不安全的。本文通过一个简单的例子,展示了如何使用 AtomicInteger 类及其核心的 CAS 无锁算法来保证线程安全。 ... [详细]
  • 本指南从零开始介绍Scala编程语言的基础知识,重点讲解了Scala解释器REPL(读取-求值-打印-循环)的使用方法。REPL是Scala开发中的重要工具,能够帮助初学者快速理解和实践Scala的基本语法和特性。通过详细的示例和练习,读者将能够熟练掌握Scala的基础概念和编程技巧。 ... [详细]
  • 如何利用Java 5 Executor框架高效构建和管理线程池
    Java 5 引入了 Executor 框架,为开发人员提供了一种高效管理和构建线程池的方法。该框架通过将任务提交与任务执行分离,简化了多线程编程的复杂性。利用 Executor 框架,开发人员可以更灵活地控制线程的创建、分配和管理,从而提高服务器端应用的性能和响应能力。此外,该框架还提供了多种线程池实现,如固定线程池、缓存线程池和单线程池,以适应不同的应用场景和需求。 ... [详细]
  • 解决Jenkins编译过程中ERROR: Failed to Parse POMs的问题
    在使用Jenkins进行自动化构建时,有时会遇到“ERROR: Failed to parse POMs”的错误。本文将详细分析该问题的原因,并提供有效的解决方案。 ... [详细]
  • 本文详细介绍了 Java 网站开发的相关资源和步骤,包括常用网站、开发环境和框架选择。 ... [详细]
  • Hadoop的文件操作位于包org.apache.hadoop.fs里面,能够进行新建、删除、修改等操作。比较重要的几个类:(1)Configurati ... [详细]
  • Java高并发与多线程(二):线程的实现方式详解
    本文将深入探讨Java中线程的三种主要实现方式,包括继承Thread类、实现Runnable接口和实现Callable接口,并分析它们之间的异同及其应用场景。 ... [详细]
  • 线程能否先以安全方式获取对象,再进行非安全发布? ... [详细]
  • 在 Linux 环境下,多线程编程是实现高效并发处理的重要技术。本文通过具体的实战案例,详细分析了多线程编程的关键技术和常见问题。文章首先介绍了多线程的基本概念和创建方法,然后通过实例代码展示了如何使用 pthreads 库进行线程同步和通信。此外,还探讨了多线程程序中的性能优化技巧和调试方法,为开发者提供了宝贵的实践经验。 ... [详细]
  • Java中不同类型的常量池(字符串常量池、Class常量池和运行时常量池)的对比与关联分析
    在研究Java虚拟机的过程中,笔者发现存在多种类型的常量池,包括字符串常量池、Class常量池和运行时常量池。通过查阅CSDN、博客园等相关资料,对这些常量池的特性、用途及其相互关系进行了详细探讨。本文将深入分析这三种常量池的差异与联系,帮助读者更好地理解Java虚拟机的内部机制。 ... [详细]
  • 深入浅析JVM垃圾回收机制与收集器概述
    本文基于《深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)》的阅读心得进行整理,详细探讨了JVM的垃圾回收机制及其各类收集器的特点与应用场景。通过分析不同垃圾收集器的工作原理和性能表现,帮助读者深入了解JVM内存管理的核心技术,为优化Java应用程序提供实用指导。 ... [详细]
author-avatar
YI恐龙_554
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有