共享模型之内存

1 Java 内存模型

JMM 即 Java Memory Model, 他定义了主存,工作内存抽象概念,底层对应着CPU寄存器、缓存、硬件内存、CPU指令优化等。

  • 原子性 - 保证指令不会收到线程上下文切换的影响
  • 可见性 - 保证指令不会受 cpu 缓存的影响
  • 有序性 - 保证指令不会受 cpu 指令并行优化的影响

2 可见性

2.1 问题

线程并不会因为 run 变量 更改为 false 而停下

static boolean run = true;

public static void main(String[] args) throws InterruptedException {
    new Thread(() -> {
        while (run) {
            // 工作
        }
    }).start();

    TimeUnit.SECONDS.sleep(5);

    run = false;
}

分析:
(1)初始状态,t 线程刚开始从主内存读取了 run 变量的值 到 工作内存
在这里插入图片描述
(2) 因为 t 线程要频繁从主内存中读取 run 的值,JIT编译器会将 run 的值缓存至自己工作内存中 的高速缓存中,减少主存中 run 的访问,提高效率
在这里插入图片描述
(3)5秒之后,main线程修改了 run 的值,并同步至主存,而 t 线程是从自己工作内存中的高速缓存中读取整个变量的值,结果永远是旧值
在这里插入图片描述

2.2 解决

run 变量用 volatile 修饰
加上 volatile ,表示当前变量只能从主存中读取

static volatile boolean run = true;

public static void main(String[] args) throws InterruptedException {
    new Thread(() -> {
        while (run) {
            // 工作
        }
    }).start();

    TimeUnit.SECONDS.sleep(5);

    run = false;
}

2.3 注意

  • volatile 只能保证元素的可见性,不能保证元素的原子性,简单来说就是不能保证读取到的元素是最新的
  • synchronized 语句块可以保证代码块的原子性,也同时保证代码块内变量的可见性,但缺点是 synchronized 是重量级锁操作,性能相对更低

3 有序性

3.1 概念

JVM会在不影响正确性的前提下,可以调整语句的执行顺序

static int i;
static int j;

// 在某个线程内执行以下赋值操作
i = ...;
j = ...;

可以看到,至于先执行 i 还是先执行 j,对最终的结果不会产生影响。所以,上面代码真正执行时,即可以是

i = ...;
j = ...;

又可以是

j = ...;
i = ...;

这种特性被称之为 指令重排,多线程下 指令重排 会影响正确性。

3.2 原理

现代处理器会设计为一个时钟周期完成一条执行时间最长的 CPU 指令。
指令可以划分为一个个更小的阶段
例如:每条指令可以分为: 取指令 - 指令译码 - 执行指令 - 内存访问 - 数据写回 五个阶段
在这里插入图片描述
术语参考:

  • instruction fetch (IF)
  • instruction decode (ID)
  • execute (EX)
  • memory access (MEM)
  • register write back (WB)

现代CPU都支持多级指令流水线,例如支持同时执行 取指令 - 指令译码 - 执行指令 - 内存访问 - 数据写回,就可以称之为 五级指令流水线。这时CPU可以在一个时钟周期内同时运行五条指令的不同阶段。本质上,流水线技术并不能缩短单条指令的执行时间,但它变相的提高了指令地吞吐率。

在这里插入图片描述

3.3 问题

static int num = 0;

static int result = 0;
// ----------------------------------------------
new Thread(() -> {
     num = 2;
     ready = true;
 }).start();

 new Thread(() -> {
     if (ready) {
         result = 2 * num;
     } else {
         result = 1;
     }
 }).start();

result 因为指令重排 可能结果为 0

num = 2;
ready = true;

3.4 解决

static volatile boolean ready = false;

4 volatile 原理

volatile 的底层实现原理是内存屏障, Memory Barrier (Memory Fence)

  • 对 volatile 变量的写指令后会加入写屏障
  • 对 volatile 变量的读指令前会加入读屏障

4.1 保证可见性

写屏障(sfence)保证在该屏障之前的,对共享变量的改动,都同步到主存当中

num = 2;
ready = true; // ready 是 volatile 赋值写屏障
// 写屏障

读屏障(Ifence)保证在该屏障之后,对共享变量的读取,加载的都是主存中最新数据

// 读屏障
// ready 是 volatile 读取值带读屏障
if (ready) {
     result = 2 * num;
 } else {
     result = 1;
 }

4.2 保证有序性

写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后

num = 2;
ready = true; // ready 是 volatile 赋值写屏障
// 写屏障

读屏障会确保指令排序时,不会将读屏障之后的代码排在读屏障之前

// 读屏障
// ready 是 volatile 读取值带读屏障
if (ready) {
     result = 2 * num;
 } else {
     result = 1;
 }

4.3 指令交错

  • 写屏障仅仅是保证之后的读能够督导最新的结果,但不能保证读在后面的情况
  • 有序性的保证也只是保证了当前线程内的代码不被重排序

4.4 double-checked locking

  • 多次检查 INSTANCE 是否为空是因为 只有第一次才需要对 Singleton 加锁保护,后面就不需要保护了
  • volatile 修饰 INSTANCE 是为了保证 INSTANCE 的可见性,并且保证后续代码不被指令排序
  • INSTANCE = new Singleton() 两步操作 创建对象 + 赋值引用
public final class Singleton {
    private Singleton() {
        // 构造方法
    }

    private static volatile Singleton INSTANCE = null;

    public static Singleton getInstance() {
        // 实例没创建,才会金融业内部的 synchronized 代码块
        if (INSTANCE == null) {
            synchronized (Singleton.class) {
                // 也许有其他线程已经创建实例,再判断一次
                if (INSTANCE == null) {
                    INSTANCE = new Singleton();
                }
            }
        }
    }
}

习题

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值