下午好,又是奋斗的一天。
登录注册
2022-04-12 20:45:19

讨论下Java中的volatile和JMM(Java Memory Model)Java内存模型

volatile和JMM(Java Memory Model)Java内存模型

接上两篇《大佬们在说的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 只保证了可见性的读取,但涉及到写入的时候,你无法阻止其他线程也在修改。

评论与留言

以下内容均由网友提交发布,版权与真实性无法查证,请自行辨别。
本站有缓存策略,时间约2小时后能看到您的评论。本站使用自动审核机制,如果您的内容包含广告/谩骂/恐怖/暴力/涉政等不和谐内容将无法展示!
微信订阅号
扫码关注「任霏博客」微信订阅号
反馈与讨论
感谢您的关注与反馈
如果您发现了BUG、安全漏洞、或者希望讨论技术内容,请点击下方链接对我进行反馈。
feedback