JMM模型 java内存模型

JMM即为JAVA 内存模型(java memory model)。

JMM是一个抽象模型,它是建立在不同的操作系统和硬件层面之上,对问题进行了统一的抽象。

因为在不同的硬件生产商和不同的操作系统下,内存的访问逻辑有一定的差异,结果就是当你的代码在某个系统环境下运行良好,并且线程安全,但是换了个系统就出现各种问题。Java内存模型,就是为了屏蔽系统和硬件的差异,让一套代码在不同平台下能到达相同的访问结果。

JMM内存模型机构

MM规定了内存主要划分为主内存和工作内存两种。此处的主内存和工作内存跟JVM内存划分(堆、栈、方法区)是在不同的层次上进行的,如果非要对应起来,主内存对应的是Java堆中的对象实例部分,工作内存对应的是栈中的部分区域,从更底层的来说,主内存对应的是硬件的物理内存,工作内存对应的是寄存器和高速缓存。

JMM是如何控制CPU工作内存和主内存间的交互呢?看下面一张图

内存交互操作

内存交互操作有8种,虚拟机实现必须保证每一个操作都是原子的,不可在分的(对于double和long类型的变量来说,load、store、read和write操作在某些平台上允许例外)。

read(读取): 读取主内存的数据

load(载入): 将读取的数据写入工作内存

use(使用): 使用工作内存的变量副本

assign(赋值): 将新的值赋给工作内存中的变量

store(存储): 将工作内存变量写入主内存

write(写入): 将store过去的新的值赋给主内存变量

lock(加锁): 将主内存变量加锁,设置为独占状态

unlock(解锁): 将主内存变量解锁,其他线程可以对变量加锁

模型特征

原子性:例如上面八项操作,在操作系统里面是不可分割的单元。被synchronized关键字或其他锁包裹起来的操作也可以认为是原子的。从一个线程观察另外一个线程的时候,看到的都是一个个原子性的操作。

可见性:每个工作线程都有自己的工作内存,所以当某个线程修改完某个变量之后,在其他的线程中,未必能观察到该变量已经被修改。volatile关键字要求被修改之后的变量要求立即更新到主内存,每次使用前从主内存处进行读取。因此volatile可以保证可见性。除了volatile以外,synchronized和final也能实现可见性。synchronized保证unlock之前必须先把变量刷新回主内存。final修饰的字段在构造器中一旦完成初始化,并且构造器没有this逸出,那么其他线程就能看到final字段的值。

有序性:java的有序性跟线程相关。如果在线程内部观察,会发现当前线程的一切操作都是有序的。如果在线程的外部来观察的话,会发现线程的所有操作都是无序的。因为JMM的工作内存和主内存之间存在延迟,而且java会对一些指令进行重新排序。volatile和synchronized可以保证程序的有序性,很多程序员只理解这两个关键字的执行互斥,而没有很好的理解到volatile和synchronized也能保证指令不进行重排序。

Happen-Before(先行发生规则)

  在常规的开发中,如果我们通过上述规则来分析一个并发程序是否安全,估计脑壳会很疼。因为更多时候,我们是分析一个并发程序是否安全,其实都依赖Happen-Before原则进行分析。Happen-Before被翻译成先行发生原则,意思就是当A操作先行发生于B操作,则在发生B操作的时候,操作A产生的影响能被B观察到,“影响”包括修改了内存中的共享变量的值、发送了消息、调用了方法等。

  Happen-Before的规则有以下几条

    • 程序次序规则(Program Order Rule):在一个线程内,程序的执行规则跟程序的书写规则是一致的,从上往下执行。
    • 管程锁定规则(Monitor Lock Rule):一个Unlock的操作肯定先于下一次Lock的操作。这里必须是同一个锁。同理我们可以认为在synchronized同步同一个锁的时候,锁内先行执行的代码,对后续同步该锁的线程来说是完全可见的。
    • volatile变量规则(volatile Variable Rule):对同一个volatile的变量,先行发生的写操作,肯定早于后续发生的读操作
    • 线程启动规则(Thread Start Rule):Thread对象的start()方法先行发生于此线程的没一个动作
    • 线程中止规则(Thread Termination Rule):Thread对象的中止检测(如:Thread.join(),Thread.isAlive()等)操作,必行晚于线程中所有操作
    • 线程中断规则(Thread Interruption Rule):对线程的interruption()调用,先于被调用的线程检测中断事件(Thread.interrupted())的发生
    • 对象中止规则(Finalizer Rule):一个对象的初始化方法先于一个方法执行Finalizer()方法
    • 传递性(Transitivity):如果操作A先于操作B、操作B先于操作C,则操作A先于操作C

  以上就是Happen-Before中的规则。通过这些条件的判定,仍然很难判断一个线程是否能安全执行,毕竟在我们的时候线程安全多数依赖于工具类的安全性来保证。想提高自己对线程是否安全的判断能力,必然需要理解所使用的框架或者工具的实现,并积累线程安全的经验。

 总结

并发安全性问题有两个因素,一个是高速缓存导致的可见性问题,另一个是指令重排序。

对于缓存一致性问题,有总线锁和缓存锁,缓存锁是基于MESI协议。

对于指令重排序,硬件层面提供了内存屏障指令。 而JMM在这个基础上提供了volatile、final等关键字,使得开发者可以在合适的时候增加相应相应的关键字来禁止高速缓存和禁止指令重排序来解决可见性和有序性问题。

 

参考文献:

https://www.cnblogs.com/null-qige/p/9481900.html

https://blog.csdn.net/qq_35576976/article/details/106388348

 

posted @ 2021-08-25 11:32  精进的浩然兄  阅读(91)  评论(0编辑  收藏  举报