Java内存模式之JMM

102 阅读5分钟

Java内存模式之JMM

1、大厂面试题

  • 你知道什么是Java内存模型JMM吗??
  • JMM与volatile它们两个之间的关系
  • JMM有哪些特性or它的三大特性是什么?
  • 为什么要有JMM,它为什么出现?作用和功能是什么?
  • happens-before先行发生原则你有了解过吗?

2、计算机硬件存储体系

img07.png

寄存器的读写速度跟主存的读写速度是不一样的,寄存器的速度会远远快于主存的速度,因此在实际运行的过程中就会导致寄存器想要从主存中读取数据,却因为主存的速度慢而导致寄存器需要长时间进行等待。也即==速度不匹配==问题。 因此就使用了一个三级CPU缓存机制,先将数据从主存中读取到CPU缓存里,当寄存器需要时再从缓存中获取。 而这就引发了一个问题,即,CPU缓存的数据可能与主存中的数据不一致,因为CPU缓存在读取完主存的数据之后,主存上的数据可能发生了修改,而CPU缓存的读取是有时间间隔的,因此如果在这个时间间隔内,寄存器刚好去读取了CPU缓存的数据,就会导致数据不一致。

因此,JVM规范中就给出了一种Java内存模型JMM,用于屏蔽各种硬件和操作系统的内存访问的差异,以实现Java在各种平台下达到一致的内存访问效果。

img08.png

3、Java内存模型Java Memory Model

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

原则:围绕多线程的原子性可见性有序性展开。

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

4、JMM规范下的三大特性

4.1 可见性

当一个线程修改了某个共享变量的值时,其他线程能够知道这个共享变量被修改了,即能获取到当前共享变量的最新值。

4.2 原子性

在多线程的环境下,操作不能被其他线程进行干扰。

4.3 有序性

为了提升性能,编译器和处理器会对指令进行重新排序,而重新排序的结果与原来的结果是一致的。

处理在进行重排序时要考虑指令之间的数据依赖性img09.png

5、JMM规范下,多线程对变量的读写过程

img10.png

6、JMM规范下,多线程先行发生原则之happens-before

6.1 happens-before

happens-before规定了哪些写操作对其他线程的读操作可见,是可见性与有序性的一套规则总结。

  • 线程对volatile变量的写,对其他线程对该变量的读可见
volatile static int x;
new Thread(()->{
	x = 10;
},"t1").start();

new Thread(()->{
	System.out.println(x);
},"t2").start();

假设t1先执行t2后执行,那么,t1在对x变量进行写操作之后,t2去执行时可以读到x的值,也就是t2输出的是10而不是x的默认值0。

  • 线程解锁m之前对变量的写,在解锁之后其他线程对m重新加锁时对该变量的读可见
static int x;  
static Object m = new Object(); 
new Thread(()->{  
    synchronized(m) {  
        x = 10;  
    }  
},"t1").start();  

new Thread(()->{  
    synchronized(m) {  
        System.out.println(x);  
    }  
},"t2").start();

假设t1先执行,t2后执行,那么t1在对m加锁之后对x进行写操作,当t1释放m之后,t2再对m进行加锁,那么对x的读是可见的,也就是t2会输出10而不是0。

  • 线程start前对变量的写,当该线程开始后对该变量的读可见
static int x;
x = 10;
new Thread(()->{
	System.out.println(x);
},"t2").start();

t2线程start之前,先执行了x=10,那么,当t2线程start后,t2线程能读到x写之后的值,也就是输出10。

  • 线程结束前对变量的写,对其他线程得知它结束后的读可见
static int x;

Thread t1 = new Thread(()->{
	x = 10;
},"t1");
t1.start();

t1.join();
System.out.println(x);

t1调用start执行,对x进行写操作之后,调用join结束,那么主线程知道了t1线程结束之后,能获取到对x进行写操作之后的值,也就是输出10。

  • 线程t1打断t2前对变量的写,对于其他线程知道t2被打断之后对变量的读可见
static int x;  
public static void main(String[] args) {  

    Thread t2 = new Thread(() -> {  
        while (true) {  
            if (Thread.currentThread().isInterrupted()) {  
                System.out.println(x);  
                break;           
            }  
        }    
    }, "t2");  
    t2.start();  
    
    new Thread(() -> {  
        try {  
            Thread.sleep(1000);  
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        }  
        x = 10;  
        t2.interrupt();  
    }, "t1").start();  
    
    while (!t2.isInterrupted()) {  
        Thread.yield();  
    }  
    System.out.println(x);  
}

t1先执行,执行到x=10之后,切换回t2执行,那么此时能读到的值为10,接着t1调用t2.interrupt()打断t2之后,t2被打断之后,main线程知道t2被打断,也就是在执行while (!t2.isInterrupted())判断条件为false时,输出的x值为10。

  • 对变量默认值的写,其他线程对该变量的读可见
  • 具有传递性,如果x hb-> y,y hb-> z,那么x hb->z

6.2 happens-before的8条规则

6.2.1 次序规则

前面代码执行的结果可以被后面代码所获取到。

6.2.2 锁定规则

在锁lock()之前,要保证已经对该锁进行unlock()

6.2.3 volatile变量规则

前面的写对后面的读是可见的。

6.2.4 传递规则

如果x hb-> y,y hb-> z,那么x hb->z

6.2.5 线程启动规则

start()方法的调用比线程内部代码都要先执行。

6.2.6 线程中断规则

先调用interrupt()之后,才能根据中断标志位去判断要进行中断

6.2.7 线程终止规则

线程的操作要比isAlive()判断是否终止的操作还要先。

6.2.8 对象终结规则

在完成对象的初始化操作之后,才能执行finalize()