Volatile实现原理
比如现在我们有这样一段代码:线程等待另一个线程将数据装载完就输出success,可是最后程序一直卡在while循环里没有往下执行。
public class VolatileDemo { private static boolean flag = false; //private static volatile boolean flag = false; public static void main(String[] args) throws Exception{ new Thread(()->{ System.out.println("等待装载数据。。。。"); while(!flag){ } System.out.println("====== SUCCESS ====="); }).start(); Thread.sleep(2000); new Thread(()->{ System.out.println("开始装载"); flag = true; System.out.println("装载完毕"); }).start(); } } /* 控制台输出 等待装载数据。。。。 开始装载 装载完毕 */
造成这个问题出现的原因是jmm原子操作造成的。jmm内存模型就是java内存模型、准确的说是java线程内存模型。它和cpu缓存模型类似、是基于cpu缓存模型来建立的。
jmm一共有8种原子操作:
read(读取):从主存读取数据
load(载入):将内存数据读到工作内存
use (使用):取出工作内存中的数据来计算
assign(赋值):将计算好的值重新赋予到工作内存中
store(存储):将工作内存数据写入主存
write(写入):将store过去的变量值赋值给主内存中的变量
lock(锁定):将主内存变量加锁,标识为线程独占状态
unlock(解锁):将主存变量解锁,解锁后其他线程可以锁定该变量
工作原理
可以看到线程1已经把变量副本加载到工作内存了,而线程2将计算后的值存到主存之后,却没有办法告诉线程1,所以就出现了线程安全问题。其实cpu与主存交互会经过"总线"这么一个概念,cpu为了解决这种数据不一致问题有两种方案:
总线加锁(性能太低)
早期cpu是对总线加锁,lock住这个数据,这样其它线程就没法对它读或写,直到这个线程用完这个数据 unlock之后才能被其他线程操作。也就是说从read开始后直到write结束才释放锁。
MESI缓存一致性协议
多个线程将同一个数据读取到各自的缓存区后,某个cpu修改了缓存的数据之后,会立马同步给主存,这都是汇编语言实现的。其他cpu通过总线嗅探机制(可以理解为监听)可以感知到数据的变化从而将自己缓存里的数据失效,从而去读取主存的值。所以mesi协议是从store开始加锁,锁的粒度更小,时间更短。实际上volatile就是这么实现可见性的。同时由于这中间过程中有store和write几步操作、还要让其他cpu缓存的数据置空都是要耗时的,可能这个过程中数据被别人改了,所以它是非原子操作的。
指令重排
指定重排只会发生在多线程情况下,单线程是不会出现指定重排的。所谓的指令重排就是JVM在编译Java代码的时候,或者CPU在执行JVM字节码的时候,对现有的指令顺序进行排序优化。但不会对有依赖关系的做重排序。比如:
int a = 1;
int b = 2;
int c = a*c;
a 和 b 没有任何关系,所以它们的顺序无所谓,但是 c 依赖于a、b。只能存在于a、b后面,不然就乱套了。在一个变量被volatile修饰后会被禁止指令重排,JVM会为我们做两件事:
1.在每个volatile写操作前插入StoreStore屏障,在写操作后插入StoreLoad屏障。
2.在每个volatile读操作前插入LoadLoad屏障,在读操作后插入LoadStore屏障。
* 比如我们通过 双层检查+锁的形式创建某个类的单例时,我们会对这个类加上volatile,就是因为指令重排的原因可能类实例初始化好了,但是里面的成员变量还是空,这样就会出现异常。
volatile保证原子性?
在java中,long和double都8个字节共64位(一个字节=8bit),JVM 规范出来的较早,那时候处理器还不能处理 64 位字长,所以 JVM 规范里定义的是 32 位字长的读写是原子的,而 64 位字长需要分成两次来操作。像long 和 double 都是 8 字节长度的类型,也就是有 64 位。需要分两步执行,每次读取32位,这样就对double和long变量的赋值就会出现问题。而通过volatile修饰之后,对这种类型数据赋值就是原子的了,因为它只是一步操作;而对于i++这种操作是无法保证原子性的,因为它实际是三步操作。现在都是64位的服务器系统了,那么对64位的long和double的读写都是原子操作的。即可以以一次性读写long或double的整个64bit。 (小知识点:防止工作中遇到杠精。)
。
原文地址:https://www.cnblogs.com/wlwl/p/15004418.html
- 走进科学:我是如何“黑了”星级酒店的
- OpenSSL空指针引用do_ssl3_write
- 针对近期“博全球眼球的OAuth漏洞”的分析与防范建议
- 黑掉美国(英国、澳大利亚、法国等)的交通控制系统
- Android 自定义标签 ViewLayout
- Identity Service - 解析微软微服务架构eShopOnContainers(二)
- 机器学习之随机森林
- Catalog Service - 解析微软微服务架构eShopOnContainers(三)
- EventBus In eShop -- 解析微软微服务架构Demo(四)
- Health Check in eShop -- 解析微软微服务架构Demo(五)
- Android Studio相见恨晚的操作锦集
- [收藏]几个常用的用正则表达式验证字符串的函数
- 走进科学: 无线安全需要了解的芯片选型、扫描器使用知识
- React Native之携程Moles框架
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- JSP 基本凉凉,学妹不知道他是啥
- Codeforces Round #615 (Div. 3)C. Product of Three Numbers
- Codeforces Round #622 (Div. 2) A.Fast Food Restaurant
- 渗透测试实战 | 一次信息泄露引发的越权
- Vue创建项目及基本语法 一
- 学 Java 开发怎么能不知道 Filter 与 Listener
- Windows 安装 MySQL 常见问题
- LootCode-链表排序-Java
- 好久不用 jQuery, 来复习一下
- Spring学习一、组件注册
- 复习 EL 表达式与 JSTL
- Spring 学习二、Bean生命周期相关注解
- 十分钟学会 HTML
- 聊一下会话跟踪技术
- 朝花夕拾之Matlab基础回顾:向量的点积、叉积、混合积