JMM:Java内存屏障,不是Java内存模型!!!

这是我参与11月更文挑战的第16天,活动详情查看:2021最后一次更文挑战

背景

随着cpu由单核变成多核,又有了超线程。所以就会出现这样的问题,多核cpu在各自的缓存处理数据后,当同步数据到同一块主内存时,无法确定以谁的缓存数据为准。

所以为了解决cpu缓存一致性的问题,特地制定了一些操作协议,例如MSI、MOSI、Firefly等。而在这些操作协议下,对特定的内存或高速缓存进行读写访问的过程,就是内存模型。不同架构(ARM/X86等)的物理机有不同的内存模型。 在这里插入图片描述

Java为了一次编译、到处运行的跨平台性,使用class字节码作为中间代码,让不同架构的系统通过JVM来转换为对应的机器码。同样,为了屏蔽不同架构的机器上的内存访问差异,让Java程序在各种平台下都能达到一致的内存访问效果,所以Java制定了一套内存操作协议,即Java内存模型。

JMM(Java Memory Model)可以理解成一种协议,通过各种操作来定义对变量的读写、监视器的加锁和释放、线程启动和合并,保证多线程下变量的缓存一致性。 在这里插入图片描述

工作内存和主内存

JMM主要目标是定义程序中各个变量的访问规则,即在JVM中将变量存储到内存和从内存中取出变量这样的底层细节,来实现缓存一致性。JMM在定义上将内存分为主内存和工作内存,主内存对应Heap,属于线程公有,工作内存对应虚拟机栈,属于每个线程私有。

同时JMM对内存中的变量做了以下规定:

  1. 规定所有的变量都存储在主内存中,线程不能直接读写主内存中的变量
  2. 每个线程有自己的工作内存,对变量的读取、赋值等必须在工作内存中进行
  3. 线程之间值的传递都需要通过主内存来完成。工作内存无法互相访问
  4. 工作内存保存了线程用到的变量的主内存副本拷贝

原子操作

JMM规定了变量在主内存和工作内存中如何传输,同时也提供了八个原子指令来具体实现细节。 在这里插入图片描述 对于上面八个原子操作的使用,JMM制定了一些规则:

  1. read/load、store/write必须成对出现
  2. 不允许线程丢弃最近的assign操作,即工作内存中变量修改之后必须同步到主内存
  3. 不允许线程无原因地(没有assign操作)将变量工作内存同步到主内存
  4. 变量只能在主内存中诞生,并且必须在工作内存中初始化才能使用。即use、store之前必须经过load和assign
  5. 一个变量在同一时刻只能被一个线程lock。但一个线程可以lock多次。几次lock,只有对应次数的unlock变量才能解锁
  6. 一个变量被lock后,会清空所有工作内存中此变量的值。再次使用需要重新load、assign来初始化
  7. 一个变量未被lock,则不允许对它执行unlock,也不允许unlock其他线程lock的变量
  8. 一个变量unlock前,必须先把此变量同步到主内存中(store、write)

如何理解这八条规则和八条指令?

lock/unlock指令让一个变量被一个线程独享,其他六个指令都是变量在工作内存中的赋值和传输操作。JMM的出现是为了让一个线程对变量修改时,其他线程停止修改,并能获取最新值,从而保证数据的一致性。

而lock可以锁定变量,让变量只能被一个线程修改,且其他线程工作内存中的此变量的值会失效,必须通过指令来获取最新的值。

JMM特性

  1. 原子性:lock和unlock提供了大范围的原子性,其他操作都具有原子性
  2. 可见性:当一个线程修改了变量的值,其他线程能立即得知修改
  3. 有序性:在本线程内操作都是有序的,相对于多个线程是无序的

场景分析

synchronize可以看做是lock/unlock的实现,底层是由monitorenter/monitorexit来实现的。

测试代码如下:

public void test() {
    synchronized (this.getClass()) {
        System.out.println("Hello World!");
    }
}
复制代码

字节码指令如下: 在这里插入图片描述 在synchronize的作用范围内,在同一时间内只能一个线程获取锁进行操作,其他线程工作内存中的变量会失效,当此线程修改完unlock的时候,其他线程会重新加载变量最新值来进行操作,从而保证数据的一致性。

volatile关键字也通过内存屏障实现了lock/unlock的功能,下一篇内存屏障会写一下。

Happen-Before先行原则

为了不让所有的操作有序都借助synchronized、volatile来完成,所有就有了以下无需借助同步器的天然先行发生关系,即不用锁就能保证执行顺序。

  1. 程序顺序规则:程序中操作A在B前,线程中A操作也必须在B之前执行
  2. 监视器加锁规则:在监视器锁上的解锁操作必须在加锁之前执行
  3. volatile变量规则:对volatile变量的写入必须在读取之前执行
  4. 线程启动规则:Thread.start()调用必须在该线程执行任何操作之前
  5. 线程结束规则:线程中任何操作都先行发生于对此线程的终止检测
  6. 中断规则:对线程interrupt()的调用先于被中断线程检测到中断事件的发生
  7. 终结器规则:对象构造方法执行先于它的finalize()方法
  8. 传递性:如果操作A先于B,操作B先于C,那操作A必先于C

猜你喜欢

转载自juejin.im/post/7031188400061284359