Java内存模型JMM
概述
-
JMM(Java内存模型Java Memory Model,简称JMM)本身是一种抽象的概念 并不真实存在,它描述的是一组规则或规范通过规范定制了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。
-
关键技术点都是围绕多线程的可见性、原子性、有序性展开的
-
为什么会推导出JMM模型呢?
- 因为cpu中还有多级缓存,速度远快于内存,cpu的运行不是直接操作内存,而是先读取到缓存中,这就会导致读写时数据不一致的情况
- Java虚拟机规范中试图定义一种Java内存模型(java Memory Model,简称JMM)来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果。推导出我们需要知道JMM
内存到线程的原子操作
一个变量如何从主内存拷贝到工作内存、如何从工作内存同步到主内存之间的实现细节,Java内存模型定义了以下八种操作来完成
- lock(锁定):作用于主内存的变量,把一个变量标记为一条线程独占状态
- unlock(解锁):作用于主内存的变量,把一个处于锁定状态的变量释放出来,释放后 的变量才可以被其他线程锁定
- read(读取):作用于主内存的变量,把一个变量值从主内存传输到线程的工作内存 中,以便随后的load动作使用
- load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工 作内存的变量副本中
- use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎
- assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内 存的变量
- store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存 中,以便随后的write的操作
- write(写入):作用于工作内存的变量,它把store操作从工作内存中的一个变量的值 传送到主内存的变量中
JMM三大特性
可见性
即一个线程修改了变量,其他线程是否能立即知道
也就是说,不能保证可见性会出现脏读的情况
原子性
原子性,是指一个线程的操作是不能被其他线程打断,同一时间只有一个线程对一个变量进行操作。在多线程情况下,每个线程的执行结果不受其他线程的干扰。
有序性
程序是按代码顺序执行的,对单线程来说确实是如此,但在多线程情况下就不是如此了。为了优化程序执行和提高 CPU 的处理性能,JVM 和操作系统都会对指令进行重排,也就说前面的代码并不一定都会在后面的代码前面执行,即后面的代码可能会插到前面的代码之前执行,只要不影响当前线程的执行结果。
先行发生原则 Happens-Before
概述
先行发生原则是判断数据是否存在竞争,线程是否安全的有效手段。先行发生是Java内存模型中定义的两项操作之间的偏序关系,比如说操作A先行发生于操作B,其实就是说发生在B之前,操作A产生的影响能被B观察到,“影响”包括修改了内存中共享变量的值,发送了消息,调用了方法的等。
如果一个操作happens-before另一个操作,那么第一个操作的执行结果对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前
两个操作之间存在happens-before关系,并不意为着一定要按照happens-before原则制定的顺序来执行。如果重排序之后的执行结果与按照happens-before关系来执行的结果一致,那么这种重排序并不非法(可以指令重排)
简而言之,如果先行发生原则是一种抽象的规则或者说一种概念一种约定。如果A操作对于B操作 先行发生 ,则A对B可见,且执行顺序为 A - B(可能进行重排序,但结果为此顺序执行的结果)。
八种规则
用于判断两个操作是否先行发生的规则:
-
次序规则
- 同一线程内,写在前面的操作先行发生于后面的操作
- 前一个操作的结果可以被后续的操作获取。即,比如前一个操作执行x=1,则后续的操作能获取到x=1
-
锁定规则
- 一个unlock操作先行发生于后一个lock操作,即一把锁只有unlock了,才能被下一个线程获取
-
volatile变量规则
- volatile写先行发生于后面对这个变量的读
-
传递规则
- 如果AB,BC,则AC
-
线程启动规则(Thread Start Rule)
- Thread对象的start( )方法先行发生于线程的每一个动作,即线程要先start 才能执行线程内的代码
-
线程中断规则(Thread Interruption Rule)
- 线程interrupt()先行发生于interrupted()被检测到中断
-
线程终止规则(Thread Termination Rule)
- 线程中的所有操作都先行发生于对此线程的终止检测
-
对象终结规则(Finalizer Rule)
- 对象没有完成初始化之前,是不能调用finalized( )方法的
volatile关键字
概述
volatile是Java提供的一种轻量级的同步机制。Java 语言包含两种内在的同步机制:同步块(或方法)和 volatile 变量,相比于synchronized(synchronized通常称为重量级锁),volatile更轻量级,因为它不会引起线程上下文的切换和调度。但是volatile 变量的同步性较差(有时它更简单并且开销更低),而且其使用也更容易出错。
其作用是修饰变量。能够保证 可见性和有序性 但不能保证 原子性。即,可以防止出现脏读,但无法防止多个线程对同一数据的修改问题
- 当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值立即刷新回主内存中。
- 当读一个volatile变量时,JMM会把该线程对应的本地内存设置为无效,直接从主内存中读取共享变量
- 所以volatile的写内存语义是直接刷新到主内存中,读的内存语义是直接从主内存中读取
volatile能够保证可见性:即修改的v变量能够立即刷新到主内存
volatile能够保证有序性:即可以利用内存屏障禁止指令重排
内存屏障
概述
内存屏障,是一类同步屏障指令,是CPU或编译器在对内存随机访问的操作中的一个同步点,使得此点之前的所有读写操作都执行后才可以开始执行此点之后的操作,避免代码重排序。
内存屏障其实就是一种JVM指令,Java内存模型的重排规则会要求Java编译器在生成JVM指令时插入特定的内存屏障指令,通过这些内存屏障指令,volatile实现了Java内存模型中的可见性和有序性,但volatile无法保证原子性。
原理
- 落地是由volatile关键字,而volatile关键字靠的是StoreStore、StoreLoad 、LoadLoad、LoadStore四条指令
- 当我们的Java程序的变量被volatile修饰之后,会添加一个ACC_VOLATI LE,JVM会把字节码生成为机器码的时候,发现操作是volatile变量的话,就会根据JVM要求,在相应的位置去插入内存屏障指令
四种内存屏障
也可以大致分为两种:
写屏障:StoreStore、StoreLoad
读屏障:LoadLoad、LoadStore
Store、Load则是程序向内存中写入,读取数据的操作。
写屏障
在每个volatile写操作的前⾯插⼊⼀个StoreStore屏障
在每个volatile写操作的后⾯插⼊⼀个StoreLoad屏障
也就是说,对于普通读写 -> volatile写
是不可以重排序的。对于volatile写->普通读写
可以重排序
读屏障
在每个volatile读操作的后⾯插⼊⼀个LoadLoad屏障
在每个volatile读操作的后⾯插⼊⼀个LoadStore屏障
也就是说,对于普通读写 -> volatile读
是可以重排序的。对于volatile写->普通读写
不可以重排序
可见性案例
@Data
class Resource {
// private volatile int number=0;
private int number = 0;
}
public class VolatileDemo1 {
public static void main(String[] args) {
Resource resource = new Resource();
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "\t coming ");
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
resource.setNumber(60);
System.out.println(Thread.currentThread().getName() + "\t update " + resource.getNumber());
}, "线程A").start();
//如果主线程访问resource.number==0,那么就一直进行循环
while (resource.getNumber() == 0) {
// 注意,此处使用sout打印则会退出循环,因为system会调用flush(),刷新所有正用于缓存优化信息的资源,导致线程重新从主内存读取num数据。
// System.out.println(resource.getNumber());
}
//如果执行到了这里,证明main现在通过resource.number的值为60
System.out.println(Thread.currentThread().getName() + "\t" + resource.getNumber());
}
}
上面的代码经验证会发现,如果变量经过volatile修饰,则最终主线程会读取到数据变化,否则不会,而是一直循环。
使用场景
-
单一赋值的场景,但是含复合运算赋值不可以(i++之类)
状态标志,判断业务是否结束
// 这种直接赋值的
volatile int a = 10
volatile boolean flag = false
- 开销较低的读,写锁策略
public class UseVolatileDemo{
/**
* 使用:当读远多于写,结合使用内部锁和 volatile 变量来减少同步的开销
* 理由:利用volatile保证读取操作的可见性;利用synchronized保证复合操作的原子性
*/
public class Counter{
private volatile int value;
public int getValue(){
return value; //利用volatile保证读取操作的可见性
}
public synchronized int increment(){
return value++; //利用synchronized保证复合操作的原子性
}
}
}