JMM
CPU的运算能力远远超出了主内存的读取数据能力,所以在CPU中存在高速缓存区Cache来作为内存和处理器之间的缓冲(将运算需要使用的数据复制到缓存中,让运算能快速的进行,当运算结束后再从缓存同步回内存之中,这样处理器就无需等待内存读写了),使得CPU在运算过程中直接从高速缓存区中读取数据,其在单线程下是没有问题的,且性能也有了很大的提升。但是在多线程中由于多核CPU每个处理器都有自己的缓存区,导致了数据不一致性,也就是缓存一致性问题。而JMM内存模型就是用来解决该问题的
内存模型
JMM(Java Memony Mode)java内存模型,主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量(包括实例字段、静态字段和构成数组对象的元素,但不包括局部变量与方法参数,因为其是线程私有的,不会存在竞争问题)的底层细节,JMM描述了java线程如何通过内存进行交互,java程序中各种变量(线程共享变量)的访问规则,以及在jvm中将变量存储到内存和从内存中读取出变量的底层细节,决定一个线程对共享变量的写入何时对另一个线程可见
- 线程之间的共享变量都存储在主内存
- 每个线程都有自己独立的工作内存,里面保存该线程使用到的变量的主内存副本拷贝,线程读写变量时操作的是自己工作内存中的变量(线程操作共享变量时,先从主内存复制共享变量到自己的工作内存,然后对工作内存里的变量进行处理,处理完之后将变量值更新到主内存),不可以直接操作主内存的变量
- 线程A无法直接访问线程B的工作内存,必须经过主内存
- 堆内存在线程之间共享,局部变量、方法参数、异常处理器参数不会在线程之间共享
有两条规定
- 线程对共享变量的所有操作都必须在自己的工作内存中进行,不能直接从主内存中读写
- 不同线程之间无法直接访问其他线程工作内存中的变量,线程间变量值的传递需要通过主内存来完成
如何知道共享变量被其他线程更新了呢?
JMM控制主内存与每个线程的本地内存之间进行交互,来提供内存可见性保证
volatile修饰的变量依然有共享内存的拷贝,但是在从工作内存中读写数据前,必须先将主内存中的数据同步到工作内存中,所以看起来如同直接在主内存中读写访问一般
通信流程
线程A和线程B之间交互,有一个共享变量x,在一开始时本地内存A、本地内存B、主内存中x的值都是0,线程A执行后,将x的值置为1,所以线程A的本地内存A中x的值变为1,当线程A和B进行通信时,线程A先将自己本地内存A中的x刷新到主内存中,此时主内存中的x变为了1,随后,线程B从主内存中读取x的值,此时线程B的本地内存B也变成了1
这个流程看起来好像是没有问题,但是在执行过程中,编译器和处理器会对指令进行重排序
- 编译器优化的重排序,编译器在不改变单线程语义程序的语义的情况下,可以重新安排语句的执行顺序
- 指令级并行的重排序,处理器采用了指令并行技术来将多条指令重叠执行,如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序
- 内存系统的重排序,由于处理器使用缓存和读写缓冲区,使得加载和存储操作看上去可能是乱序执行
这也就使得上述的流程出现了问题,在A还没有将本地内存A中的x的值刷到主内存中时,线程B也开始操作x的值,此时从主内存拿到的是x的值是0
内存间交互操作
交互操作
java内存模型中定义了以下8种操作来完成主内存与工作内存之间交互的实现细节
- lock(锁定) 作用于主内存的变量,把一个变量标识为锁定(线程独占)的状态
- unlock(解锁) 作用于主内存的变量,把一个处于锁定状态的变量释放出来,释放后的变量才能被其他线程锁定
- read(读取) 作用于主内存的变量,把一个变量的值从主内存传输到线程的工作内存,以便之后的load动作使用
- load(载入) 作用于工作内存的变量,把read操作从主内存中得到的变量值放入到工作内存的变量副本中
- use(使用) 作用于工作内存的变量,把工作内存中的一个变量传递给执行引擎,当虚拟机遇到一个需要使用变量的值的字节码指令时会执行该操作
- assign(赋值) 作用于工作内存的变量,把一个从执行引擎接收到的值赋给工作内存的变量,当虚拟机遇到一个变量赋值的字节码指令时执行该操作
- store(存储) 作用于工作内存的变量,把工作内存中的一个变量的值传递到主内存中,以便之后的write操作使用
- write(写入) 作用于主内存的变量,把store操作从工作内存中得到的值放入到主内存变量中
在不使用volatile的情况下,也可以使用Thread.sleep(0)或Thread.yield()来主动造成上下文切换,从而使得主内存load最新的数据到线程工作内存
java
public class TestVolation02 {
private static boolean initFlag = false;
public static void main(String[] args) throws InterruptedException {
Thread thread01 = new Thread(new Runnable() {
@Override
public void run() {
while (!initFlag){
// 使用Thread.sleep(0)或Thread.yield()就会导致上下文切换,从而使得从主内存load数据
// try {
//
// Thread.sleep(0);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
Thread.yield();
}
System.out.println("thread02开始执行,此时initFlag状态为"+initFlag);
}
});
thread01.start();
Thread.sleep(1000);
Thread thread02 = new Thread(new Runnable() {
@Override
public void run() {
initFlag = true;
System.out.println("thread02修改initFlag状态为"+initFlag);
}
});
thread02.start();
Thread.sleep(100000);
}
}
交互规则
- 不允许read和load、store和write操作单独出现,以上两个操作必须按照顺序执行,但是指令之间可插入其他指令
- 不允许一个线程丢弃它的最近的assign操作,即变量在工作内存中改变了之后必须把该变化同步到主内存
- 不允许一个线程没有发生过任何assign操作把数据从线程的工作内存同步到主内存中
- 一个新的变量只能从主内存中创建,不允许在工作内存中直接使用一个未被初始化的变量,即对一个变量进行use或store操作之前,必须先执行assign和load操作
- 一个变量在同一时刻只允许一条线成对其进行lock操作,但lock操作可以被同一线程重复多次,多次执行lock后,需要执行相同次数的unlock操作,才会被解锁
- 如果对一个变量执行lock操作,将清空工作内存中此变量的值,在执行引擎使用该变量时,需要重新执行load或assign操作初始化变量的值
- 如果一个变量实现没有被lock操作锁定,则不允许对它执行unlock操作,也不允许unlock一个被其他线程锁定的变量
- 对一个变量执行unlock之前,必须先把变量同步到主内存(执行store和write操作)
内存屏障
内存屏障(Memory Barrier)是一种CPU指令,用于控制特定条件下的重排序和内存可见性问题。java编译器也会根据内存屏障的规则禁止重排序
分为几个类型
-
LoadLoad屏障:对于Load1;LoadLoad;Load2,在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕
-
StoreStore屏障:对于Store1;StoreStore;Store2,在Store2及后续写入操作执行前,保证Store1的写入操作对其他处理器可见
-
LoadStore屏障:对于Load1;LoadStore;Store2,在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕
-
StoreLoad屏障:对于Store1;StoreLoad;Load2,在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。开销最大
happens-before规则
- 程序次序规则:一个线程内保证语义的串行性,同一个线程的每个动作都happens-before于出现在其后的任何一个动作
- 锁定规则:解锁操作必然发生在随后的加锁操作前,对一个监视器的解锁happens-before于每一个后续对同一个监视器的加锁
- volatile变量规则:volatile变量的写先于读发生,保证了volatile变量的可见性,对volatile字段的写入操作happens-before于每一个后续的同一个字段的读操作
- 线程启动规则:线程的start()方法先于它的每一个动作,Thread.start的调用会happens-before于启动线程里的其他操作
- 线程终止规则:Thread中的所有动作都happens-before于其他线程检查到此线程结束
- 线程中断规则:一个线程A调用另一个线程B的interrupt()都happens-before于线程A发现B被A中断
- 对象终结规则:一个对象构造函数结束happens-before于该对象的finalize方法的开始
- 传递规则:如果动作A happens-before于动作B,而动作B happens-before于动作C,则动作A happens-before 于动作C
本文由mdnice多平台发布