大厂面试之JMM模型

一、JMM模型

1 推导出我们需要知道JMM

因为有这么多级的缓存(cpu和物理主内存的速度不一致的),CPU的运行并不是直接操作内存而是先把内存里边的数据读到缓存,而内存的读和写操作的时候就会造成不一致的问题

Java虚拟机规范中试图定义一种Java内存模型(java Memory Model,简称JMM) 来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果。推导出我们需要知道JMM

2 Java内存模型

JMM(Java内存模型Java Memory Model,简称JMM)本身是一种抽象的概念并不真实存在它仅仅描述的是一组约定或规范,通过这组规范定义了程序中(尤其是多线程)各个变量的读写访问方式并决定一个线程对共享变量的写入何时以及如何变成对另一个线程可见,关键技术点都是围绕多线程的原子性、可见性和有序性展开的。

3 能干嘛?

  • 通过JMM来实现线程和主内存之间的抽象关系
  • 屏蔽各个硬件平台和操作系统的内存访问差异以实现让Java程序在各种平台下都能达到一致的内存访问效果。

4 JMM规范下,三大特性

4.1 可见性

是指当一个线程修改了某一个共享变量的值,其他线程是否能够立即知道该变更 ,JMM规定了所有的变量都存储在主内存中。

java中普通的共享变量不保证可见性,因为数据修改被写入内存的时机是不确定的,多线程并发下很可能出现"脏读",所以每个线程都有自己的工作内存,线程自己的工作内存中保存了该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取,赋值等 )都必需在线程自己的工作内存中进行,而不能够直接读写主内存中的变量。不同线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成

4.2 原子性

指一个操作是不可中断的,即多线程环境下,操作不能被其他线程干扰

4.3 有序性

对于一个线程的执行代码而言,我们总是习惯性认为代码的执行总是从上到下,有序执行。但为了提供性能,编译器和处理器通常会对指令序列进行重新排序。指令重排可以保证串行语义一致,但没有义务保证多线程间的语义也一致,即可能产生"脏读",简单说,两行以上不相干的代码在执行的时候有可能先执行的不是第一条,不见得是从上到下顺序执行,执行顺序会被优化。

  • 源代码 ——》编译器优化重排——》指令并行重排——》内存系统重排——》最终指令的执行

单线程环境里面确保程序最终执行结果和代码顺序执行的结果一致。处理器在进行重排序时必须要考虑指令之间的数据依赖性多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测

5 多线程对变量的读写过程

  • 我们定义的所有共享变量都储存在物理主内存中
  • 每个线程都有自己独立的工作内存,里面保存该线程使用到的变量的副本(主内存中该变量的一份拷贝)
  • 线程对共享变量所有的操作都必须先在线程自己的工作内存中进行后写回主内存,不能直接从主内存中读写(不能越级)
  • 不同线程之间也无法直接访问其他线程的工作内存中的变量,线程间变量值的传递需要通过主内存来进行(同级不能相互访问)

6 多线程先行发生原则之happens-before

在JMM中,如果一个操作执行的结果需要对另一个操作可见性或者代码重排序,那么这两个操作之间必须存在happens-before关系。

6.1 先行发生原则说明

如果Java内存模型中所有的有序性都仅靠volatile和synchronized来完成,那么有很多操作都将会变得非常啰嗦,但是我们在编写Java并发代码的时候并没有察觉到这一点。

我们没有时时、处处、次次,添加volatile和synchronized来完成程序,这是因为Java语言中JMM原则下,有一个“先行发生”(Happens-Before)的原则限制和规矩

这个原则非常重要

它是判断数据是否存在竞争,线程是否安全的非常有用的手段。依赖这个原则,我们可以通过几条简单规则一揽子解决并发环境下两个操作之间是否可能存在冲突的所有问题,而不需要陷入Java内存模型苦涩难懂的底层编译原理之中。

6.2 happens-before总原则

如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。

两个操作之间存在happens-before关系,并不意味着一定要按照happens-before原则制定的顺序来执行。如果重排序之后的执行结果与按照happens-before关系来执行的结果一致,那么这种重排序并不非法。

