多线程 -- JMM、volatile关键字、内存屏障、happens-before原则、缓存一致性
0.前言
在学习有关 volatile 关键字的我,在网上搜罗大量资料的时候,基本都会见到标题中这些关键字样,接下来我以我查阅到的资料来对如上概念进行串联并且解析。本文为多方资料的汇总,在文中会有很多关键部分贴上引用的链接,点击可以查看更加详细。
1、缓存一致性问题
描述:为了弥补 CPU 和 内存速度的差异,CPU 和内存之间会隔着缓存(Cache)。在多级缓存的结构中,多核CPU的每个核心都有属于各自的一级缓存(参考链接),在这种情况下,多线程并发作业就会产生缓存一致性问题。以线程间的共享变量进行 i++ 为例,i 的初始值是 0 ,那么在开始每块缓存都存储了 i 的值 0,当第一块内核做 i++ 的时候,其缓存中的值变成了 1,即使马上回写到主内存,那么在回写之后第二块内核缓存中的 i 值依然是 0,其执行 i++,回写到内存就会覆盖第一块内核的操作,使得最终内存中的结果是1,而不是预期中的2。所以为了避免这种情况的出现,CPU各个缓存之间需要通过某种方式来达到一致性。
总线锁定:当其中一个处理器要对共享内存进行操作的时候,在总线上发出一个 LOCK# 信号,这个信号使得其他处理器无法通过总线来访问到共享内存中的数据,总线锁定把 CPU 和内存之间的通信锁住了,这使得锁定期间,其他处理器不能操作其他内存地址的数据,总线锁定的开销比较大,这种机制显然是不合适的。
MESI协议:为了达到数据访问的一致,需要各个处理器在访问缓存时遵循一些协议,在读写时根据协议来操作,常见的协议有 MSI、MESI、MOSI 等。最常见的就是 MESI 协议。关于 MESI 可以移步查看(参考链接)注:MEIS解决的是核心专有cache不一致的问题,CPU架构实际十分复杂,中有cache、buffer、queue等很多,也有CPU架构并不支持 MESI 的情况。
2、JMM(JAVA Memory Model)
概念:Java虚拟机规范试图定义一种Java内存模型(JMM),来屏蔽掉各种硬件和操作系统的内存访问差异,让Java程序在各种平台上都能达到一致的内存访问效果。
(Oracle定义:)多线程的行为可能会让人感到困惑和违反直觉,尤其是在没有正确同步的时候。这章描述了多线程程序的语义;它包括了一些规则,规定了被多线程更新的共享内存可以读取到哪些值。因为这个定义与不同硬件平台体系的内存模型很相似,所以这些语义一般被称为java程序语言内存模型。没有歧义的话,我们可以简单的称呼这些规则为“内存模型“
(维基百科:)内存模型描述了java多线程与共享内存的交互过程。更进一步说,java内存模型描述了共享内存中的值被多线程修改和读取的过程,规定了共享变量的可见性和执行顺序。
(JSR133)11页描述了有关内存模型。
总之就是规定了一些列对内存的访问操作规则,定义了一些语法集,这些语法集映射到Java语言中就是volatile、synchronized等关键字。
注:与 JVM 的内存结构完全就是两个东西,详情=>(参考链接)
三个原则:JMM主要就是围绕着如何在并发过程中如何处理原子性、可见性和有序性这3个特征来建立的,通过解决这三个问题,可以解除缓存不一致的问题。关于这三个特性,可以参考这个文章(参考链接)
3、happens-before原则:
描述:在有序性中,JMM规定了一些默认的 "先天原则",称为 happens-before 原则,A happens-before B 指的就是 A 执行于 B 之前,且 A 的操作结果要对 B 可见
具体:(参考链接)
4、volatile关键字 和 内存屏障
1)volatile 关键字保证了 JMM 内存模型中的可见性
2)volatile 并不能保证原子性
3)volatile 可以阻止在编译阶段JAVA对代码的重排来保证有序性,实现原理就是内存屏障
volatile的内存屏障策略非常严格保守,非常悲观且毫无安全感的心态:
- 在每个volatile写操作前插入StoreStore(SS)屏障,在写操作后插入StoreLoad屏障;
- 在每个volatile读操作前插入LoadLoad(LL)屏障,在读操作后插入LoadStore屏障;
5、MESI 和 JMM中的volatile 并无直接的强关联!
引入缓存一致性的问题描述是为了帮助理解后面的 volatile 的知识点,但是很多时候会被人搞混,volatile 只是在语言语义层面做了规定,与实际的实现相比是经过了许多层的抽象,其中就有可能用到 MESI 来解决,所以说联系不是没有,但是搞清楚 MESI 与 JMM 甚至 volatile 没有直接的关系,拿过来直接进行比较实际上是不太对的。详情可见(参考连接)
6、链接集合