深入学习掌握JUC并发编程系列(四) -- 深入剖析Java内存模型

一、概述

  • JMM(Java Memory Model):
    • 一种抽象的概念,描述了一组规则或规范,通过规范定义了程序中各个变量的访问方式
    • 定义了主存(线程共享数据)、工作内存(线程私有数据)的抽象概念
    • 工作内存是从主内存拷贝的副本,当线程启动时,从主内存中拷贝副本到工作内存,执行相关指令操作,最后写回主内存
  • 三大特性:
    • 原子性:保证指令不会受到线程上下文切换的影响( 线程对共享变量的操作,不受其它线程干扰)
    • 可见性:保证指令不会受到 cpu 缓存的影响(线程修改共享变量值后,另一个线程能否立刻知道)
    • 有序性:保证指令不会受到 cpu 指令并行优化的影响(多线程下,指令重排)

二、可见性

  • 问题:因为while(run),t线程频繁从主内存中读取run的值,JIT即时编译器为了提高效率,会将run的值缓存到t线程的工作内存中,当主线程修改了主内存中的值后,t线程从工作内存读取的仍然是旧值
static boolean run = true;
public static void main(String[] args) throws InterruptedException {  
    Thread t = new Thread(()->{
        while(run){
            // ....
        }
    });
    t.start();
   
    sleep(1);
    run = false; // 线程t不会如预想的停下来
}

  • 描述:一个线程对主内存中的数据修改后,对于另一个线程不可见
  • 解决:
    • 在共享变量定义时,加volatile关键字修饰(禁止线程从缓存中获取数据,只能从主内存中读取)
    • 在读取数据和修改数据的代码块上加锁(synchronized会清空工作内存)
  • volatile(易变的):修饰成员变量和静态成员变量,让线程必须从主内存中获取变量值

三、可见性VS原子性

  • 可见性保证的是一个线程对 volatile 变量的修改对另一个线程可见,不能保证原子性
  • 可见性只能保证看到最新值,不能解决指令交错问题(原子性)
  • 适用情况:仅用在一个写线程,多个读线程的情况
  • synchronized既可以保证代码块的原子性,又可以保证变量的可见性(缺点是重量级操作,性能低)

四、有序性

  • 指令重排:JIT即时编译器会在不影响正确性的前提下,调整语句的执行顺序(单线程没问题,多线程影响正确性)
  • 一条指令可以分为:取指令 - 指令译码 - 执行指令 - 内存访问 - 数据写回,五个阶段
  • 多级(五级)指令流水线:CPU可以在一个时钟周期内,同时运行五条指令的不同阶段
  • 指令级并行:在不改变程序结果的前提下,指令的各个阶段可以重排序和组合
  • 解决:volatile 修饰的变量,可以禁用指令重排

五、volatile原理(JDK1.5之后)

  • volatile 的底层实现原理是内存屏障(Memory Barrier/fence) :
    • 对 volatile 变量的写指令后会加入写屏障
    • 对 volatile 变量的读指令前会加入读屏障
  • volatile保证可见性:
    • 写屏障(sfence):在写屏障之前的,对共享变量的改动(赋值操作),都同步到主内存当中
    • 读屏障(lfence):在读屏障之后的,对共享变量的读取,加载的是主内存中最新数据
  • volatile保证有序性:指令重排序时
    • 写屏障:确保不会将写屏障之前的代码排在写屏障之后
    • 读屏障:确保不会将读屏障之后的代码排在读屏障之前
  • volatile无法保证原子性:不能解决指令交错问题(synchronized可以解决三种特性问题)
    • 写屏障是保证之后的读能够读到最新的结果,不能保证其它线程的读跑到它前面去
    • 有序性的保证只是保证了本线程内相关代码不被重排序

六、DCL懒汉式单例模式

public final class Singleton {
    private Singleton() { }
    private static Singleton INSTANCE = null;
   
    public static Singleton getInstance() {
        if(INSTANCE == null) {           
            synchronized(Singleton.class) {
                if (INSTANCE == null) {
                    INSTANCE = new Singleton();
                }
            }
        }
        return INSTANCE;
    }
}
  • 单例模式:一个类只有一个实例,通过构造器私有化实现
  • 饿汉式:类加载时单例对象就被创建;懒汉式:首次使用单例对象时才被创建
  • DCL:double-checked-locking(双重 if 判断)
    • 懒惰实例化:用到时才创建
    • 首次使用 getInstance() 才使用 synchronized 加锁,后续使用时无需加锁
  • 字节码解析(INSTANCE = new Singleton();):
    • 17行(new):创建对象,将对象引用入栈 // new Singleton
    • 20行(dup):复制一份对象引用 // 引用地址
    • 21行(invokespecial):根据引用地址,调用构造方法
    • 24行(putstatic):利用对象引用,赋值给静态变量 INSTANCE
  • DCL懒汉式单例问题:
    • jvm可能会对指令顺序优化:先执行 24,再执行 21
    • 第一个 if 判断使用的 INSTANCE 变量,是在synchronized同步块之外
    • t2线程在t1线程还未完成21(调用构造方法),就读取了INSTANCE的值(未初始化的单例)
  • 问题根本:JIT在创建对象的过程中,会将对象引用赋值给变量(24)和构造方法初始化(21)这两条指令顺序进行优化
  • 问题解决:volatile关键字修饰INSTANCE,禁止指令重排序

七、happens-before规则

  • 可见性与有序性的一套规则总结
  • 规定了对共享变量的写操作对其它线程的读操作可见:
    • 线程解锁(synchronized)之前对变量的写,对于接下来加锁的其它线程对该变量的读可见
    • 线程对 volatile 变量的写,对接下来其它线程对该变量的读可见
    • 线程 start 前对变量的写,对该线程开始后对该变量的读可见
    • 线程运行结束前对变量的写,对其它线程得知它结束(join)后的读可见
    • 线程 t1 打断 t2(interrupt)前对变量的写,对于其他线程得知 t2 被打断后对变量的读可见(通过t2.interrupted 或 t2.isInterrupted)
    • 对变量默认值(0,false,null)的写,对其它线程对该变量的读可见
    • 可见传递性:
volatile static int x;
static int y;
new Thread(()->{
    y = 10;
    x = 20;
},"t1").start();
 
new Thread(()->{
    // x=20 对 t2 可见, 同时 y=10 也对 t2 可见
    System.out.println(x);
},"t2").start();

总结

  • JMM:
    • 可见性 - 由 JVM 缓存优化引起(一个读,多个写)
    • 有序性 - 由 JVM 指令重排序优化引起
    • happens-before 规则(写入对其它线程可见)
  • 原理:
    • CPU指令并行
    • volatile(写屏障之前,读屏障之后)
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值