讨论下Java中的volatile和JMM(Java Memory Model)Java内存模型
2022年04月12日 20:45:19 · 本文共 2,235 字阅读时间约 8分钟 · 3,845 次浏览接上两篇《大佬们在说的AQS,到底啥是个AQS(AbstractQueuedSynchronizer)同步队列》和《Java中说的CAS(compare and swap)是个啥》的内容,都提到了volatile,虽然网络上这块已经被各个大佬讲烂了,我也回忆复习一下,并且加入我自己的理解,因为我是自学的Java所以不是专业科班的标准答案,还有我自己的理解在里面,如果有错误欢迎指正。
JMM(Java Memory Model)Java内存模型
在讨论 volatile 之前,我们需要先了解一下JMM(Java Memory Model)Java内存模型,如果没有 JMM 直接讨论 volatile 会有点奇怪,所以还是得先说下 JMM。
为了保证共享内存的正确性(可见性、有序性、原子性),内存模型定义了共享内存系统中多线程程序读写操作行为的规范。JVM 也定义了自己的内存模型,注意!这里的 JMM 内存模型跟 JVM 内存模型,不是一回事,并且也不能做比较,完全是两码事哈,不要记叉了。
简单描述一下,Java 将内存划分为了主内存(main memory)和工作内存(working memory),主内存可以理解为大家一起用的内存,工作内存是每个线程自己的内存,就像 CPU Cache 缓存一样,每个核心有自己的缓存而不是共用 RAM 内存,在程序操作变量的时候是在操作自己的工作内存,然后再写回主内存,所以在多个线程操作同一个变量的时候就可能出现问题,因此JMM需要提供原子性、可见性、有序性的保证。
Java 内存模型和硬件内存架构是不同的。硬件内存架构不区分线程堆栈和堆。在硬件上,线程栈和堆都位于主存中。部分线程堆栈和堆有时可能存在于 CPU 缓存和内部 CPU 寄存器中。
但是,我看网上有人说工作内存(working memory)是在栈上,我有点不同意,我觉得工作内存(working memory)是对 CPU Cache 缓存的抽象,不知道对不对?
原子性
JMM保证除 long 和 double 以外的基本数据类型的读写操作是原子性的,当然你用 synchronized 也可以保证原子性,这依赖 monitorenter 和 monitorexit 监视器指令。
为啥 long 和 double 特殊呢?因为他俩都是 64 位的,要照顾 32位 CPU,就把这种拆成两个 32位来操作,所以JVM规范不保证原子性,但鼓励JVM去实现原子性,我记得加 volatile 好像就是原子的了,不确定,还得查具体的 JVM 实现的文档,这太细节了。
可见性
就是咱们要讨论的 volatile 了,强制变量的赋值会同步刷新回主内存中,强制变量的读取从主内存中加载,保证不同线程始终能看到该变量的最新值,这就保证了可见性。
有序性
也是咱们要讨论的 volatile,可以阻止指令重排,还有 happens-before 原则,后面慢慢展开说,这节是 JMM 内容,就先到这里吧。
Volatile
终于到主题 volatile 了,这货在 JUC 里写的满世界都是,说明很重要啊,了解一下 volatile 是干啥的。
Volatile可见性
在上面 JMM 的介绍中,我们知道程序不是直接在主内存中交互的,而是复制一个副本到自己的工作内存里进行操作,这就会导致线程A在核心1上的操作,线程B在核心2上也操作,但他们相互之间可能看不见,加 volatile 修饰的变量就可以解决这个问题,当修改以后其他线程就可以立即看到修改后的值,怎么做到的呢?先了解一下一致性协议,例如 Intel 的 MESI 协议。
MESI(缓存一致性协议)
当一个CPU修改变量时,发现在其他核心也有这个变量的缓存,会通知其他核心将缓存设置为过期状态,这样其他核心再读取时从主内存中直接读取,而不是自己的缓存中读取。
volatile 这么好为啥不给变量都加上?如果你滥用 volatile 也会造成一些问题,上面我们介绍了缓存一致性协议,CPU 之间的通讯是依赖总线的,总线的带宽就那么多,如果你使用大量 volatile 并且配合 CAS 自旋,会在总线上造成消息风暴,占用总线带宽,那么机器的性能也会下降。
Volatile有序性
volatile 可以禁止指令重排,现在的系统为了提高效率,会重排序咱们的代码指令,其中包括编译期间的重排、CPU执行期间的指令重排,比如上一条指令和下一条指令没啥依赖关系,那么就可能被重排序。
volatile 在编译时会在适当的位置插入内存屏障,有四种屏障:
- StoreStore:插入到两个 Store 中间,就可以确保下面的以及后续的 Store 操作可以看到上面的 Store 操作
- LoadLoad:插入到两个Load操作中间,就可以确保下面的 Load 读取可以读取到上面 Load 的数据
- LoadStore:插入 Load 与 Store 中间,就可以确保下面的 Store 执行前可以读取到上面的 Load 操作
- StoreLoad:插入 Store 与 Load 中间,就可以确保下面的 Load 执行前可以读取到上面的 Store 操作
有点复杂,从JDK5开始,提出了happens-before的概念,通过这个概念来阐述操作之间的内存可见性。
有了这个概念,就可以描述为:对一个volatile域的写操作,happens-before于任意线程后续对这个volatile域的读,再大白话一些,你改了volatile域的变量,那么后面任意线程都可以读得到。
Volatile无法保证原子性
前面说的可见性好像很好使,但直接操作 volatile 变量其实无法保证原子性,你虽然可见它的变化,但是通过内存屏障保证了happens-before,让单次读写变得有序,只能保证你读取到最新的,你读取成功以后,做操作的时候,其他线程可能还在修改,所以无法标准原子性。
volatile与synchronized
很多人说 volatile 是轻量级的 synchronized,volatile 只保证了可见性的读取,但涉及到写入的时候,你无法阻止其他线程也在修改。
版权声明:本文为博主「任霏」原创文章,遵循 CC BY-NC-SA 4.0 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.renfei.net/posts/1003522
相关推荐
猜你还喜欢这些内容,不妨试试阅读一下以下内容均由网友提交发布,版权与真实性无法查证,请自行辨别。
- 前后端分离项目接口数据加密的秘钥交换逻辑(RSA、AES)
- OmniGraffle 激活/破解 密钥/密匙/Key/License
- Redis 未授权访问漏洞分析 cleanfda 脚本复现漏洞挖矿
- CleanMyMac X 破解版 [TNT] 4.6.0
- OmniPlan 激活/破解 密钥/密匙/Key/License
- 人大金仓 KingbaseES V8 R3 安装包、驱动包和 License 下载地址
- Parallels Desktop For Mac 16.0.1.48911 破解版 [TNT]
- Parallels Desktop For Mac 15.1.4.47270 破解版 [TNT]
- Sound Control 破解版 2.4.2
- CleanMyMac X 破解版 [TNT] 4.6.5
- 博客完全迁移上阿里云,我所使用的阿里云架构
- 微软确认Windows 10存在bug 部分电脑升级后被冻结
- 大佬们在说的AQS,到底啥是个AQS(AbstractQueuedSynchronizer)同步队列
- 比特币(BTC)钱包客户端区块链数据同步慢,区块链数据离线下载
- Java中说的CAS(compare and swap)是个啥
- 小心免费主题!那些WordPress主题后门,一招拥有管理员权限
- 强烈谴责[wamae.win]恶意反向代理我站并篡改我站网页
- 讨论下Java中的volatile和JMM(Java Memory Model)Java内存模型
- 新版个人网站 NEILREN4J 上线并开源程序源码
- 我站近期遭受到恶意不友好访问攻击公告