Java内存模型
一、Java内存模型
定义Java内存模型并非一件容易的事情,这个模型必须定义得足够严谨,才能让Java的并发内存访问操作不会产生歧义;但是,也必须定义得足够宽松,使得虚拟机的实现有足够的自由空间去利用硬件的各种特性(寄存器、高速缓存和指令集中某些特有的指令)来获取更好的执行速度。
1.1 主内存和工作内存
Java内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。此处的变量(Variables)与Java编程中所说的变量有所区别,它包括了实例变量、静态字段和构成数组对象的元素,但不包括局部变量与方法参数,因为后者是线程私有的,不会被共享,自然就不会存在竞争问题。为了获得较好的执行效能,Java内存模型并没有限制执行引擎使用处理器的特定寄存器或缓存来和主内存进行交互,也没有限制即时编译器进行调整代码执行顺序这类优化措施。
Java内存模型规定了所有的变量都存储在主内存(Main Memory)中。每条线程还有自己的工作内存(Working Memory),线程的工作内存中保存了被该线程使用的变量的主内存副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成,线程、主内存、工作内存三者的交互关系如下:
重点理解:主内存、工作内存和Java内存区域中的Java堆、栈、方法区等并不是同一个层次的内存划分,两者没有关系。如果两者一定要勉强对应起来,主内存主要定义于Java堆中的对象实例数据部分,而工作内存则对应于虚拟机栈中的部分区域!
1.2 内存间交互操作
关于主内存与工作内存之间具体的交互协议,即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步到主内存之类的实现细节,Java内存模型中定义了8种操作来完成,虚拟机实现时必须保证下面提及的每一种操作都是原子、不可再分的!如下:
1.3 对volatile型变量的特殊规则
1)可见性
当一个变量定义为volatile之后,它将具备两种特性,第一是保证此变量对所有线程的可见性,这里的“可见性”是指当一个线程修改了这个变量的值,新值对于其他线程来说是可以立即得知的。而普通变量的值不能做到这一点,普通变量的值在线程间传递均需要通过主内存来完成。例如:线程A修改了一个普通变量的值,然后向主内存进行回写,另外一条线程B在线程A回写完成了之后再从主内存进行读取操作,新变量值才会对线程B可见。
volatile变量在各个线程的工作内存中不存在一致性问题(在各个线程的工作内存中,volatile变量也可以存在不一致的情况,但由于每次使用之前都要先刷新(即从主内存中read、load操作),执行引擎看不到不一致的情况,因此可以认为不存在一致性问题),但是Java里面的运算并非原子操作( i++ ,先取值,再加一,最后再赋值)!
private static volatile int race = 0;
public static void increase(){ race++; }
编译后的字节码指令展示:
从字节码分析失败原因:getstatic指令把race 的值读取到操作数栈栈顶时,volatile关键字保证了race 的值在此时是正确的,但是在执行iconst_1 、iadd 这些指令时,其他线程可能已经将 race 的值加大了,而在当前线程的操作数栈栈顶的race 值已经变为过期的数据,所以 putstatic 指令执行后就可能把较小的race 值同步到主内存了。
volatile可见性可以修饰多个线程共享的单一原子变量的操作,例如:
private volatile boolean shutdownRequested; public void shutdown(){ shutdownRequested = true; } public void doWork(){ while(!shutdownRequested){ //TODO } }
2)禁止指令重排序
例子可以参考:https://www.cnblogs.com/blogtech/p/10053638.html
使用volatile变量的第二个语义是禁止指令重排序优化,普通的变量仅仅会保证在该方法的执行过程中所有依赖赋值结果的地方都能获取到正确的结果,而不能保证变量赋值操作的顺序与程序代码中的执行顺序保持一致!
a. 重排序不会对存在依赖关系的操作记性重排序;
b. 重排序不管怎么排,在单线程下程序的执行结果肯定正确,不会改变;
c. volatile变量读操作的性能消耗与普通变量几乎没有什么差别,但是写操作则可能会慢一些,因为它需要在本地代码中插入许多内存屏障指令来保证处理器不发生乱序执行;
d. 使用volatile关键字修改共享变量可以禁止指令重排序,在编译时,会在执行序列原子赋值操作后,会执行一段空操作(相当于插入内存屏障),将变量的值进行”store”和“write”同步到主内存中,进而保证其他线程访问该变量值的正确性!(再次访问时,再次进行”read”,“load”将变量的值同步到工作内存中)!
1.4 原子性、可见性和有序性
volatile修饰的变量,线程在Java工作内存中一旦更新了此变量的值就会立即写入到主内存中,以及每次使用时都会主动将该变量的值从主内存刷新到工作内存中,而普通变量不会保证这一点!
除了volatile之外,Java还有两个关键字能实现可见性,即synchronized和final。同步块的可见性是由“对一个变量执行unlock之前,必须先把此变量同步到主内存中(执行“store”,”write”)这条规则。而fianl修饰的字段在构造器中一旦初始化完成,并且构造器没有把“this”的引用传递出去,那么其他线程中就能看到final字段的值!(final修饰的变量保证多个线程之间可见性,不可变)
问题1:为什么要进行指令重排序呢?同一个线程中串行执行有啥劣势?
1)现在的CPU一般采用流水线来执行指令。一个指令的执行被分成:取指、译码、访存、执行、写回、等若干个阶段。然后,多条指令可以同时存在于流水线中,同时被执行;
2)指令流水线并不是串行的,并不会因为一个耗时很长的指令在“执行”阶段呆很长时间,而导致后续的指令都卡在“执行”之前的阶段上;
3)重排序的目的是为了性能。
问题2:上文讲到,volatile关键字能够保证变量在多个线程中的原子性,比如:变量此时 a = 5 在主内存中,线程A 的 updateA() 方法修改 a = 1,线程B 的 updateB() 方法修改 a = 2;若此时两个线程同时读取到工作内存 a 的值为 5,但是线程 A 执行 updateA() 方法修改 a 的值,同时线程B 也执行 updateB() 方法修改 a 的值,那么线程修改完同步到主内存中,另外一个线程中看到的 a 的值却为 5 ,而不是其他值,为什么?(此问题是本人想的,不知道逻辑对不对?若不对,希望大家给予指导)
1)volatile关键字能够保证原子性,原子性即为一个操作执行过程中不会被中断,要么全部执行要么全部失败,俗称“同生共死”;
2)Java内存模型8中原子操作注意事项:第一变量在工作内存中改变了之后必须把该变化同步到主内存;第二没有发生任何的assign赋值操作,不允许将变量的值从线程中同步到主内存中!
分析原因:上面这两条Java内存模型规则定义的,线程B在修改 a 的值时,可能所看到a 的值已经被线程 A 修改为 1了,但是这并不影响线程 A修改,因为此时对于线程 A来说 a 的值已经不重要,将其修改为 2 即可!