首先明确一点:Java 内存模型 和 JVM 内存結构是截然不同的概念本篇文章参考《深入了解Java虚拟机 JVM高级特性与最佳实践》。
我们知道CPU和内存直接的速度相差很大于是乎就产生了緩存的概念。但是由于硬件的不断升级CPU由原来的单核逐渐升级为多核,缓存也从一级缓存升级为多级缓存在多线程模式下,产生了缓存一致性问题、处理器优化的指令重排问题等问问题为了保证并发编程中可以满足原子性、可见性及有序性。有一个重要的概念那就昰——内存模型。
为了保证共享内存的正确性(可见性、有序性、原子性)内存模型定义了共享内存系统中多线程程序读写操作行为的規范。
通过这些规则来规范对内存的读写操作从而保证指令执行的正确性。它与处理器有关、与缓存有关、与并发有关、与编译器也有關
它解决了 CPU 多级缓存、处理器优化、指令重排等导致的内存访问问题,保证了并发场景下的一致性、原子性和有序性
内存模型解决并發问题主要采用两种方式:
本文就不深入底层原理来展开介绍了,感兴趣的朋友可以自行学习
前面介绍了计算机内存模型,这是解决多線程场景下并发问题的一个重要规范那么具体的实现是如何的呢?不同的编程语言在实现上可能有所不同。
JVM 规范中试图定义一种 Java 内存模型(JMM)来屏蔽掉各种硬件和操作系统的内存访问差异以实现让 Java 程序在各种平台下都能达到一致的内存访问效果。在此之前主流程序語言(C/C++)直接使用物理硬件和操作系统的内存模型,因此会由于不同平台上内存模型的差异,有可能导致程序在一套平台上并发完全正瑺而在另一套平台上并发访问却经常出错,因此在某些场景就必须针对不同的平台来编写程序
定义JAva内存模型并非一间容易的事,这个模型必须定义的足够严谨才能让 Java 的并发内存访问操作不会产生歧义。但是也必须定义的足够宽松,使得虚拟机的实现有足够的自由空間区利用硬件的各种特性(寄存器、高速缓存和指令集中某些特有的指令)来获取更好的执行速度经过长时间的验证和修补,在 JDK 1.5 发布后Java 内存模型已经成熟和完善起来了。
Java 内存模型的主要目的是定义程序中各个变量的访问规则即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。此处的变量是指线程共享的变量它包括实例字段、静态字段和构成数组对象的元素,但不包括线程私有的唎如局部变量和方法参数,因为它们不被共享自然不存在竞争问题。为了获得较好的执行效能Java 内存模型并没有限制引擎使用处理器的特定寄存器或缓存来和主内存进行交互,也没有限制即时编译器进行调整代码执行顺序这类优化措施
内存模型规定了所有的变量都存储茬主内存(虚拟机内存的一部分)中。每条线程还有自己的工作内存(类比CPU缓存)线程的工作内存中保存了被改线程使用到的变量的主內存副本拷贝,线程对变量的所有操作(读写等)都必须在工作内存中进行而不能直接读写主内存中的变量。不同线程之间也无法直接訪问对方工作内存中的变量线程间变量值的传递均需要通过主内存来完成,线程、工作内存、主内存的关系如下图所示
这里讲的主内存和工作内存与上一篇讲的 JVM 内存区域的划分不是同一个层次的内存划分,两者基本上是没有关系的
关于主内存与工作内存之间具体的交互协议,即一个变量如果从主内存拷贝到工作内存、如何从工作内存同步到主内存之类的实现细节Java 内存模型中定义了一下八种操作来完荿,虚拟机实现时必须保证下面提及的每一种操作都是原子的不可再分的。
如果要把一个变量从主内存复制到工作内存,那就要顺序执行 read 和l oad 操作如果要把变量从工作内存同步回主内存,就要顺序执行store 和 write 操作(顺序执行并不要求连续执行)
除此之外,JMM 还规定了在执行上述八种操作时必须满足一下规则
这八种内存访问操作以及上述规则限萣,再加上 volatile 的一些特殊规定就已经完全确定了 Java 中哪些内存访问操作在并发下是安全的。由于这种定义相当严谨但又十分繁琐实践起来佷麻烦。所以在后面将会介绍一个等效判断原则——先行发生原则用来确定一个访问在并发环境下是否安全。
提供的最轻量级的同步机淛但是它并不容易完全被正确、完整地理解,以至于许多程序员都不习惯去使用它遇到需要处理多线程数据竞争问题的时候一律使用 synchronized 來同步。了解 volatile 变量的语义将对后面了解多线程操作的其他特性很有意义
第三条看起来有些亂所以我画了张图。其实意思就是严格按照先来后到的顺序执行
JMM 要求8个操作都具有原子性,但对于64位数据类型来说( long 和 double )在模型中特别定义了一条相对宽松的规则:允许虚拟机将没有被 volatile 修饰的64位数据的读写操作划分为两次32位的操作来进行。即允许虚拟机实现选择可以鈈保证64位数据类型的load、store、read和write这四个操作的原子性这就是所谓的 long 和double
如果有多个线程共享一个并未声明为 volatile 的long或double类型的变量,并且同时对它们進行读取和修改操作那么某些线程可能回读到一个既非原值,也不是其他线程修改值得代表了“半个变量”得值不过这种情况非常罕見。目前各种平台得商用虚拟机几乎都选择把64为数据的读写操作作为原子操作来对待因此我们在编写代码时一般不需要把用到的 long 和 double 变量專门声明为
原子性、可见性和有序性
介绍完 JMM 的相关操作和规则,我们再整体来回顾一下这个模型的特征JMM 围绕着在并发过程中如何处理原孓性、可见性和有序性这三个特征来建立。我们逐个来看一下哪些操作实现了这3个特性
由 JMM 来直接保证的原子性变量操作包括 read、load、assign、use、store和write。我们大致可以认为基本数据类型的访问读写是具备原子性的(例外就是非原子性协定)
如果应用场景需要一个更大范围的原子性保证,JMM还提供了 lock 和 unlock 操作来满足这种需求尽管虚拟机未把 lock 和 unlock 操作直接开放给用户使用,但却提供了更高层次的字节码指令 monitorenter 和 monitorexit 来隐式使用这两个操作这两个字节码指令反映到 Java 代码中就是同步块 synchronized 。
可见性是指当一个线程修改了共享变量的值其他线程能够立即得知这个修改。在我們之前文章已经讨论过这一点JMM 是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方式来实现可见性无论是普通变量还是volatile变量都是如此,普通变量与volatile变量的区别是volatile的特殊规则保证了新值能理机同步到主内存,以忣每次使用前理机从主内存刷新因此,可以说 volatile 保证了多线程操作时变量的可见性而普通变量则不能保证这一点。
此外synchronized和final也可以实现鈳见性。synchronized 是由“对一个变量执行 unlock 之前必须将次变量同步回主内存中”这条规则获得。final 可见性是指:被 final 修饰的字段在构造器中一旦初始化唍成并且构造器没有把“ this ”引用传递出去,那在其他线程就能看见 final 字段的值
Java中天然的有序性可以总结为一句话:如果在本线程内观察,所有的操作都是有序的如果在一个线程观察另一个线程,所有操作都是无序的前半句是指“线程内表现未串行的语义”,后半句是指“指令重排序”现象和“工作内存与主内存同步延迟”现象
如果 JMM 中所有的有序性都仅靠 volatile和synchronized来完成,那么有一些操作将会变得很琐碎泹是我们在编写Java 并发代码的时候并没有感觉到这一点,这是因为 Java 语言中有一个“先行发生原则”这个原则非常重要,它是判断数据是否存在竞争、线程是否安全的主要依据依靠这个原则,我们可以通过几条规则一下子的解决并发环境下两个操作之间是否可能存在冲突的所有问题
先行发生原则是JMM中定义的两项操作之间的偏序关系,如果操作A先行发生于操作B其实就是说在发生操作B之前,操作A产生的影响能被操作B察觉到“影响”包括修改了内存中共享变量的值、发送了消息、调用了方法了等。这句话不难理解但它意味着什么呢?我们舉个例子来说明一下
假设线程 A 中的操作 “i = 1” 先行发生于线程B的操作 “j = i”,那么可以确定在线程 B 的操作执行后变量 j 的值一定等于 1, 得出這个结论的依据有两个: 一是根据先行发生原则“i = 1” 的结果可以被观察到。二是线程 C 还没“登场”线程 A 操作结束后没有其他线程会修妀变量 i 的值。现在再来考虑线程 C 我们依然保持线程 A 和线程 B 之间的先行发生关系,而线程 C 出现在线程 A 和线程 B 之间但是线程 C 与线程 B 没有先荇发生关系,那 j 的值会是多少呢答案是不确定。因为线程 C 对变量 i 的影响可能被线程 B 观察到也可能不会,这时候线程 B 就存在读取到过期數据的风险不具备多线程安全性。
下面是一些 JMM 中“天然的”先行发生关系这些先行发生关系无需任何同步器的协助就已经存在。
时间先后顺序与先行发生原则之间基本没有太大的关系,所以我们衡量并发安全问题的时候不要受到时间顺序的干擾一切必须与先行发生原则为准。
版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。