6.3 happens-before之8条

1:次序规则:一个线程内,按照代码顺序,写在前面的操作先行发生于写在后面的操作,前一个操作的结果可以被后续的操作获取。
讲白点就是前面一个操作把变量X赋值为1,那后面一个操作肯定能知道X已经变成了1。

2:锁定规则:一个unLock操作先行发生于后面((这里的“后面”是指时间上的先后))对同一个锁的lock操作;

即:上一个线程Lock了,下一个线程才能获得锁,然后再进行加锁

3:volatile变量规则:对一个volatile变量的写操作先行发生于后面对这个变量的读操作,前面的写对后面的读是可见的,这里的“后面”同样是指时间上的先后。

4:传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C;

5:线程启动规则(Thread Start Rule):Thread对象的start()方法先行发生于此线程的每一个动作

6:线程中断规则(Thread Interruption Rule):对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生;可以通过Thread.interrupted()检测到是否发生中断

7:线程终止规则(Thread Termination Rule):线程中的所有操作都先行发生于对此线程的终止检测,我们可以通过Thread::join()方法是否结束、Thread::isAlive()的返回值等手段检测线程是否已经终止执行。

8:对象终结规则(Finalizer Rule):一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始,即:对象没有完成初始化之前,是不能调用finalized()方法的。

二、volatile与Java内存模型

1 volatile的特点

  • 可见性
  • 有序性(禁止指令重排)
  • 不保证原子性

2 volatile的作用

  • 当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值立即刷新回主内存中。

  • 当读一个volatile变量时,JMM会把该线程对应的本地内存设置为无效,直接从主内存中读取共享变量

    所以volatile的写内存语义是直接刷新到主内存中,读的内存语义是直接从主内存中读取。

3 volatile为什么可以保证可见性和有序性?

通过内存屏障 (Memory Barriers / Fences)保证可见性和有序性,在JAVA中有个unsafe类(可以说是内存屏障的后门),里面方法可以调用到内存屏障。

内存屏障在虚拟机中有四个指令

屏障类型指令指示说明
LoadLoadLoad1,LoadLoad,Load2保证Load1的读取操作在Load2之后续读取操作之前执行
StoreStoreStore1,StoreStore,Store2在Store2及其后的操作前执行,保证Store1的写操作已经刷新到主存
LoadStoreLoad1,LoadStore,Store2在Store2及其后的写操作执行前,保证Load1的读操作已经结束
StoreLoadStore1,LoadStore,Load2保证Store1的写操作一刷新到主内存之后,Load2及其后的读操作才能执行
  • StoreStore屏障可以保证在volatile写之前,其前面的所有普通写操作都已经刷新到主内存中。
  • StoreLoad屏障的作用是避免volatile写与后面可能有的volatile读/写操作重排序
  • LoadLoad屏障用来禁止处理器把上面的volatile读与下面的普通读重排序。
  • LoadStore屏障用来禁止处理器把上面的volatile读与下面的普通写重排序。

写操作:

  • 在每个 volatile 写操作的前⾯插⼊⼀个 StoreStore 屏障
  • 在每个 volatile 写操作的后⾯插⼊⼀个 StoreLoad 屏障

读操作

  • 在每个 volatile 读操作的后⾯插⼊⼀个 LoadLoad 屏障
  • 在每个 volatile 读操作的后⾯插⼊⼀个 LoadStore 屏障

4:既然一修改就是可见,为什么还不能保证原子性?

要use(使用)一个变量的时候必需load(载入),要载入的时候必需从主内存read(读取)这样就解决了读的可见性。

写操作是把assign和store做了关联(在assign(赋值)后必需store(存储))。store(存储)后write(写入)。
也就是做到了给一个变量赋值的时候一串关联指令直接把变量值写到主内存。

就这样通过用的时候直接从主内存取,在赋值到直接写回主内存做到了内存可见性。

