挥发性有很多文章。我已经阅读了许多相关文章,但是我总是觉得我不太了解,但是我不能说为什么。它可能太追求了基本实施的原则,总是想问为什么。
撰写本文的目的非常简单,也就是说,突然理解为什么要像这样设计的挥发性。当然,要分享好东西,因此有本文。
从硬件到软件,再到特定案例,要讨论挥发性的底部原理,文章相对较长,但是收集后,可以阅读。
最初的CPU没有缓存区域,CPU直接读写。但是,存在问题。CPU的运行效率是读取和写入内存效率之间差距的100倍以上。始终无法执行CPU执行写作操作的时钟周期,然后等待内存的执行超过超过100个时钟周期。
因此,在CPU和内存之间添加了缓存(CPU缓存:缓存内存),这是CPU和内存之间的临时内存。这就像MySQL具有瓶颈时一样,我们将考虑通过简短的CACE DATA。CPU缓存的出现是为了解决CPU和内存之间无与伦比处理的问题。
目前,我们有一个粗略的图片:
但是,考虑进一步优化数据的调度,CPU缓存分为主要缓存,次要缓存和第三级高速缓存。它们主要用于优化数据的吞吐量和临时存款并提高执行效率。主流CPU通常使用三层缓存:
在上述细分之后,您可以进一步完善上面的图片:
这里的另一个概念:类似CACHE,它是CPU缓存存储的数据的最小单元,该数据将稍后使用。上面的CPU高速缓存也称为高速缓存。
引入缓存后,每个CPU的处理过程是:首先计算高速缓存中所需数据的计算。计算CPU时,请直接从高速缓存读取数据,然后将其写入缓存。整个操作过程完成后,缓存中的数据与主内存同步。
There is no problem if it is a single -core CPU.But in a multi -nuclear system, each CPU may cache the same data into its own high -speed cache, which has a problem of cache data consistency.
The CPU layer provides two solutions: bus lock and cache consistency.
The front -end bus (also known as the CPU bus) is the main road connected by all CPUs to the chipset. It is responsible for the communication between the CPU and the outside world, including high -speed cache, memory, and North Bridge.The address signal specifies the components they want to access and transmit it through the data bus.
For example, when CPU1 wants to operate shared memory data, first send a LOCK#signal on the bus, and other processors cannot operate cache the cache of the shared variable memory address, that is, block other CPUs, so that the processor can enjoy this exclusive to enjoy this.Shared memory.
Obviously, the cost is very expensive, so in order to reduce the lock size, the CPU introduced the cache lock.
Cache consistency: On the whole, the cache consistency mechanism is that when a CPU operates the data in the cache, it will notify other CPUs to abandon the cache stored inside them or read it from the main memory.
The core mechanism of the cache lock is based on the cache consistency protocol, that is, the cache wrote of a processor's cache to the memory will cause the cache of other processors to be invalid.protocol.
Cache consistency is a protocol. The specific implementation of different processors will be different. MESI is a relatively common cache consistency protocol implementation.
The MESI protocol is named after several states of the cache line (the full name is Modified, Exclusive, Share or Invalid).The protocol requires two states at each cache. Each data unit may be in one of the four states: M, E, S, and I. The meaning of various states is as follows:
The above state will continue to change with the operation of different CPUs:
For the MESI protocol, the following principles will be followed from the perspective of CPU reading and writing:
CPU reading data: When the CPU needs to read the data, if the state of its cache is i, you need to read from the memory and turn your own state into S.Value, but before that, you must wait for the monitoring results of other CPUs. If other CPUs also have the cache of the data and the state is M, you need to wait for it to update the cache to the memory before reading it.
CPU writing data: When the CPU needs to write data, it can be executed only when its cache is M or E, otherwise a special RFO instruction (Read or Ownership, this is a bus transaction), notify other CPU settings cacheInvalid (i), the performance overhead is relatively large in this case.After the writing is completed, the cache state is M.
After the introduction of bus lock or cache consistency protocol, the structure of the CPU, cache, and memory becomes below:
In the interaction of the MESI protocol above, we can already see a large number of message transmission (monitoring processing) between each CPU.The consistency message transmission of cache takes time, which causes delay when switching.A CPU changes in data in the cache may need to obtain other CPUs to continue, and during this period, it is blocked.
The process of waiting for the confirmation process will block the processor and reduce the performance of the processor.And this waiting is far longer than the execution of a instruction.In order to avoid waste of resources, the CPU introduced the Store Buffres.
Based on the storage cache, the CPU will write to the memory data to write it in the Store Buffres, send messages at the same time, and then continue to process other instructions.The data will be submitted when they receive the failure confirmation of all other CPUs (Invalidate Acknowledge).
例如,解释商店Buffres的执行过程:例如,内存中变量A的值从1个从1修改为166。
在第一步中,CPU 0将A = 66写入Store Buffres,然后将无效的消息发送给其他CPU。您可以继续执行其他说明,而无需等待其他CPU。
在第二步中,当CPU-0接收到无效通知的CPU通知时,然后将商店Buffres中的共享变量同步到缓存和主存储器。
商店前进(存储转发)商店Buffres的引入提高了CPU的效率,但它也带来了新的问题。在上面提到的第一步中,商店Buffers中的数据尚未与CPU-0高速缓存同步。如果CPU-0此时需要读取变量a,则缓存中的数据不是最新的,因此CPU需要firstread是否值得在buffres中值得。在您自己的缓存中读取它,这是So -call的“存储前方”。
故障队列CPU将在向其他CPU发送消息时将数据写入商店Buffres。由于商店缓冲空间很小,而其他CPU可能正在处理其他事情,因此无法及时回复,消息将在等待。
为了避免接收消息的CPU无法及时处理无效的故障数据,CPU指令等待,并将异步消息队列添加到接收CPU中。消息发送者将数据失败消息发送到此队列,接收CPU并返回收据。发件人CPU可以继续执行以下操作。接收器CPU慢慢处理消息“入侵”。
在上述优化之后,CPU不仅可以确保效率并确保缓存的一致性。在大多数情况下,它还可以根据商店Buffres和故障队列的异步处理来接受CPU的短延迟。
但是,在多线程线程的极端情况下,缓存数据仍然是不一致的。这次,CPU-1正忙于处理其他业务,没有时间处理消息队列,并且CPU-1处理业务再次使用变量,此时,这将导致值得阅读的值是旧价值。
由于CPU缓存优化,无法感知以下说明中的这种说明。看来指令之间的执行顺序是混乱的。对于这种现象,我们通常被称为“ CPU混沌执行”。
随机序列执行是多线程程序错误的原因。解决方案很简单:禁用CPU缓存优化。但是,在大多数情况下,共享没有问题。直接残疾将导致整体绩效下降,并且会丢失。因此,它提供了多线程共享场景的解决方案机制:内存屏障机构。
使用内存屏障后,它将确保在编写数据时执行所有指令,以便可以立即将修改的数据暴露于其他CPU。读取数据时,可以确保消耗所有“失败队列”消息。CPU。然后根据推论消息判断其缓存状态,然后正确读取和编写数据。
CPU级别的内存屏障在CPU级别提供了三种类型的内存屏障:
以下由伪代码解释:
在上面的示例中,可以防止指令通过内存屏障重新解散,这可以获得预期的结果。
简而言之,内存屏障的作用可以通过防止CPU无序执行来确保多线程中共享数据的可见性。因此,它如何在JVM中解决此问题?也就是说,如何控制程序员?这涉及我们的关键字我们想谈论。
内存屏障解决了由CPU缓存优化引起的指令执行的顺序和可见性,但是不同硬件系统提供的“内存屏障”指令不同。作为开发人员,不必熟悉所有内存屏障说明。Java均匀地封装了不同的内存屏障指令。开发人员只需要注意程序逻辑开发和记忆障碍即可。
该包装解决方案的模型是我们经常指的JAVA内存模型,称为JMM。JMM的核心值是解决可见性和顺序。它是硬件模型的抽象,它定义了共享内存中多线程程序的行为规格。
这组规范可以通过记忆的读取操作确保指令的正确性,并解决由CPU多级缓存,处理器优化和指令分类引起的内存访问问题,并确保并发可见性场景。
本质上,JMM将硬件底层的问题抽象到JVM级别,阻止了每个平台的硬件差异,然后将CPU级别提供的内存屏障指令以及编译器的限制已解决,以解决并解决一致性。问题。
将内存分为JMM抽象模型中的主要内存和工作记忆:
该线程是CPU调度的最小单元。线程之间共享变量值的传输必须通过主内存完成。
JMM抽象模型结构图如下:
JMM内存模型只是概述::
如果线程A需要与线程B进行通信,请首先将本地缓存中的数据更新为主内存,然后从主内存中获得线程B。JMM通过控制JAVA程序提供了对Java程序的内存可见保证主内存与每个线程的本地内存之间的交互。
除了重新分配硬件级别外,Java编译器还将重新安排指令以提高性能。Java规范规定了JVM线程在内部维护语义词素,即最终,即最终程序的结果等于其有序执行的结果,指令的执行顺序与代码顺序不一致。此过程称为指令的重分。
可以通过处理器特性(CPU多阶段缓存系统,多核处理器等)对JVM进行适当的分类,以便机器指令可以更好地满足CPU的执行特性并最大程度地提高机器的性能。
从源代码到最终执行示例图:
其中,有2和3属于CPU执行阶段的重分类阶段,其中1属于编译器阶段的重分类。编译器将符合发生的规则和AS-IF-的指令重排。串行语义。
以前发生的规则发生:如果在b和b hapens之前进行h hapens-b hapens-b hapens-需要确保hapepens-be c.。
AS-IF-Serial语义:不管如何对其进行排序(编译器和处理器以并行改进),都无法更改(单线程)程序的执行结果。编译器,运行时和处理器必须遵守AS- 如果是串行语义。
为了使处理器重新分类,JMM要求Java编译器在生成指令序列时将插入特定类型的内存屏障指令以禁止特定类型的处理重型分类。
我了解CPU的内存类别,并且内存屏障在JMM中分为四个类别:
其中,Storeload屏障具有前三个障碍的效果,但是开销的性能非常大。
为了实现挥发性的记忆语义,JMM将限制这两种类型的重分类类型。下面的图是编译器编译器的挥发性消除规则形成。
可以从图中获得基本规则:
为了实现挥发性的内存语义,编译器在生成字节代码以禁止特定类型的处理器进行排序时,将在指令序列中插入内存屏障。对于编译器,几乎不可能找到最佳布局的总数最小化插入障碍。为此,JMM采用了保守的策略。以下是基于保守策略的JMM记忆障碍插入策略:
在保守策略中,挥发性写入插入记忆屏障后产生的指令序列图:
在保守策略中,挥发性读取插入内存屏障后生成的指令序列图:
与挥发性有关的JVM内存说明为::
操作可变修改的挥发性时,需要满足以下规则:
通过上述分析,从理论层面清楚地解释了挥发性关键字的来源以及其修改的变量的可见性和顺序。LET查看一个可见示例。
示例代码如下:
根据上面的理论知识,首先猜测该线程在上面打印了什么?首先打印“螺纹线程来修改共享变量initflag”,然后打印“ threadb当前线程螺纹螺纹当前嗅探initflag 0”状态的“ threadb current thread treendb”。?
当该程序真正执行时,会发现整个线程在while循环中被阻止,而无需打印第2条。在此时间,JMM操作如下所示:
尽管线程A已将Initflag更改为true并最终将其同步回主内存,但始终是从工作内存中读取线程B中的Initflag读取的 - 读取线程B中的读取,因此不会退出死周期。
在挥发性修饰符被添加到变量initflag之后:
JMM操作如下所示:
添加挥发性修改后,将打印两个日志。这是因为在添加挥发性关键字后,将有一个锁定指令,使用缓存一致性协议,线程B始终嗅探是否更改了initflag。更改为i(无效状态),然后从主内存中重新阅读。
尽管波动性保证了共享变量的可见性和顺序,但并不能保证原子质。
以共同的自我提示操作(++)为例,通常将其分为三个步骤:
让我们分析此过程中将发生的线程安全问题:
在第一步中,线程A和B同时获得计数的初始值。这个步骤很好。
在第二步中,线程自我提出的计数并回信,但是Thread B目前也已经获得了计数,并且将不再获得线程的值。因此,线程安全性问题可能会问,是否应该在增加并重新加载后将其他通知其他CPU缓存失败?我们需要知道,重新获得的前提是阅读。当螺纹a写回去时,线程B具有计数的值,并且没有重新阅读的场景。在添加说明中,没有任何场景需要从缓存中再次读取。
波动性关键字仅保证可见性,因此在以下情况下,您需要使用锁来确保原子质:
因此,如果您想使用挥发性变量提供理想的线程安全性,则必须同时满足两个条件:
换句话说,修改后的变量值独立于任何程序的状态,包括变量的当前状态。
使用布尔状态徽标表示重要的一次性事件,例如完成初始化或请求停止。
在执行Dowork()的过程中,线程2可能会呼叫关闭,因此布尔变量必须波动。
该州标记的公共特征之一是只有一个州的conversion依。ShutDownRequest徽标从false转换为true,并且程序停止。此模式可以扩展过渡的状态徽标,但是只有在未检测到转换周期(从false到true,然后转换为false)时才可以扩展。)。此外,还需要一些原子状态转换机制,例如原子变量。
在缺乏同步的情况下,对象引用的更新值可能与参考值(由另一线程编写)和对象状态的旧值同时遇到。
该场景将出现在著名的双重检查锁定锁上:
其中,Singlet的第三个单元组由多个步骤(分配的内存空间,初始化对象,并将对象指向分布式内存空间)执行。对于某些编译器,出于性能原因,将对第二步骤和第三步进行排序(第二和第三步(分配内存空间,将对象指向内存空间的分配,初始化对象)。这样,线程可能会获得一个实例未完全初始化。
场景:定期“释放”观察结果,用于内部使用程序。例如,传感器感知温度,线程每隔几秒钟读取传感器,并更新当前挥发性修饰器变量。其他线程可以读取此变量并查看该变量任何时候的最新温度。
另一种情况是应用程序收集统计信息的应用程序。例如,记录最后的登录用户名,并反复使用lastuser引用来发布其他程序的值。
挥发性bean模式的基本原理是,许多框架为脆弱数据持有人(例如httpsession)提供容器,但是这些容器中的对象必须是螺纹-safe.in the volatile bean模式,Javabean的所有数据成员都是类型挥发性,而Getter和Setter方法必须非常普通 - 也就是说,它不包括约束。
如果读取操作远远超过写作操作,则可以将其与内部锁和挥发性变量结合使用,以降低公共代码路径的成本。
例如,下一个线程的安全计数代码是原子,可确保增量操作是原子,并且使用当前结果的可见性。如果更新不经常,则该方法可以实现更好的性能,因为阅读路径仅涉及挥发性读取操作,通常比不受欢迎的锁定费用要好。
使用锁执行更改,并使用挥发性仅读取。挥发性允许多个线程同时执行阅读操作。
本文首先从硬件级别分析了CPU处理机制。为了优化CPU,已引入缓存。为了进一步优化商店Buffres的引入,Store Buffres导致了缓存一致性。因此,有一个总线锁定和缓存一致性协议(EMSI实现),因此可以使用CPU的内存屏障机制。
CPU的内存屏障反映在Java编程语言中。使用JAVA内存模型(JMM),JMM阻止了底部硬件的差异,并提供了统一的操作和顺序。
之后,挥发性及其负面案例的作用立即没有线程安全保证。在本文中,一个例子说明了使用挥发性的示例。
据推测,通过本文,您完全理解了与挥发性有关的知识?来吧,跟随一波。
博客作者简介:“ Springboot Technology Inner Book”技术书籍作者,喜欢学习技术,撰写技术干货文章。
公共帐户:博客作者的公共帐户“计划的新愿景”,欢迎关注?
技术交流:请联系博客微信:Zhuan2quan
参考文章:
https://juejin.cn/post/6876395693854949389
https://zhuanlan.zhihu.com/p/43526907
https://www.chinacion.cn/article/8093.html
https://blog.csdn.net/u012988901/article/details/111313057
https://blog.csdn.net/fumitzuki/article/details/81630048
https://www.cnblogs.com/yaowen/p/11240540.html
https://blog.csdn.net/vking_wang/article/details/9982709
