系统相关
首页 > 系统相关> > JVM进阶——资深面试官必问的Java内存模型,顺利收获Offer

JVM进阶——资深面试官必问的Java内存模型,顺利收获Offer

作者:互联网

硬件的效率与一致性


物理计算机中的并发问题和Java虚拟机有很多相似之处。

为了解决处理器与内存之间的速度矛盾,引入了高速缓存

高速缓存的引入带来了问题:缓存一致性。多路处理器系统中,每个处理器有各自的高速缓存,而他们又共享同一主内存。当多个处理器的运算任务额都涉及同一块主存区域的时候,将可能导致各自的缓存数据不一致。

为了解决一致性的问题,需要各个处理器在访问缓存时都遵循一些协议,在读写时要根据协议来进行操作。我们将会多次提到“内存模型”一词,可以理解为在特定的操作协议下,对特定的内存或高速缓存进行读写访问时的过程抽象。不同的物理机器可以拥有不同的内存模型。而Java虚拟机也拥有自己的内存模型

除了增加高速缓存,处理器会对代码进行乱序执行优化。对应JVM中的指令重排序

Java内存模型


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

此前(如:C语言)直接使用物理硬件和操作系统的内存模型,导致一套程序在不同的平台上出现不同的错误。

主内存与工作内存

Java内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。

这里的变量不包括局部变量和参数,因为其实线程私有的,不会被共享,自然不会存在竞争问题。

Java内存模型规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存(可与高速缓存类比),线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取赋值)都必须在工作内存中进行,而不能直接读写主内存中的变量(包括volatile变量也是这样)。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。

在这里插入图片描述

一起吹水聊天

内存间交互操作

关于主内存和工作内存之间具体的交互协议:即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步回主内存之类的实现细节。Java内存模型中定义了以下8种操作来完成,虚拟机实现时必须保证下面提及的每一种操作都是原子的。

注意:Java内存模型只要求这两个操作(read、load 操作,store、write 操作)必须按顺序执行,而没有保证是连续执行。

也就是说readwrite之间是可以插入其他指令的,如

read a、read b、load b、load a

原子性、可见性与有序性


Java内存模型是围绕着在并发过程中如何处理原子性、可见性和有序性这三个特征来建立的

原子性

一个操作要么都发生,要么都不发生

可见性

一个线程修改了值,其他线程能够立刻知道

有序性

在本线程中观察,所有操作都是有序的(指本线程内表现为串行的语义)

在另一个线程中观察,所有操作都是无序的(指“指令重排序”现象和“工作内存和主内存同步延迟”现象)

先行发生原则


Java内存模型中有一个先行发生原则,它是判断数据是否存在竞争,线程是否安全的重要手段。

我们

《一线大厂Java面试题解析+后端开发学习笔记+最新架构讲解视频+实战项目源码讲义》

【docs.qq.com/doc/DSmxTbFJ1cmN1R2dB】 完整内容开源分享

举个例子来说明说明什么是先行发生原则

// A 线程

i = 1;

// B 线程

j = i;

// C 线程

j = 2;

如果 A 先行发生于 B,且 C 没有登场,那么,j 的值一定是 1。如果 C 登场了,仍旧只是 A 先行发生于 B,那么 j 可能是 2,也可能是 1

Java 内存模型中有一些天然的先行发生原则,其中介绍下面两条;

volatile变量规则

一起吹水聊天

对于volatile变量的读和写而言,如果在实际执行时间上有写在读前的话

(如线程A的assign在先于线程B的use执行,前面说了assign、use是原子操作)

,那么就有写在读前的先行发生关系,这样就保证了一切对于volatile变量写操作可见的变量(即happens before volatile写操作的其他变量操作所造成的一切影响),对于后面的volatile变量读操作也是可见的。如果换成可普通变量,即使是有时间上的写在读前,但如不是同一线程就没有happens关系,这样就不能保证可见性。

private int value = 0;

public void setValue(int value) {

this.value = value;

}

public int getValue() {

return value;

}

线程安全


Java中的线程安全排序:

this逃逸

在构造器构造还未彻底完成前(即实例初始化阶段还未完成),将自身this引用向外抛出并被其他线程复制(访问)了该引用。

class ThisEscape {

int i;

static ThisEscape obj;

public ThisEscape() { // 由于指令重排序,所以不能确定这两部谁先进行

i = 1;

obj = this;

}

}

// 如果线程A还没来得及为i赋值,线程B就使用了这个obj.i;会导致空指针。或者其他情况下会导致对象不完整。

什么情况下会This逃逸?

(1)如上述的明显将this抛出

(2)在构造器中内部类使用外部类情况:内部类访问外部类是没有任何条件的,也不要任何代价,也就造成了当外部类还未初始化完成的时候,内部类就尝试获取为初始化完成的变量

不可变

一起吹水聊天

绝对线程安全

再Java API中标注自己是线程安全的类,大多数都不是绝对的线程安全。

如:Vector类,它的add()、get()等方法都是由synchronized修饰的,但不意味着调用它的时候不需要原子操作(指要么都做要么都不做)了。

public void method(Vector vec) {

synchronized(vec) {

vec.set(…);

vec.get(…);

}

}

相对线程安全

指我们通常意义上的线程安全,它需要保证这个对象单次的操作是线程安全的。如:Vector类。

线程兼容

指我们通常所说的线程不安全。指对象本身并不安全,但可以通过调用端正确地使用同步手段来保证对象在并发环境中可以安全地使用。

线程对立

指不管怎样操作都无法在多线程环境中并发使用。由于Java本身就具备支持多线程地特性,线程对立地情况很少出现。

线程安全的实现方法


互斥同步

同步是指在多线程并发访问共享数据时,保证共享数据同一时刻只被一条线程使用(或者是一些,当使用信号量时)。

互斥是同步的一种手段:临界区、互斥量和信号量都是常见的互斥实现方法。

互斥同步这四个字里:互斥是因,同步是果。互斥是方法,同步是目的。

我们拿Reentrantsynchronized多了一些高级功能:

一起吹水聊天

非阻塞同步

互斥同步面临的问题是线程阻塞和唤醒带来的性能开销,这称为阻塞同步,是一种悲观的发展策略,因为不管是否出现竞争都进行加锁。

非阻塞同步:是一种乐观发展策略,就是不管风险先进行操作(如CAS算法),但是它需要硬件指令集的发展(使多个步骤的操作具备原子性),使某些看起来的多步操作只要一步就可以完成。不过它无法解决ABA问题,如果要解决ABA问题,改用传统的互斥同步可能会更快。

无同步方案

同步和线程安全没有必然的联系。如果一个方法本来就不涉及共享数据,那自然就不需要任何同步措施就能保证其正确性。

锁优化


从 JDK5 到 JDK6 虚拟机开发团队实现的各种锁优化技术

自旋锁与自适应自旋

互斥同步对性能最大的影响是阻塞的实现,需要用户态与内核态切换(需要消耗的不止CPU资源)。

如果机器上有多个处理器或者处理器核心,能让两个线程并行执行,我们就会请求其中一个线程等一会儿,但不放弃处理器的执行时间,这里的等一会就是忙循环(自旋)。默认忙循环10次,也可以自行设置。

如果,循环的时间不再是固定的,而是它自己决定的,那就是自适应自旋。

锁消除

对被检测到不可能存在共享数据竞争的锁进行消除。你可能觉得自己没有加锁呀,可是编译器会加锁。如:

标签:面试官,必问,Java,变量,线程,内存,操作,volatile
来源: https://blog.csdn.net/m0_64867688/article/details/122024041