read-load-use 和 assign-store-write 成为了两个不可分割的原子操作,但是在use和assign之间依然有极小的一段真空期,有可能变量会被其他线程读取,导致写丢失一次

但是无论在哪一个时间点主内存的变量和任一工作内存的变量的值都是相等的。这个特性就导致了volatile变量不适合参与到依赖当前值的运算,如i = i + 1; i++;之类的那么依靠可见性的特点volatile可以用在哪些地方呢? 通常volatile用做保存某个状态的boolean值或者 int值

《深入理解Java虚拟机》

由于volatile变量只能保证可见性,在不符合以下两条规则的运算场景,我们仍然要通过加锁(synchronized,java.util.concurrent中锁或原子类)来保证原子性:

  • 运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值
  • 变量不需要与其他状态变量共同参与不变的约束

5 内存屏障是什么?

是一种屏障指令,它使得CPU或编译器对屏障指令的前和后所发出的内存操作,执行一个排序指令。

6 内存屏障能干吗?

  • 阻止屏障两边的指令重排序
  • 写数据时加入屏障,强制将线程私有工作内存的数据刷回主物理内存
  • 读数据时加入屏障,线程私有工作内存的数据失效,重新到主物理内存中获取最新数据

7 凭什么我们java写了一个volatile关键字系统底层加入内存屏障?两者关系怎么勾搭上的?

字节码层面:加了个volatile后,在字节码层面会有一个ACC_VOLATOLE关键字

8 如何正确使用volatile

单一赋值可以,但是含复合运算赋值不可以(i++之类)

//单一赋值
volatile int a = 10;
volatile boolean flag = false;

状态标志,判断业务是否结束

/**
 * 使用:作为一个布尔状态标志,用于指示发生了一个重要的一次性事件,例如完成初始化或任务结束
 * 理由:状态标志并不依赖于程序内任何其他状态,且通常只有一种状态转换
 * 例子:判断业务是否结束
 */
public class UseVolatileDemo{
    private volatile static boolean flag = true;

    public static void main(String[] args){
        new Thread(() -> {
            while(flag) {
                //do something......
            }
        },"t1").start();

        //暂停几秒钟线程
        try { TimeUnit.SECONDS.sleep(2L); } catch (InterruptedException e) { e.printStackTrace(); }

        new Thread(() -> {
            flag = false;
        },"t2").start();
    }
}

开销较低的读,写锁策略

public class UseVolatileDemo{
    /**
     * 使用:当读远多于写,结合使用内部锁和 volatile 变量来减少同步的开销
     * 理由:利用volatile保证读取操作的可见性;利用synchronized保证复合操作的原子性
     */
    public class Counter{
        private volatile int value;

        public int getValue(){
            return value;   //利用volatile保证读取操作的可见性
           }
        public synchronized int increment(){
            return value++; //利用synchronized保证复合操作的原子性
           }
    }
}

DCL双端锁(线程安全的单例模式)

public class SafeDoubleCheckSingleton{
    private static SafeDoubleCheckSingleton singleton;
    //私有化构造方法
    private SafeDoubleCheckSingleton(){
    }
    //双重锁设计
    public static SafeDoubleCheckSingleton getInstance(){
        if (singleton == null){
            //1.多线程并发创建对象时,会通过加锁保证只有一个线程能创建对象
            synchronized (SafeDoubleCheckSingleton.class){
                if (singleton == null){
                    //隐患:多线程环境下,由于重排序,该对象可能还未完成初始化就被其他线程读取
                    singleton = new SafeDoubleCheckSingleton();
                }
            }
        }
        //2.对象创建完毕,执行getInstance()将不需要获取锁,直接返回创建对象
        return singleton;
    }
}

线程安全的单例另一种写法(采用静态内部类的方式实现)

public class SingletonDemo{
    private SingletonDemo() { }

    private static class SingletonDemoHandler{
        private static SingletonDemo instance = new SingletonDemo();
    }

    public static SingletonDemo getInstance(){
        return SingletonDemoHandler.instance;
    }
}
 
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值