JUC之JMM(Java内存模型)

经典面试题

  • 什么是Java内存模型JMM?
  • Java内存模型是Java虚拟机(JVM)规范中定义的一种抽象的内存模型,它定义了程序中对变量的访问和操作行为, 且每个JVM都必须遵守这套规范,保证程序在各个平台的优化编译中的正确运行(尤其多线程),避免潜在的并发风险。它屏蔽了操作系统对内存的访问和硬件的差异。
  • JMM与volatile它们两个之间的关系?(volatile文章中详解)
  • JMM有那些特性或者说它的三大特性有什么?
  • JMM的作用和功能是什么?
  • 说说happens-before先行发生原则。

JMM的作用

  1. 通过JMM来实现线程和主内存之间的关系。
  2. 屏蔽各个硬件平台和操作系统对内存访问的差异,保证Java程序在各个平台可以正确的运行且达到一致的内存访问效果。

三大特性

可见性

可见性是指当其中一个线程对共享变量进行了修改,其他线程是否能够立刻感知到数据的变更。JMM中规定了所有的变量都储存到主内存中。

Java中普通的共享变量不能保证可见性,每个线程都有自己的工作空间,在运行时会将主内存中的变量拷贝一份到自己的工作内存,然后对 自己的工作内存中的数据进行读和写(线程不能直接写主内存中的变量),但写入主内存的时机是不确定的,就会导致在多线程的情况下造成脏数据。
每个线程的工作空间不可直接访问,只能通过主内存来传递信息。

JUC之JMM(Java内存模型)_第1张图片
例如:

  • 主内存中有变量 x,初始值为 0
  • 线程 A 要将 x 加 1,先将 x=0 拷贝到自己的私有内存中,然后更新 x 的值
  • 线程 A 将更新后的 x 值回刷到主内存的时间是不固定的
  • 刚好在线程 A 没有回刷 x 到主内存时,线程 B 同样从主内存中读取 x,此时为 0,和线程 A 一样的操作,最后期盼的 x=2 就会变成 x=1

原子性

指一个操作是不可被中断的,在Java多线程中 例如i++操作 在i++执行的过程中不可被其他的线程所干扰(一旦执行,不可被中断);

有序性

在一个程序执行的过程中,它并不是一定按照我们代码的书写顺序来执行的,为了提高我们代码的性能,编译器或处理器通常会进行指令重排序,指令重排序之后,它会保证我们的串行语义一致;但不会保证在多线程环境下的语义一致;
对于没有数据依赖的两行代码,cpu可能并不会按照代码的顺序去执行,执行顺会被优化;
在单线程环境中会确保最终结果和代码顺序执行的结果顺序一致,但如果在多线程的环境下两个线程交替执行,无法保证变量的一致性,可能会造成脏读;
在指令重排序过程中,必须要考虑数据依赖性,如果具有数据依赖,那么必须保证其执行顺序;

> 1 	public void main() {
> 2			int x = 10;
> 3		 	int y = 20;
> 4			int s = x * y;
> 5			int a = y;
> 6		}
  • 对于2行和第3行 不存在数据依赖关系,那么它的最终执行顺可能是 2 -> 3;
  • 但对于234行 他们之间存在数据依赖关系,那么在4行执行前 23行必须执行;

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

由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),工作内存是每个线程的私有数据区域,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝到的线程自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,各个线程中的工作内存中存储着主内存中的变量副本拷贝,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成;
JUC之JMM(Java内存模型)_第2张图片

先行发生原则之happens-before

“happens-before” 是Java内存模型(Java Memory Model,JMM)中的一个概念,用于描述多线程程序中操作顺序和可见性
在Java中,线程之间的操作可能是乱序执行的,这是由于编译器、处理器和缓存等因素的优化导致的。为了确保多线程程序的可靠性和正确性,Java内存模型定义了一组规则,其中就包括"happens-before"关系。
"happens-before"关系是一种偏序关系,它规定了两个操作之间的顺序关系。如果操作A happens-before操作B,那么A的结果对B可见,且A在B之前执行。
例如:

x = 5 操作A执行
y = x 操作B执行

y是否等于5呢?
如果A操作(x= 5)happens-before(先行发生)B的操作(y = x),那么可以确定B执行后y = 5 一定成立;
如果他们不存在happens-before原则,那么y = 5 不一定成立。

happens-before总原则

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

细分(8条)

次序规则:

一个线程内,按照代码顺序,写在前面的操作先行发生于写在后面的操作;
说明:前一个操作的结果可以被后续的操作获取。

锁定规则

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

volatile变量规则:

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

传递规则:

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

线程启动规则:

Thread对象的start()方法先行发生于此线程的每一个动作

线程中断规则:

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

线程终止规则:

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

对象终结规则:

一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始通俗说:对象没有完成初始化之前,是不能调用finalized()方法的;

案例说明

JUC之JMM(Java内存模型)_第3张图片

假设存在线程A和B,线程A先(时间上的先后)调用了setValue(1),然后线程B调用了同一个对象的getValue(),那么线程B收到的返回值是什么?
解释:
我们就这段简单的代码一次分析happens-before的规则(规则5、6、7、8 可以忽略,因为他们和这段代码毫无关系):

  1. 由于两个方法是由不同的线程调用,不在同一个线程中,所以肯定不满足程序次序规则;
  2. 两个方法都没有使用锁,所以不满足锁定规则;
  3. 变量不是用volatile修饰的,所以volatile变量规则不满足;
  4. 传递规则肯定不满足;

所以我们无法通过happens-before原则推导出线程A happens-before线程B,虽然可以确认在时间上线程A优先于线程B指定,但就是无法确认线程B获得的结果是什么,所以这段代码不是线程安全的。那么怎么修复这段代码呢?
修复:(两种方式

  1. 把getter/setter方法都定义为synchronized方法;
  2. 把value定义为volatile变量,由于setter方法对value的修改不依赖value的原值,满足volatile关键字使用场景

你可能感兴趣的:(JUC,java,JUC,JMM,内存模型,面试重点)