Java 面试宝典:什么是可见性?volatile 是如何保证可见性的?

news/2024/7/24 13:18:45 标签: Java 面试, Java 面试宝典, Java 并发面试

大家好,我是大明哥,一个专注「死磕 Java」系列创作的硬核程序员。
本文已收录到我的技术网站:https://skjava.com。有全网最优质的系列文章、Java 全栈技术文档以及大厂完整面经


什么是可见性?

可见性是指一个线程对共享变量所作的修改能够被其他线程及时地看到。

在单核时代,其实是不存在可见性问题的,因为所有的线程都是在一个CPU中工作的,一个线程的写操作对于其他的线程一定是可见的。

但是,在多核时代,每个 CPU 都有自己的缓存。一个线程对共享变量的修改可能只是在它所在 CPU 的本地缓存中进行,而不是在主内存中进行。这就可能导致其他线程看不到这个修改,从而引发可见性问题。

解决可见性的方案有两种:

  1. 使用 volatile 修饰共享变量:一个变量被声明为 volatile 后,对这个变量的读写操作都是在主内存中进行的,从而保证了不同线程之间对该变量修改的可见性。
  2. 使用同步机制,比如锁或者 synchronized。当一个线程成功获取锁进入一个同步块时,它会看到由其他线程在相同同步块内对共享变量的修改。

volatile 是如何保证可见性的?

这部分内容在 volatile 的实现原理中有,但是为了更好地阅读,大明哥直接复制过来了。

对于 volatile 变量,会在写入 volatile 变量的指令前添加 lock 前缀(汇编层面),当某个线程写入 volatile 变量时,其值会被强制刷入主内存,而其他处理器的缓存由于遵守了缓存一致性协议(MESI 协议),其他处理器的工作内存会被标志为无效。当其他处理器来访问这个变量时,由于它们的本地缓存是无效的,它们就不得不从主内存中重新加载这个变量的最新值。这样就保证了线程的可见性。

lock 前缀是用于实现原子操作的一种机制。当它用于一个指令前,它会锁定一个特定的内存地址,确保该指令执行期间,该内存地址不会被其他处理器访问。

MESI 协议

MESI协议,即缓存一致性协议,它是一种用于维护多处理器系统中缓存一致性的协议。从上面我们知道,每个处理器都有自己的工作内存,这可能导致同一内存位置的多个副本同时存在于不同的缓存中。为了保证这些副本的一致性,引入 MESI 协议来保证一致性。

其核心思想:当 CPU 写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。

MESI 代表四种缓存行状态:Modified(修改)、Exclusive(独占)、Shared(共享)和Invalid(无效)。

  1. Modified(修改):数据有效,数据已被修改,且只存在于当前缓冲中。这个状态下的缓存行数据于主内存数据不一致,在数据被写回主内存之前,任何对这个缓存行的读取或写入操作都只会发生在这个缓存中。
  2. Exclusive(独占):数据有效,且只存在于当前缓存中。这个状态下的缓存行数据与主内存中的数据是一致的。如果 CPU 需要写入这个缓存行,它可以直接改变状态到Modified,而无需与其他处理器或主内存通信。
  3. Shared(共享):数据有效,且可能存在于多个 CPU 的缓存中,并且数据与主内存中的数据是一致的。这个状态下的缓存行任何 CPU 都可以读到,但如果某个 CPU 需要写入,它必须首先通知其他拥有该缓存行副本的 CPU,使它们的副本无效。
  4. Invalid(无效):数据无效。如果有 CPU 需要读取这个缓存行数据,它必须从拥有有效副本的其他缓存或主内存中读取数据。

其工作流程如下:

读数据

  • 如果数据在本地缓存中并且状态是 Modified、Exclusive 或 Shared,处理器直接从缓存中读取,因为这三种状态的数据是有效的。
  • 如果数据不在本地缓存中,或者缓存行状态是 Invalid,处理器向其他缓存发送读取请求:
    • 如果其他缓存中没有该数据,或者都是 Invalid 状态,处理器从主内存读取数据,并将本地缓存行状态设置为 Exclusive。
    • 如果其他缓存中有该数据并且至少一个是 Shared 或 Modified 状态,处理器从拥有该数据的缓存复制数据,并将所有拥有该数据的缓存行状态设置为 Shared。

写数据

  • 如果数据在本地缓存且状态是 Modified,处理器直接写入本地缓存。
  • 如果数据在本地缓存且状态是 Exclusive,处理器将缓存行状态改为 Modified,并执行写入操作。
  • 如果数据在本地缓存且状态是 Shared 或者不在本地缓存中,处理器向其他缓存发送失效通知:
    • 其他缓存如果有该数据,则将其缓存行状态设置为 Invalid。
    • 本地缓存将数据写入,并将缓存行状态设置为 Modified。

