王有志,一个分享硬核Java技术的互金摸鱼侠
加入Java人的提桶跑路群:共同富裕的Java人
今天是《面霸的自我修养》的第二弹,内容是Java并发编程中关于Java内存模型(Java Memory Model)和锁的基础理论相关的问题。这两块内容的八股文倒是不多,但是难度较大,接下来我们就一起一探究竟吧。
数据来源:
- 大部分来自于各机构(Java之父,Java继父,某灵,某泡,某客)以及各博主整理文档;
- 小部分来自于我以及身边朋友的实际经历,题目上会做出标识,并注明面试公司。
叠“BUFF”:
- 八股文通常出现在面试的第一二轮,是“敲门砖”,但仅仅掌握八股文并不能帮助你拿下Offer;
- 由于本人水平有限,文中难免出现错误,还请大家以批评指正为主,尽量不要喷~~
- 本文及历史文章已经完成PDF文档的制作,提取关键字【面霸的自我修养】。
难易程度:🔥🔥🔥🔥🔥
重要程度:🔥🔥🔥🔥🔥
公司:阿里巴巴
Java内存模型篇
关于Java内存模型的内容可以说是八股文中最晦涩难懂的部分之一了, 《JSR-133 Java Memory Model and Thread Specification》中是这么介绍这部分内容的:
The discussion and development of this specification has been unusually detailed and technical, involving insights and advances in a number of academic topics.
不过还好,面试中通常不会过分深入的考察Java内存模型的部分。
🔥描述下Java内存模型,说说你对它的理解。
难易程度:🔥🔥🔥🔥🔥
重要程度:🔥🔥🔥🔥🔥
公司:美团,爱奇艺,阿里巴巴
Java内存模型(Java Memory Model,JMM)是Java语言规范中的一套规则,它描述了多线程环境下的线程与内存(主内存和高速缓存)的交互方式,以保证可见性,有序性和原子性,同时它屏蔽了硬件与操作系统的底层差异,使得Java程序在所有平台下的内存访问效果一致。
高速缓存带来的可见性问题
我们知道,CPU的运算速度是远高于内存读写速度的,为了减少速度间的差异,CPU为每个核心引入了高速缓存(通常分为L1,L2和L3)。多线程的程序中,线程可能会运行在不同的核心上,这时它们使用自己缓存中从主内存拷贝的数据副本,假设每个CPU只有一个高速缓存,画一个简易的模型:
如果线程T1和线程T2分别从主内存中读取同一个数据的到自己的高速缓存中进行操作,如果线程T1是先于线程T2发生的,那么此时线程T2无法感知到线程T1对缓存中数据做出的修改,导致可线程间的可见性问题。
Tips:虽然硬件层面引入了缓存一致性协议,但仍旧存在可见性问题,另外不同的CPU架构对缓存一致性协议的实现不同导致出现的问题也不相同,这部分内容大家可以自行探索。
上下文切换带来的原子性问题
Java中常常会使用count++
的方式来实现计数器的自增操作,直觉上我们认为该操作是“一气呵成”的,但实际上对应的计算机中执行了3条指令:
- 指令1:将count读入缓存;
- 指令2:执行自增操作;
- 指令3:将自增后的count写入内存。
如果运行在同一个核心上的线程T1和线程T2先后执行count++
,可能会存在一种情况:
初始状态下count为0,我们期望执行结束后线程T1的执行结果是1,线程T2的执行结果是2,但实际上恰恰相反,这就是上下文切换带来的原子性问题。
Tips:上下文切换的内容请参考《面霸的自我修养:Java线程专题》。
指令重排带来的有序性问题
指令重排是CPU一项重要的优化手段,在不改变单线程执行结果的前提下,CPU可以自行选择如何优化指令。指令重排遵循两个基本原则:
- 数据依赖原则:如果两个操作使用的数据存在依赖性,那么不能通过指令重排来优化这两个操作的执行顺序;
- as-if-serial语义:无论如何重排序,都要保证单线程场景下的语义不能被改变(或者说执行结果不变)。
我们举个Java中经典的例子,未正确同步的单例模式:
public static class Singleton {
private Singleton instance;
public Singleton getInstance() {
if (instance == null) {
synchronized(this) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
private Singleton() {
}
}
Java中通过关键字new来创建一个对象要经历3步:
- 为这个对象分配内存;
- 初始化这块内存;
- 将变量名指向这块内存。
分析数据依赖原则,操作1是要先于操作2和操作3执行的,操作2和操作3之间并没有依赖性,如果操作2和操作3交换了执行顺序,依旧满足单线程环境下的语义,因此,在实际的执行过程中,无论是1->2->3还是1->3->2都是可以接受的。
那么在这个例子中,可能出现如下情况&#