Java内存模型相关面试题

一、谈谈你对Java内存模型的理解可以吗?

对于Java内存模型大家千万不能和JVM内存模型弄混了,不一样的;
JVM内存模型是指JVM的内存分区,就是JVM分了几个区域;而Java内存模型是一种虚拟机规范。

首先来看下面一段代码,它的结果可能为1,也可能为2,咱们就借用这个代码来讲解一下Java内存模型;

public class WaitTest {

    static int data = 0;

    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                data++;
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                data++;
            }
        }).start();
    }

}

根据下面这个流程图咱们讲解一下Java内存模型:

  1. Java的每个变量都是放在主内存中,每个线程呢又有自己独立的工作内存,这个工作内存只有当前线程能访问;
  2. 线程1首先会从主内存中读取data的值,以便后续load使用;
  3. 将data装入到工作线程的变量副本中;
  4. 进行data++操作;
  5. 将data++后的值赋值到工作线程的变量副本中;
  6. 将data保存到主内存中,以便后续write使用;
  7. 将data写入到主内存中;

线程2是同样的操作,其实Java内存模型主要的操作就是下图中的6个流程,还有另外两个是lock和unlock;

  • lock(锁定):作用于主内存的变量,把一个变量标识为一条线程独占状态;
  • unlock(解锁):作用于主内存变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定;
  • read(读取):作用于主内存变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用;
  • load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中;
  • use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作;
  • assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作;
  • store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作;
  • write(写入):作用于主内存的变量,它把store操作从工作内存中一个变量的值传送到主内存的变量中;
    在这里插入图片描述

二、你知道Java内存模型中的原子性、有序性、可见性是什么吗?

沿用上面的例子咱们来讲讲这几个特性

  • 可见性
    如果线程1对data进行++后线程2立马就能够读取到最新的值,那么就可以说data是具有可见性的,反之则没有;
  • 原子性
    线程1在进行data++操作时,线程2不能够对data进行操作,那么这个操作就具有原子性,也就是加锁,当下只有一个线程能够执行该操作;
  • 有序性
    指data++操作的代码顺序是有序的,因为CPU在执行代码时会进行指令重排,增加代码执行效率,什么情况下会重排,咱们接下来会讲到;

三、能从Java底层角度聊聊volatile关键字的原理吗?

volatile解决了多个线程之间的可见性,那volatile是如何做到的呢,咱们继续使用下面这个流程图来讲解,假设线程2比线程1慢执行,当线程1已经进行完data++并写入到主内存时,会将其他有使用到data的线程内保存的变量副本设置成失效状态,此时如果线程2正要去use data变量,发现data变量此时为失效状态,那么就会重新去主内存中读取data,这就做到了data的可见性。
在这里插入图片描述

四、你知道指令重排以及happens-before原则是什么吗?

CPU和编译器为了提升程序的执行效率,会对写好的代码进行执行顺序的调换,即写在后面的代码可能会先于前面的代码执行,这就是指令重排,但是并非所有的程序都会进行指令重排,因为一旦程序执行顺序改变可能会造成逻辑错误,所以只有不满足happens-before原则的程序才可能会被指令重排;

happens-before原则一共有8个:

  • 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作;
  • 锁定规则:一个unLock操作先行发生于面对同一个锁的lock操作;
  • volatile变量规则:对一个volatile变量的写操作先行发生于后面对这个volatile变量的读操作,volatile变量写,再是读,必须保证是先写,再度;
  • 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C;
  • 线程启动规则:Thread对象的start()方法先行发生于此线程的每一个动作;
  • 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发送;
  • 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行;
  • 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始;

以上这些规则有些拗口,大家没有必要去背他,只需要知道什么是指令重排,happens-before原则是啥,大概举例几点,还有一个就是volatile可以保证程序的有序性就可以了;

五、volatile底层是如何基于内存屏障保证可见性和有序性的?

这一块是比较复杂的,所以咱们只要记住两点:

  • lock指令:volatile保证可见性
    对volatile修饰的变量,执行写操作的话,JVM会发送一条lock前缀指令给CPU,CPU在计算完之后会立即将这个值写回主内存,同时因为有MESI缓存一致性协议,所以各个CPU都会对总线进行嗅探,自己本地缓存中的数据是否被别人修改;
    如果发现别人修改了某个缓存的数据,那么CPU就会将自己本地缓存的数据过期掉,然后这个CPU上执行的线程在读取那个变量的时候,就会从主内存重新加载最新的数据了;
    所以主要是lock前缀指令+MESI缓存一致性协议。

  • 内存屏障:volatile禁止指令重排序
    加入了屏障之后,就能够禁止volatile和前后的代码进行指令重排。

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值