【面试突击】硬件级别可见性问题面试实战(下:synchronized和volatile底层对原子性、可见性、有序性的保证)

发布时间:2024年01月20日

🌈🌈🌈🌈🌈🌈🌈🌈
欢迎关注公众号(通过文章导读关注:【11来了】),及时收到 AI 前沿项目工具及新技术的推送!

在我后台回复 「资料」 可领取编程高频电子书
在我后台回复「面试」可领取硬核面试笔记

文章导读地址:点击查看文章导读!

感谢你的关注!

🍁🍁🍁🍁🍁🍁🍁🍁

synchronized 对原子性、可见性和有序性的保证

学习内存屏障注意事项:

对于内存屏障的内容不要太抠细节,因为对于不同的底层硬件,内存屏障的实现也是不同的,所以在学习的时候,有些文章中是加这个屏障,而另外一些文章又是加其他的屏障,这个都无所谓的,我们只 需要学习到内存屏障是如何保证可见性和有序性的就可以了

这里主要聊一聊 synchronized 底层到底是如何保证原子性、可见性和有序性的

  • 原子性的保证

这里保证的原子性就是当一个线程执行到 synchronized 的同步代码块中时,不会在执行过程中被其他线程中断

synchronized 是基于两个 JVM 指令来实现的:monitorentermonitorexit

那么在这两个 JVM 指令中的代码就是被上了锁的,这一段代码就只有当前加锁的线程可以执行,从而保证原子性

  • 可见性的保证

通过添加一些 内存屏障 来保证,在 synchronized 修饰的同步代码块中所做的 所有变量写操作,都会在释放锁的时候,强制执行 flush 操作,来保证可以让其他处理器中的线程可以感知到变量的更新

而在进入 synchronized 的同步代码块时,会先执行 refresh 操作,来保证读取到最新变量

  • 有序性的保证

也是通过加各种 内存屏障 来保证的,避免指令重排的问题

接下来看一下,在 synchronized 同步代码块中,到底会添加哪些 内存屏障

int b = 0;
int c = 0;
synchronized (this) {  --> monitorenter
  --> Load 内存屏障
  --> Acquire 内存屏障
  
  int a = b;
  c = 1;
   
  --> Release 内存屏障
} --> monitorexit
--> Store 内存屏障

这里可能大家对 AcquireRelease 内存屏障有点陌生,但是一定知道 LoadLoad、LoadStore、StoreStore、StoreLoad 屏障,下边说一下他们的关系:

  • Acquire 屏障 = LoadLoad + LoadStore
    • Acquire 屏障确保一个线程在执行到屏障之后的内存操作之前,能看到其他线程在屏障之前的所有内存操作的结果
  • Release 屏障 = LoadStore + StoreStore
    • Release 屏障用于确保一个线程在执行到屏障之后的内存操作之前,其他线程能看到该线程在屏障之前的所有内存操作的结果

那么对于上边 synchronized 的同步代码块,这里解释一下每个屏障的作用:

  1. monitorenter 指令后,添加 Load 屏障,执行 refresh 操作,可以去将其他处理器中修改过的最新数据加载到自己的高速缓存种
  2. Load 屏障之后,添加了 Acquire 屏障,可以保证当前线程可以读到 Acquire 屏障前所有内存操作的结果
  3. monitorexit 指令前,添加 Release 屏障,保证一个线程在执行到屏障之后的内存操作之前,其他线程能看到该线程在屏障之前的所有内存操作的结果
  4. monitorexit 指令后,添加了 Store 屏障,对自己在同步代码块中修改的变量执行 flush 操作,刷新到高速缓存或者=主内存中,让其他处理器中的线程可以感知到数据的变化

因此通过 Acquire、Release、Load、Store 屏障来保证了有序性

一句话总结

简单总结一下就是,在 synchronized 代码块开始时,加内存屏障,保证可以感知到屏障前所有的内存操作变化,在 synchronized 结束后,加一个内存屏障,保证可以将内存操作的更新情况立即刷新到高速缓存或者主内存中,可以让其他线程感知到!

volatile 对可见性、有序性的保证

volatile 是不保证原子性的,只保证了可见性和有序性,底层就是基于各种 内存屏障 来实现的

使用 volatile 关键字之后,加入的内存屏障如下:

volatile boolean flag = false;

--> Release 屏障
flag = true;  // volatile 写
--> Store 屏障

--> Load 屏障
if (flag) { // volatile 读
	--> Acquire 屏障
	// ... 
}

主要是在 volatile 写操作和读操作前后都添加内存屏障来保证:

  • 在 volatile 写操作之前,加入了 Release 屏障,保证了 volatile 写和 Release 屏障之前的任何读写操作不会发生指令重排
  • 在 volatile 写操作之后,加入了 Store 屏障,保证了写完数据之后,立马会执行 flush 操作,让其他处理器的线程感知到数据的更新
  • 在 volatile 读操作之前,加入了 Load 屏障,保证可以读取到这个变量的最新数据,如果这个变量被其他处理器中的线程修改了,必须从其他处理器的高速缓存或者主内存中加载到自己本地高速缓存里,保证读到的是最新数据
  • 在 volatile 读操作之后,加入了 Acquire 屏障,禁止 volatile 读操作之后的任何读写操作volatile 读操作 发生指令重排
文章来源:https://blog.csdn.net/qq_45260619/article/details/135715975
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。