java顶级内功心法(1) —— 可见性、MESI、volatile精讲 (8000字由浅入深)(1)

简介: java顶级内功心法(1) —— 可见性、MESI、volatile精讲 (8000字由浅入深)

前言

volatile是我们在学习java线程方面的内容时,经常被提到的一个词,很多人知道他与可见性有关,看过《深入了解java虚拟机的》同学也许能讲得更多点,如禁止指令重排序等。但如果更深入一点:可见性是什么,volatile在java中是如何实现的? 恐怕会陷入混乱了。


这次我们就好好聊聊可见性 和 volatile,争取讲得深入浅出,让大家彻底明白。


一、voaltile是什么意思?

volatile 是java中的一个关键字,用来修饰变量,它的字面意思是“”易变的“。当开发者给一个变量修饰上 volatile ,意味着告诉jvm这个变量可能会被多个线程同时修改,要格外注意该变量的情况,即保证可见性。


二、可见性问题的探索

在讲清楚voaltile之前,需要带大家复习和思考一些问题


1.多线程环境下的可见性问题

随着计算机的发展,我们目前家用机的CPU都能做到8核乃至更多核心,加之超线程技术,一个处理器上能够同时运行的线程数也是大大增加。此时,当一个线程修改了共享变量的值,其他线程往往无法及时“看到”,还继续使用“老旧“的数据,从而导致程序出现难以预测的行为,这类问题称为可见性问题


2.共享变量为什么“不可见”

那么多线程下,为什么共享变量会不可见呢?可以看这张图:

96c4346270fc4a4b9493e7b1a06e1ad2.png

RAM是主存储器,我们程序里的变量会存储其中。但是我们都知道CPU的速度比内存要快得多,不可能CPU的每次运算都实时的从内存中取数据,处理完后又放回内存。那CPU相当于每次瞬间做完工作,然后就会陷入漫长的等待。因此引入了缓存,引入了寄存器等更快速的介质来存储数据,会把一些常用数据存储在这些高速介质上。


需要注意的是,缓存中存储的数据是RAM中的备份,而每个CPU核心都有自己的缓存,也就是说一个数据可能在不同的缓存里都有备份。这样当某个线程修改了该数据,只有该线程所在核心的备份被更新了。RAM和其他核心的缓存用的仍然是“”旧“数据,那么这次修改对其他核心的线程就是不可见的。


3.RAM的更新时机

其实通过上一问我们已经知道,缓存之间,或者缓存与RAM之间的数据好像很容易不一致,主要是由于RAM的数据“老旧”,不由的我们会产生疑问


当CPU更新数据时,“新数据“是什么时候写回RAM的?CPU通常会有两种策略


3.1 写回(write-back)

当CPU写命中缓存,缓存中的数据被修改后,C并且将缓存中该数据对应位置块的“脏位”标记为1,表示该块数据已被修改。当CPU需要将该数据替换出缓存时,才将其写回RAM


72e4672f8efe42529ca6067cc0e34f1f.png

3.2 写直达(write-through)

当CPU写时,无论是否命中缓存,都会直接将其写回RAM

2c20a5c14db348de866709781ff25c28.png

可以看到两种策略的特点鲜明:


  • “写回”策略讲究写的效率,不到万不得已不写RAM,对于一个线程频繁修改的数据,这样做无疑会节省很多时间,但代价就是RAM迟迟得不到更新,容易造成数据不一致。
  • “写直达”每一次更新都直接写入RAM,意味着缓存和RAM数据总是一致的,但带来的问题就是写的效率变低了,如果数据其实只有本线程在用,将造成时间的浪费。

【 扩展1】:写直达法在未命中缓存时,对于是否将修改过的主存块取到cache,有两种选择。一种是取来并且为它分配一个行位置,称为WTWA法(Write–Through–with–Write–Allocate),这样逻辑复杂,但缓存会存下最新的数据。另一种是不取称为WTNWA法(WriteThrough–with.NO-Write–Allocate),逻辑简单,但缓存中就没有刚刚的数据。


【 扩展2 】:后文提到的MESI协议是支持写回(write-back)缓存的最常用协议


4.缓存的更新时机

