IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> Java知识库 -> 要是面试官再问我volatile,我就这么答 -> 正文阅读

[Java知识库]要是面试官再问我volatile,我就这么答

回答volatile关键字

volatile关键字的三个特性:可见性、禁止指令重排、不保证原子性



1、可见性

此处的核心是理解MESI协议,基于volatile关键字展开,然后延申到MESI协议,并且完成MESI协议相关的一些计算机组成的概念的补充

①现象:保证了可见性,在进行线程交互中过程中,被该关键字修饰的变量会具有可见性,能够保证变量在线程间的信息是同步的

②字节码:被该关键字修饰的变量在被编译为字节码的时候,会添加了一个ACC_VOLATILE关键字

③理论基础:MESI协议,可见性的本质特性是基于计算机的缓存一致性协议。MESI分别对应已修改、独占、共享、无效。之所以该协议能够保证变量在不同线程间具有可见性,就是得益于该协议。



在理解理解MESI协议之前需要了解的一下计算机组成相关的概念:

  1. 总线事务:从我们的请求开始,到我们的请求结束,这一整个过程称之为总线事务。
  2. 总线仲裁:在我们的总线事务请求过程中,如果出现,多个处理器同时访问同一片内存空间,那么就一定要对我们的多个请求做出决策,到底使用执行哪一个请求。这个选择的过程叫做总线裁决。
  3. 总线锁定:当总线裁决后,只会有一个请求去执行对应的操作,此时就会出现一个LOCK#信号量(类比理解为Java中的synchronized,添加一个锁),出现了该信号量,表示出现了总线锁定
  4. 缓存锁定:与总线锁定类似,只是锁定的粒度不同,此时锁定的是缓存块。(缓存锁定和总线锁定的区别就好比InnoDB存储引擎下的行锁和表锁)
  5. 总线窥探:在缓存锁定的基础上,通过缓存锁定自己独特的方式去实现数据同步问题,这个独特的方式就是MESI协议,这个MESI协议的基础背景理论则是总线窥探机制。当我们将数据写入一个缓存数据行的时候,其他总线是能够感知到数据变化,继而做出相对应的调整。这就是第一点中所说的为什么volatile能够保证可见性,他背后的原理就是MESI协议,自然也就离不开这里的总线窥探机制

总结:总线事务总是存在,所以需要总线裁决来决定到底是哪一个进行,常规情况下我们直接使用总线锁定,这样就能够保证数据的安全。但是由于人们希望能够提高处理效率,所以又基于总线窥探机制,提出了一个缓存锁定的理论。将原来的锁定内存块变成了粒度更小的缓存块。



MESI协议(详细版请参考浅谈volatile与计算机缓存一致性协议(MESI)之间的联系):

情景前提为,两个线程共同访问num=1这个变量,MESI协议在这个过程中的体现

  1. 线程1将主存中的num = 1加载到线程1私有的缓存空间中。由于num被volatile关键字修饰,所以会执行对应的一致性协议(添加对应的LOCK前缀指令)。此时线程1的缓存状态为独占独占(E);
  2. 如果此时线程2也加载了num变量,那么线程1和线程2的各自的缓存空间对应的缓存状态就会变成共享(S);
  3. 此时线程1将缓存中的数据加载到CPU,在ALU(算数逻辑单元)的配合下完成对应的逻辑运算;
  4. 线程1的CPU完成逻辑运算后,将数据返回给缓存。此时总线发现了数据发生变化,就会修改对应缓存空间的缓存状态,将当前线程的缓存状态修改为已修改(M),其他线程中对应的缓存块状态修改为无效(I)。于此同时,还需要将线程1中的缓存变量num,立即刷新回主存;
  5. 那么当线程2想加载自己缓存空间中对应的变量时,就无法命中(原因就是第四步),所以又重新去主存中加载,此时线程2加载到的num就变成了最新的数据;
  6. 最后线程2在将数据加载到CPU完成自己的逻辑运算,最终返回给主存。

总结:简单来说就是,最开始的人拿到了数据就是E,如果第二个人也拿到了数据就变了两个S,如果一个人修改了数据,那么状态就会变成M,然后数据放到主存,与此同时第二个人的就变成I。既然是无效的I,那么第二个人在使用数据的时候就只能自己再去主存中获取,此时获取到的数据,就成个最新的一笔数据。





2、禁止指令重排

程序在执行的时候,内部会自动优化掉一些不影响结果的逻辑,这个时候就会涉及代码顺序的调整。那么添加了volatile关键字后,就能够禁止代码结构的优化。编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器进行重排序。编译器选择了比较保守的JMM内存屏障策略,这样就能保证在任何处理器平台上,任何程序中都能够得到正确的volatie内存语义。这个策略分成:

LoadStore:在读后面再插入

StoreStore:在写前插入

StoreLoad:在写后插入

LoadLoad:在写读后插入



当然,如果我们只是要解决我们的问题,那么我们添加一个volatile关键字就可以了。如果局部变量也需要使用volatile,那么我们可以手动的添加内存屏障。我们可以使用Java自带的Unsafe工具类,调用对应的方法完成该操作。不过该方法只能通过反射调用。





3、不保证原子性

对任意单个volatile变量的读/写具有原子性,但类似于i++这种复合操作不具有原子性

  • 常见的例子就是使用两个线程对一个变量进行i++操作,最后的结果值不等于循环的次数(我个人不是很能理解为什么会把这一个点当作一个特性来描述,因为i++不具有原子性是客观存在的,即便是你不添加volatile也是没有办保证原子性的,既然i++本身就有问题,那为什又要和volatile关键字扯上关系呢?)
  • 还有一个比较常见的例子,就是单例模式的双重检测锁,我们需要对instance变量添加volatile关键字,因为很有可能在赋值的位置,由于创建对象的过程不是原子性的(类比理解i++),加上这个过程可能会出现指令重排,所以添加对应的关键字就能够很好的避免创建单例对象过程出现的线程安全问题。
  Java知识库 最新文章
计算距离春节还有多长时间
系统开发系列 之WebService(spring框架+ma
springBoot+Cache(自定义有效时间配置)
SpringBoot整合mybatis实现增删改查、分页查
spring教程
SpringBoot+Vue实现美食交流网站的设计与实
虚拟机内存结构以及虚拟机中销毁和新建对象
SpringMVC---原理
小李同学: Java如何按多个字段分组
打印票据--java
上一篇文章      下一篇文章      查看所有文章
加:2021-11-15 15:42:36  更:2021-11-15 15:44:31 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 3:01:22-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码