这篇文章我们主要聊聊关于JAVA 内存模型(JAVA MEMORY MODEL, JMM)的相关知识
二、java内存模型
1.三大特性
JAVA内存模型遵循的三大特性:
1.原子性(Atomicity):原子代表不可切割的最小单位。原子性是指一个操作或多个操作要么全部执行,且执行的过程不会被任何因素打断,要么就都不执行。
2.可见性:一个线程修改了一个变量的值后,其他线程立即可以感知到这个值的修改。
3.有序性:此有序在单线程下即为语句的顺序,但在多线程中的运行顺序一般以程序先行发生原则为依据:
2.程序先行发生原则(happen-before):
1、程序次序原则
在一个线程内部,按照代码的顺序,书写在前面的先行发生与后边的。 或者更准确的说是在控制流顺序前面的先行发生与控制流后面的,而不是代码顺序,因为会有分支、跳转、循环等。
2、管程锁定规则
一个unlock操作先行发生于后面对同一个锁的lock操作。这里必须注意的是对同一个锁,后面是指时间上的后面
3、volatile变量规则
对一个volatile变量的写操作先行发生与后面对这个变量的读操作,这里的后面是指时间上的先后顺序
4、线程启动规则
Thread对象的start()方法先行发生与该线程的每个动作。 当然如果你错误的使用了线程,创建线程后没有执行start方法,而是执行run方法,那此句话是不成立的,但是如果这样其实也不是线程了
5、线程终止规则
线程中的所有操作都先行发生与对此线程的终止检测,可以通过Thread.join()和Thread.isAlive()的返回值等手段检测线程是否已经终止执行
6、线程中断规则
对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测到是否有中断发生。
7、对象终结规则
一个对象的初始化完成先行发生于他的finalize方法的执行,也就是初始化方法先行发生于finalize方法
8、传递性
如果操作A先行发生于操作B,操作B先行发生于操作C,那么操作A先行发生于操作C。
volatile关键字:
一旦一个变量呗volatile修饰之后,它便具有以下语义:
1)对所有不同线程间的可见性,也就是时间维度上的顺序先行顺序。(可见性)
例子:
1: public class VolatileExample extends Thread{
2: //设置类静态变量,各线程访问这同一共享变量
3: private static boolean flag = false; //1
4: //private static volatile boolean flag = false; //2
5: //无限循环,等待flag变为true时才跳出循环
6: public void run() {while (!flag){};}
7:
8: public static void main(String[] args) throws Exception {
9: new VolatileExample().start();
10: //sleep的目的是等待线程启动完毕,也就是说进入run的无限循环体了
11: Thread.sleep(100);
12: flag = true;
13: }
14: }
当主线程用//1 执行的时候,按照程序的先行发生原则(程序次序原则和线程启动原则),主线程执行完(flag已变为了true),主线程中新建的VolatileExample读取到的flag由于一直是false,则处于无限循环中。
当主线程用//2执行的时候,按照程序现行发生原则(程序次序原则、线程启动原则和变量的volatile原则),当主线程执行完后,flag会同步到VolatileExample线程中,flag置为true,循环结束。
2)禁止指令重排序(有序性)
重排序的定义
大多数现代微处理器都会采用将指令乱序执行(out-of-order execution,简称OoOE或OOE)的方法,在条件允许的情况下,直接运行当前有能力立即执行的后续指令,避开获取下一条指令所需数据时造成的等待。通过乱序执行的技术,处理器可以大大提高执行效率。除了处理器,Java运行时环境的JIT编译器也会做指令重排序操作,即生成的机器指令与字节码指令顺序不一致。
as-if-serial原则
As-if-serial语义的意思是,所有的动作(Action)都可以为了优化而被重排序,但是必须保证它们重排序后的结果和程序代码本身的应有结果一致。
所以,若数据间有依赖关系,则不会发生指令的重排序。
//有数据依赖关系则不会重排序
int a= 1;
b=a+1;
同样,符合程序先行发生原则规定的代码快也同样不会发生指令重排序。
JAVA语言的目标即为成为一门平台无关性的语言,所以不同系统间的指令重排问题则是首要需要解决的问题。为解决此问题,便制定了Java内存模型(Java memory model)模型来屏蔽系统差异性的问题。
内存屏障
内存屏障分为两种:Load Barrier 和 Store Barrier即读屏障和写屏障。
内存屏障有两个作用:
1.阻止屏障两侧的指令重排序;
2.强制把写缓冲区/高速缓存中的脏数据等写回主内存,让缓存中相应的数据失效。
对于Load Barrier来说,在指令前插入Load Barrier,可以让高速缓存中的数据失效,强制从新从主内存加载数据;
对于Store Barrier来说,在指令后插入Store Barrier,能让写入缓存中的最新数据更新写入主内存,让其他线程可见。
java的内存屏障通常所谓的四种即LoadLoad,StoreStore,LoadStore,StoreLoad实际上也是上述的两两组合,完成一系列的屏障和数据同步功能。
LoadLoad屏障:对于这样的语句Load1; LoadLoad; Load2,在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
StoreStore屏障:对于这样的语句Store1; StoreStore; Store2,在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。
LoadStore屏障:对于这样的语句Load1; LoadStore; Store2,在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。
StoreLoad屏障:对于这样的语句Store1; StoreLoad; Load2,在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。它的开销是四种屏障中最大的。在大多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能
volatile的内存屏障策略非常严格保守,非常悲观且毫无安全感的心态:
在每个volatile写操作前插入StoreStore屏障,在写操作后插入StoreLoad屏障;
在每个volatile读操作前插入LoadLoad屏障,在读操作后插入LoadStore屏障;
由于内存屏障的作用,避免了volatile变量和其它指令重排序、线程之间实现了通信,使得volatile表现出了锁的特性。
volatile 的读性能消耗与普通变量几乎相同,但是写操作稍慢,因为它需要在本地代码中插入许多内存屏障指令来保证处理器不发生乱序执行。
synchronized关键字:
sychronized关键字在CPU 指令中不仅加入了内存屏障,也通过monitorenter 和 monitorexit 两个指令(即锁功能)实现了其线程内部的排他性,synchronized即保证了可见性,有序性,和原子性。
转载:https://blog.csdn.net/weixin_41855204/article/details/105660562