内存屏障

volatile 通过在在每个读操作前都加上**Load屏障,强制从主内存读取最新的数据,在每个写操作后加上Store屏障,强制将数据刷新到主内存。**这样每次写都能将最新数据刷入到主内存,读都能从主内存读取最新数据,以此达到可见性。

下面以 i++ 为例来阐述下:

如上图所示,流程如下:

  • 线程 A 读取 i 时,遇到 Load 屏障,需要强制从主内存中读取得到 i = 0,加载到工作内存中。
  • 线程 A 执行 i++ 操作得到 i = 1,执行 assign指令进行赋值,遇到 Store 屏障,需要将 i = 1 强制刷新回主内存,此时主内存数据 i = 1
  • 然后线程 B 读取 i,也遇到Load 屏障,强制从主内存读取 i 的最新值, i = 1,执行 i++ 操作,得到 i = 2,同样在执行 assign 赋值后,遇到Store屏障立即将数据刷新回主内存,此时主内存数据 i = 2

这里可能有小伙们会认为,线程 A 和线程 B 同时执行,都从主内存读取 i = 0,然后执行 i++,最后主内存数据 i = 1,会不会存在这种情况?会,但是我们通过同步机制让他们不会,为什么?因为这个操作不是原子操作,在并发情况下会产生线程安全问题,我们是需要采用同步或者锁机制来保护的。


http://www.niftyadmin.cn/n/5452256.html

相关文章

Ubuntu系统部署Inis博客结合内网穿透实现公网访问本地站点

文章目录 前言1. Inis博客网站搭建1.1. Inis博客网站下载和安装1.2 Inis博客网站测试1.3 cpolar的安装和注册 2. 本地网页发布2.1 Cpolar临时数据隧道2.2 Cpolar稳定隧道(云端设置)2.3.Cpolar稳定隧道(本地设置) 3. 公网访问测试总…

51单片机入门:定时器与中断系统

定时器的介绍 定时器:51单片机的定时器属于单片机的内部资源,其电路的设计连接和运转均在单片机内部完成。根据单片机内部的时钟或者外部的脉冲信号对寄存器中的数据加1,定时器实质就是加1计数器。因为又可以定时又可以计数,又称…

【Java程序设计】【C00367】基于(JavaWeb)Springboot的粮仓管理系统(有论文)

TOC 博主介绍:java高级开发,从事互联网行业六年,已经做了六年的毕业设计程序开发,开发过上千套毕业设计程序,博客中有上百套程序可供参考,欢迎共同交流学习。 项目简介 项目获取 🍅文末点击卡片…

测试组合生成器-allpairspy

1、前言 在我们写功能用例时,常常会遇到多个参数有很多的选项,而如果想把这些参数值都要覆盖执行的话,工作量可想而知。那有没有什么办法既可以减少用例数量,也可以保证用例质量又降低测试时间成本,本篇将介绍一款工具…

数据库专题(oracle基础和进阶)

前言 本专题主要记录自己最近学的数据库,有兴趣一起补习的可以一起看看,有补充和不足之处请多多指出。希望专题可以给自己还有读者带去一点点提高。 数据库基本概念 本模块有参考:数据库基本概念-CSDN博客 数据库管理系统是一个由互相关联的…

【Qt】QMainWindow

目录 一、概念 二、菜单栏 2.1 创建菜单栏 2.2 在菜单栏中添加菜单 2.3 创建菜单项 2.4 在菜单项之间添加分割线 三、工具栏 3.1 创建工具栏 3.2 设置停靠位置 3.3 设置浮动属性 3.4 设置移动属性 四、状态栏 4.1 状态栏的创建 4.2 显示实时消息 4.3 显示永久消…

面试经典150题【91-100】

文章目录 面试经典150题【91-100】70.爬楼梯198.打家劫舍139.单词拆分322.零钱兑换300.递增最长子序列77.组合46.全排列39.组合总和(※)22.括号生成79.单词搜索 面试经典150题【91-100】 五道一维dp题五道回溯题。 70.爬楼梯 从递归到动态规划 public …

Git基础(25):Cherry Pick合并指定commit id的提交

文章目录 前言指定commit id合并使用TortoiseGit执行cherry-pick命令 前言 开发中,我们会存在多个分支开发的情况,比如dev,test, prod分支,dev分支在开发新功能,prod作为生产分支已发布。如果某个时候,我们…