从上文我们已经知道了如果CPU某个核心做了写处理,RAM会在某个时间被更新。但是还有一个问题,其他核心的缓存的数据什么时候被更新呢?这也就引申出了缓存一致性问题

98651a82533d475ca157e483cef65f69.png

如上图,A核心对变量 i 做了 +1 处理,但B核心却是不知道的,为了使B核心的缓存能够及时更新,我们似乎可以采取一种传播策略


4.1 写传播(write-propagation)

顾名思义,写传播就是在写一个缓存的时候,需要广播到其他核心的缓存,它的一种常见实现方式就是总线嗅探

4b9cf091dd034193afaae19e5f93e280.png

例如上文的 A 号 CPU 核心修改了缓存 中 i 变量的值,通过总线把这个事件广播通知给其他所有的核心,然后每个 CPU 核心都会监听总线上的广播事件,并检查是否有相同的数据在自己的缓存中,如果 B 号 CPU 核心的缓存中有该数据,那么也需要把该数据更新到自己的缓存里。


很显然,这种方式的逻辑十分清晰。但也有其问题,


每一次修改都会在总线广播,会导致总线负载过重。

如果有两个核心同时修改了某个数据,最终的数据会是什么呢?这将无法控制和预知

5 缓存一致性协议(MESI 协议)

很显然,结合上文的 3.1-写回 和4.1-写传播,虽然解决了数据更新的部分问题,但存在着不足,比如:


如果一个缓存被更新,而此刻其他缓存没存储该数据,由于内存更新的滞后特点。当其他缓存要从内存读取该数据时,读到的仍然是“旧”数据。

如果有两个核心同时修改一个数据,那么结果将不可知,这对程序来说是致命漏洞。

很自然的,我们想防止这些场景,就需要保证


内存及时更新,至少在其他缓存要从内存读取该数据的时候能知道内存里的数据是新是旧

控制并发,保证一个数据在同一时间只能被一个核心修改

基于这样的要求,于是产生了CPU 缓存一致性协议 ,即MESI协议


MESI协议其实是缓存中数据的四种状态的缩写,这四种状态决定着数据更新将如何同步

M : modified —— 修改,数据只存在于本Cache中,但被本核心修改,还没同步到内存

E : exclusive —— 独享,数据只存在于本Cache中,和内存数据一样

S : shared —— 共享,数据存在于很多Cache中,和内存数据一样

I : invalid ——无效,本核心Cache line无效。


当然这四种状态不是乱来的,需要严格按照定义来执行,比如说本地缓存中有一条数据状态为 E-独享,或者M-已修改,意味着其他缓存要么没有这条数据,要么就只能是 I-废弃。更全面的关系图如下。

任意一对缓存,对应缓存行的相容关系:

6722600bde4344b49ec94dd17cc7abec.png


看起来很复杂,其实只要一张状态变化图就可以概括,如果你看不懂,没关系,并不是让你死记硬背,记住核心规则后你将很容易理解。

5aabad99c5e24acbbb8ddf084c2fe5ab.png


我们直接举个最复杂的场景用来诠释


这里有四个核心A、B、C、D,有一个数据X在核心A的缓存中,且被该核心改过,但没有传回主内存,而其他缓存则没有这个数据X。

这样的情况下,核心A里的数据X,其状态就是M——修改状态,此时,核心B想修改数据X,那么首先:

核心B通过总线告知自己需要获取数据X,总线向各核心索取X的最新数据

核心A将数据X传回总线,核心B和主内存都从总线读取最新的数据X,然后A、B核心的缓存块状态为S——共享

核心B对自己缓存中的数据X修改,B核心缓存块为M——修改,同时将该消息告知总线,A核心缓存为I——无效


如果你看明白了上面这个例子,其实你已经理解了流程的核心原理,即各核心通过总线嗅探来检测和传播数据,并严格遵守状态转换规则,实时更新本缓存的状态。这里,可以给大家介绍一个MESI过程的可视化网站,方便大家加深理解:MESI网站


