JMM的出现,是为了屏蔽各种硬件和操作系统的内存访问差异,以实现让java程序在各种平台下都能达到一致的内存访问效果。
JMM主要的目标是定义程序中各个变量的访问规则。此处的变量仅考虑在共享区域的变量,共享的变量才有并发的意义,如实例字段,静态字段,够成数组的对象等。而不考虑局部变量,方法参数等线程私有的变量。
物理内存模型
我们可以通过物理的内存模型类比java内存模型。在物理计算机中,处理器需要经常与内存进行I/O操作,而计算机存储设备的速度与CPU运算速度有几个数量级的差距,为缓解这种差距带来的性能浪费,通常会在他们之前引入一个高速缓存设备【1】作为缓冲:将运算所需要的数据复制到缓冲中,让运算能快速执行,当运算结果结束后再从缓冲同步回内存之中。而这样一个环节速度矛盾的方法也会带来一个新问题:缓存一致性【2】。为此,也为处理器引入了一些协议来解决这一问题。除了高速缓存,为提高运算单元的利用率,处理器可能会对输入的代码进行乱序执行优化【3】。
Java内存模型
通过物理内存模型,可以一一对应出java内存模型。【1】引入工作内存与主内存的概念,所有变量都存储在主内存中,每条线程有自己的工作内存,工作内存中保存了线程需要使用到的变量的主内存副本拷贝,线程对变量的操作都必须在工作内存中执行,不同线程之间也无法直接访问对方工作内存中的变量,变量传递需要借助主内存。【2】对于数据一致性问题,JMM也设定了主内存与工作内存中的交互协议。【3】JMM中也存在编译器的指令重排序进行优化,但这一问题也会对并发下的数据一致性产生影响,我们也设定了Happens-before规则保证最基本的代码执行结果正确,若要保证并发下的数据一致性,仍然是需要一些额外措施。
JMM数据一致性:原子性+可见性+有序性
可以看出,设计JMM需要解决的问题是数据一致性问题,在串行下这不算问题,但在并发下就会出现数据不一致。JMM中,只要保证了原子性、可见性和有序性,就能保证并发下的数据一致性。下面,我们分别分析这三点:
A、原子性
java中,对基本数据类型的操作都是原子性。(long和double具有非原子协定:允许虚拟机将没有被volatile修饰的64位数据的读写操作划分为2次32位操作进行)
具有原子性:synchronized、lock、CAS。前两者是通过锁的功能--在一个时刻只能有一个线程访问来保证的,后者CAS是计算机硬件操作,是通过硬件集成保证的。
B、可见性
1. 首先,java为数据一致性设定了主内存与工作内存之间的交互协议。8个基本操作以及与之对应的8个规则。
锁相关:lock、unlock
主内存-->工作内存-->执行引擎:read、load、use (从主内存read进工作内存,load进工作内存的变量副本,在工作内存中使用(给执行引擎))
执行引擎-->工作内存-->主内存:assign、store、write (从执行引擎assign给工作内存中的变量,从工作内存中store进主内存中,在主内存中write进变量中)
8个规则(总结成四个):
(1)必须按这个执行,不允许缺失或乱序 read-->load-->use
(2)assign-->store-->write
(3)一个变量的lock操作,在同一时间内只允许一个线程重复执行多次,并且只有执行相同次数的unlock该变量才能被释放
(4)释放锁unlock之前将最新数据写入主内存,进入锁lock之前将最新数据读入工作内存
2. 具有可见性:volatile 、sychronized、lock、final
其中sychronized和lock具有可见性是因为规则4,而final修饰的是常量,不可变量,必然是可见的(在哪都一样)。
volatile在后面讲解。
C、有序性
1. happens-before(8个),JMM具有的先天有序性
(1)程序次序规则:在一个线程内按顺序执行;
(2)锁定规则:unlock先行发生于对同一个锁的lock;
(3)volatile:对一个volatile的读规则先行发生于对其的写规则;
(4)传递规则:a线性发生于b,b先于c,则a先于c;
(5)线程启动规则:线程的start先行发生于其他所有操作;
(6)线程中断:interrupt()(改变中断标志)先行发生于检测到中断事件的发生;
(7)线程终止:线程其他所有动作先行发生于线程终结检测;
(8)对象终结原则:对象初始化先行发生于finalize()。
2.具有有序性:volatile lock、synchronize(同一时刻只允许一个线程访问+程序次序规则)
volatile在后面讲解。
所以,通过上面的分析可知:lock sychronized是线程安全的,volatile只能保证可见性与有序性,不能保证线程安全。
Volatile
1.保证可见性和有序性
有序性:在编译的时候,编译器总是会进行优化,而volatile的意思是易变化的,就是告诉编译器,volatile修饰的变量随时会被意外修改,这个变量的值在当前上下文看来貌似没什么变化,但是有可能会被其他线程修改。
可见性:指的是看起来好像是直接对主内存进行操作一样,不管什么时候,其他线程看到的必然都是最新的值。
2.那么volatile是如何保证可见性与有序性的呢?
是通过内存屏障来保障的。
通过使用Lock前缀的指令禁止变量在线程工作内存中缓存来保证volatile变量的内存可见性、通过插入内存屏障禁止会影响变量内存可见性的指令重排序
3.为什么要使用Volatile & 适用场景?
Volatile变量修饰符如果使用恰当的话,它比synchronized的使用和执行成本会更低,因为它不会引起线程上下文的切换和调度。
In case only one thread reads and writes the value of a volatile variable and other threads only read the variable, then the reading threads are guaranteed to see the latest value written to the volatile variable. Without making the variable volatile, this would not be guaranteed.
事实上,当只有一个线程对volatile修饰的变量进行读写操作,并且其他线程仅仅只进行读操作时,那么就能保证读线程独到的必然是最新的值。若该变量没有被volatile修饰的话,就不能够保证。
4.Java 中能创建 volatile 数组吗?
能创建volatile数组,但volatile的作用并不能对数组中的元素生效,知识对该数组的引用生效(将数组看成是对象)
5.volatile 能使得一个非原子操作变成原子操作吗?
一个典型的例子是在类中有一个 long 类型的成员变量。如果你知道该成员变量会被多个线程访问,如计数器、价格等,你最好是将其设置为 volatile。为什么?因为 Java 中读取 long 类型变量不是原子的,需要分成两步,如果一个线程正在修改该 long 变量的值,另一个线程可能只能看到该值的一半(前 32 位)。但是对一个 volatile 型的 long 或 double 变量的读写是原子。
网友评论