写个文章是因为一次字节面试中,问到java内存模型了解吗?我答了一些堆、方法区、虚拟机栈什么的。然后说这个不是。我一脸蒙蔽。。。之后了解到JMM,才知道自己有多蠢,原来是这些东西,原来这些叫JMM。
所以,现在写一篇文章总结一下。
大家都知道java是通过java虚拟机来跨平台运行。但,它是怎么实现的呢,有没有什么规则?
答:不同计算机操作系统对内存模型操作不一样,这时候就要有统一的规范来完成操作。所以就要通过JAVA内存模型(Java Memory Model,JMM)
!!!下面2,3,4,5,6段落都是有关JAVA内存模型的相关规范或者规则。!!!
文章最后做总结。
Java内存模型的主要目的是定义程序中各种变量的访问规则
- 关注在虚拟机中把变量值存储到内存和从内存中取出变量值这样的底层细节。
这个段落的目标:针对的是线程之间可以共享的变量。
变量根据是否可以共享划分为:线程私有的和线程公有的。
- 线程私有:局部变量与方法参数
- 线程公有:实例字段、静态字段和构成数组对象的元素
Java内存模型规定了所有的变量都存储在主内存中。每条线程还有自己的工作内存,线程的工作内存中保存了被该线程使用的变量的主内存副本,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的数据。如图所示:
这一部分要和JAVA内存区域作区分。
主内存与工作内存之间具体的交互协议,
Java内存模型中定义了以下8种操作来完成。Java虚拟机实现时必须保证下面提及的每一种操作都是原子的、不可再分的。(简单看看就行)
- lock(锁定):作用于主内存的变量,它把一个变量标识为一条线程独占的状态。
- unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
- read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用。
- load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
- use(使用):作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。
- assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
- store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后的write操作使用。
- write(写入):作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。
Java设计团队,将Java内存模型的操作简化为read、write、lock和unlock四种,但这只是语言描述上的等价化简,Java内存模型的基础设计并未改变。
volatile定义的变量有两个特性:
1. 概念:当一条线程修改了这个变量的值,新值对于其他线程来说是可以立即得知的。而普通变量并不能做到这一点,普通变量的值在线程间传递时均需要通过主内存来完成。
2. 线程安全:
在并发中,并不一定是线程安全。
Java里面的运算操作(这里指的是a=b+1,类似这种,不是a=1这样)符并非原子操作,这导致volatile变量的运算在并发下一样是不安全的。
网上有很多利用线程对一个变量加10000次,但是最后结果不是10000*线程数
变量的++操作在字节码中分解为三个部分(此处并不严谨,代表意思为分成多步骤),这样会导致线程不安全(单独的读写是安全的)。
1. 概念:是指处理器采用了允许将多条指令不按程序规定的顺序分开发送给各个相应的电路单元进行处理。但并不是说指令任意重排,处理器必须能正确处理指令依赖情况保障程序能得出正确的执行结果。
注意:在同一个线程的方法执行过程中无法感知到指令重排序,但是其实其中的一些执行顺序发生了改变但保证结果不变。
2. 禁止指令重排序的例子:单例模式懒汉
class Singleton{ private static volatile Singleton instance = null; private Singleton(){} public static Singleton getInstance(){ if(instance == null){ synchronized(Singleton.class){ if(instance == null){ instance = new Singleton(); } } } return instance; } } 复制代码
代码解释:假如没有volatile修饰,在new Singleton
的时候,对instance
已经赋予了内存空间,但是内存中没有东西。此时有另一个线程获取单例去使用,发现这个内存中没有对象无法使用(就是初始化一半),发生了线程安全的问题。
原理解释:汇编指令中增加lock
修饰
3. 使用原则:
对于上面的Java内存模型要求lock、unlock、read、load、assign、use、store、write这八种操作都具有原子性。
但是对于64位的数据类型(long和double),在模型中特别定义了一条宽松的规定:允许虚拟机将没有被volatile修饰的64位数据的读写操作划分为两次32位的操作来进行,
经过实际测试,在目前主流平台下商用的64位Java虚拟机中并不会出现非原子性访问行为,但是对于32位的Java虚拟机,譬如比较常用的32位x86平台下的HotSpot虚拟机,对long类型的数据确实存在非原子性访问的风险。
如果Java内存模型中所有的有序性都仅靠volatile和synchronized来完成,那么有很多操作都将会变得非常啰嗦,但是我们在编写Java并发代码的时候并没有察觉到这一点,这是因为Java语言中有一个“先行发生”(Happens-Before)的原则。
Java内存模型下一些天然的先行发生关系,这些先行发生关系无须任何同步器协助就已经存在,可以在编码中直接使用。存在8中规则:
由Java内存模型来直接保证的原子性变量操作包括read、load、assign、use、store和write这六个,我们大致可以认为,基本数据类型的访问、读写都是具备原子性的。
如果应用场景需要一个更大范围的原子性保证,Java内存模型还提供synchronized关键字,因此在synchronized块之间的操作也具备原子性。
可见性就是指当一个线程修改了共享变量的值时,其他线程能够立即得知这个修改。
Java程序中天然的有序性可以总结为:
前半句是指线程内似表现为串行的语义,后半句是指指令重排序现象和工作内存与主内存同步延迟现象。
这篇文章参考:《深入理解Java虚拟机(第3版)》