0%

JMM

JMM

CPU的运算能力远远超出了主内存的读取数据能力,所以在CPU中存在高速缓存区Cache来作为内存和处理器之间的缓冲(将运算需要使用的数据复制到缓存中,让运算能快速的进行,当运算结束后再从缓存同步回内存之中,这样处理器就无需等待内存读写了),使得CPU在运算过程中直接从高速缓存区中读取数据,其在单线程下是没有问题的,且性能也有了很大的提升。但是在多线程中由于多核CPU每个处理器都有自己的缓存区,导致了数据不一致性,也就是缓存一致性问题。而JMM内存模型就是用来解决该问题的

内存模型

JMM(Java Memony Model)java内存模型,主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量(包括实例变量、静态变量和构成数组对象的元素,但不包括局部变量与方法参数,因为其是线程私有的,不会存在竞争问题)的底层细节,JMM描述了java线程如何通过内存进行交互,java程序中各种变量(线程共享变量)的访问规则,以及在jvm中将变量存储到内存和从内存中读取出变量的底层细节,决定一个线程对共享变量的写入何时对另一个线程可见

  • 线程之间的共享变量都存储在主内存
  • 每个线程都有自己独立的工作内存,里面保存该线程使用到的变量的主内存副本拷贝,线程读写变量时操作的是自己工作内存中的变量(线程操作共享变量时,先从主内存复制共享变量到自己的工作内存,然后对工作内存里的变量进行处理,处理完之后将变量值更新到主内存),不可以直接操作主内存的变量
  • 线程A无法直接访问线程B的工作内存,必须经过主内存
  • 堆内存在线程之间共享,局部变量、方法参数、异常处理器参数不会在线程之间共享
JMM

有两条规定

  • 线程对共享变量的所有操作都必须在自己的工作内存中进行,不能直接从主内存中读写
  • 不同线程之间无法直接访问其他线程工作内存中的变量,线程间变量值的传递需要通过主内存来完成

如何知道共享变量被其他线程更新了呢?

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最新的数据到线程工作内存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
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于出现在其后的任何一个动作
  • 锁定规则:解锁操作必然发生在随后的加锁操作前,对一个监视器的解锁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

欢迎关注我的其它发布渠道