java内存模型JMM
java内存模型(Java Memory Model,简称JMM) 本身是一种抽象的概念,并不真实存在,它描述的是一组规范或规则
,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素的访问方式).
同时我们知道,jvm运行程序的实体是线程,而每个线程创建时jvm都会为其创建一个工作内存(包括堆栈寄存器,程序计数器,线程控制块
)用于存储其私有的数据.
java内存模型中规定,所有变量都存储在主内存中,主内存是共享内存区域,所有线程都可以访问
,但线程对变量的操作,比如读取,赋值等必须在自己的工作内存中进行.
对变量的操作步骤:
首先,将变量从主内存拷贝到自己的工作内存中去;
然后在自己的工作内存中对变量进行操作,操作完成后,再将变量写回主内存.
注意:
- 不能直接操作主内存中的变量,每个线程的工作内存存储的时主内存中变量的副本拷贝.
工作内存时每个线程的私有区域
,因此不同的线程之间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成.
JMM的主内存
- 主内存中主要存储的是java实例对象,所有线程创建的对象都存放在这里.
- 主内存里还包括了
成员变量
,类信息
,常量
,静态变量
等. - 主内存属于
数据共享
的区域,所以多线程并发操作时会引发线程安全问题.
JMM的工作内存
- 工作内存主要存储当前方法的所有本地变量信息,每个线程只能访问自己的工作内存,所以说本地变量对其他线程不可见.
- 工作内存中还有
字节码行号指示器
,native方法信息
- 工作内存属于线程私有数据区域,
不存在线程安全问题
.
JMM与java内存区域
JMM与java内存区域划分是不同的概念层次
java内存区域就可以直接理解为jvm运行时数据区
- JMM描述的是一组规则,通过这组规则控制程序中各个变量在共享数据区和私有数据区的访问方式,围绕
原子性
,有序性
,可见性
展开. - 两者的相似点: 都存在
共享数据区域
和私有数据区域
,在JMM中主内存属于共享数据区域,从某种程度上讲,它包含了堆和方法区,而工作内存包含了虚拟机栈,本地房发展,程序计数器.
主内存与工作内存的数据存储类型以及操作方式归纳
- 方法里的8种基本数据类型本地变量将直接存储在工作内存的栈帧结构中.
- 如果方法中的本地变量是引用类型的,那么
引用变量存储在工作内存的栈帧中
中,实例对象存储在主内存
中,所以实例对象可以被多线程共享,假设两个线程同时调用了同一个对象的同一个方法,那么两个线程都会将要操作的数据拷贝一份到自己的工作内存中. 成员变量
,static变量
,类信息
均会被存储在主内存中.- 主内存共享的方式是线程个拷贝一份数据到各自的工作内存,操作完成后刷新回主内存.
JMM如何解决可见性问题
总的来说:就是把数据从内存加载到缓存,寄存器,然后运算结束再写回主内存.
在执行程序的时候,为了提高性能,处理器和编译器常常会对指令进行重排序,但是重排序不是随意的,有一定的条件:
- 在
单线程环境下不能改变程序运行的结果
. - 存在
数据依赖关系的不允许重排序
.
总结来说:就是无法通过happens-before原则推导出来的,才能进行指令重排序
JMM内部解决可见性的实现通常是依赖于内存屏障,通过禁止某些重排序的方式
提供内存可见性保证,也就是实现了各种happends-before原则
,与此同时,更多的复杂度在于需要尽量各种编译器和处理器能够提供一致的行为.
happends-before原则
该原则是判断数据是否存在竞争,线程是否安全的关键.
通过这个原则我们就可以解决在并发环境下,两个操作存在冲突
的问题.
如果A操作的结果需要对B操作可见
,则A与B必须存在happens-before关系.
案例:
假设线程A的操作就是令i=1
,且线程A它happens-before线程B的操作j=1
,即线程B要依赖于线程A对j赋值,线程A先于线程B发生
.
所以我们可以确定,线程B执行后,j=1一定是正确 的.
如果他们不存在happends-before原则,那么j=1就不一定成立.
happends-before的八大原则
happends-before概念:
如果两个操作不满足上述任何一个happends-before规则,那么这两个操作就没有顺序的保障,jvm可以对这两个操作进行重排序.
如果操作A happends-before 操作B,那么操作A在内存上所做的操作对操作B都是可见的
.
例子:
假设线程A执行write操作,线程B执行read操作.
他们不满足happends-before原则当中的任何一个,所以这段代码不是线程安全的,read的结果也是不确定的.
解决方法是给value加volatile关键字修饰或者给方法加synchronized关键字修饰
volatile关键字
volatile关键字: 它是jvm提供的轻量级同步机制. 它有如下两个作用:
- 保证被volatile修饰的共享变量对所有线程总是可见的,即当一个线程对volatile修饰的变量修改了其值的时候,其他线程立刻就会知道.
- 禁止指令重排序优化.
volatile的可见性
被volatile修饰的变量对所有线程都是立即可见的,对volatile变量的所有写操作总是能立即反映到其他线程中.
但是, 对于volatile变量运算操作在多线程环境中并不保证安全性.
例子:
上述代码如果多条线程同时调用就会出现问题.
要修改该问题,就需要在方法前加上synchronized修饰.
这样子就不会有线程安全问题了.
synchronized还会创建内存屏障,内存屏障指令保证所有变量的执行结果都会直接刷新到主存中,从而保证了操作的内存可见性,同时也使得先获取该锁的线程的操作都happends-before后获取该锁的线程的操作
所以说,synchronized除了具备可重入性,还具备原子性,可见性,有序性.
volatile变量是如何做到对其他线程立即可见的?
当写一个volatile变量时,JMM会把该线程对应的工作内存中的共享变量值刷新到主内存中
.
当读取一个volatile变量时,JMM会把该线程对应的工作内存置为无效
,那么该线程只能从主内存中读取该变量的值.
volatile如何禁止重排优化?
先看看什么是-内存屏障
内存屏障是一个CPU指令,它的作用有两个:
- 保证特定操作的执行顺序
由于编译器和处理器都能执行指令的重排优化,如果在指令之间插入一个内存屏障
,那么编译器或处理器就知道了这个不能重排序.
即通过插入内存屏障指令禁止对内存屏障前后的指令执行重排序优化.
- 保证某些变量的内存可见性,volatile就是通过这个特性实现的可见性.
强制刷出各种CPU缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本
volatile就是通过内存屏障实现的可见性和禁止重排序优化
线程安全的单例模式
有问题的版本:
如图所示: 当重排序后,如果在还没有初始化之前另一个线程来了,发现instance不为空,直接返回,此时instance还没有初始化,所以就会报错.
更改后是这样的:使用volatile禁止重排序就好了
volatile和synchronized的区别