目录
相关文章
|
5天前
|
存储 缓存 Java
Java 并发编程——volatile 关键字解析
本文介绍了Java线程中的`volatile`关键字及其与`synchronized`锁的区别。`volatile`保证了变量的可见性和一定的有序性,但不能保证原子性。它通过内存屏障实现,避免指令重排序,确保线程间数据一致。相比`synchronized`,`volatile`性能更优,适用于简单状态标记和某些特定场景,如单例模式中的双重检查锁定。文中还解释了Java内存模型的基本概念,包括主内存、工作内存及并发编程中的原子性、可见性和有序性。
Java 并发编程——volatile 关键字解析
|
4天前
|
安全 Java Kotlin
Java多线程——synchronized、volatile 保障可见性
Java多线程中,`synchronized` 和 `volatile` 关键字用于保障可见性。`synchronized` 保证原子性、可见性和有序性,通过锁机制确保线程安全;`volatile` 仅保证可见性和有序性,不保证原子性。代码示例展示了如何使用 `synchronized` 和 `volatile` 解决主线程无法感知子线程修改共享变量的问题。总结:`volatile` 确保不同线程对共享变量操作的可见性,使一个线程修改后,其他线程能立即看到最新值。
|
5天前
|
缓存 安全 Java
Java volatile关键字:你真的懂了吗?
`volatile` 是 Java 中的轻量级同步机制,主要用于保证多线程环境下共享变量的可见性和防止指令重排。它确保一个线程对 `volatile` 变量的修改能立即被其他线程看到,但不能保证原子性。典型应用场景包括状态标记、双重检查锁定和安全发布对象等。`volatile` 适用于布尔型、字节型等简单类型及引用类型,不适用于 `long` 和 `double` 类型。与 `synchronized` 不同,`volatile` 不提供互斥性,因此在需要互斥的场景下不能替代 `synchronized`。
2068 3
|
2月前
|
SQL 缓存 安全
[Java]volatile关键字
本文介绍了Java中volatile关键字的原理与应用,涵盖JMM规范、并发编程的三大特性(可见性、原子性、有序性),并通过示例详细解析了volatile如何实现可见性和有序性,以及如何结合synchronized、Lock和AtomicInteger确保原子性,最后讨论了volatile在单例模式中的经典应用。
61 0
|
3月前
|
缓存 Java 编译器
JAVA并发编程volatile核心原理
volatile是轻量级的并发解决方案,volatile修饰的变量,在多线程并发读写场景下,可以保证变量的可见性和有序性,具体是如何实现可见性和有序性。以及volatile缺点是什么?
|
4月前
|
安全 Java 编译器
Java 中的 volatile 变量
【8月更文挑战第22天】
38 4
|
4月前
|
安全 Java
|
5月前
|
存储 安全 Java
Java面试题:深入探索Java内存模型,Java内存模型中的主内存与工作内存的概念,Java内存模型中的happens-before关系,volatile关键字在Java内存模型中的作用
Java面试题:深入探索Java内存模型,Java内存模型中的主内存与工作内存的概念,Java内存模型中的happens-before关系,volatile关键字在Java内存模型中的作用
45 1
|
4月前
|
存储 SQL 缓存
揭秘Java并发核心:深度剖析Java内存模型(JMM)与Volatile关键字的魔法底层,让你的多线程应用无懈可击
【8月更文挑战第4天】Java内存模型(JMM)是Java并发的核心,定义了多线程环境中变量的访问规则,确保原子性、可见性和有序性。JMM区分了主内存与工作内存,以提高性能但可能引入可见性问题。Volatile关键字确保变量的可见性和有序性,其作用于读写操作中插入内存屏障,避免缓存一致性问题。例如,在DCL单例模式中使用Volatile确保实例化过程的可见性。Volatile依赖内存屏障和缓存一致性协议,但不保证原子性,需与其他同步机制配合使用以构建安全的并发程序。
73 0
|
5月前
|
缓存 安全 算法
Java面试题:如何通过JVM参数调整GC行为以优化应用性能?如何使用synchronized和volatile关键字解决并发问题?如何使用ConcurrentHashMap实现线程安全的缓存?
Java面试题:如何通过JVM参数调整GC行为以优化应用性能?如何使用synchronized和volatile关键字解决并发问题?如何使用ConcurrentHashMap实现线程安全的缓存?
62 0