Java面试题:JMM与锁的理论

news2024/11/20 16:34:09

封面:JMM与锁的理论.png

王有志,一个分享硬核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只有一个高速缓存,画一个简易的模型:
图1:缓存带来的可见性问题.png
如果线程T1和线程T2分别从主内存中读取同一个数据的到自己的高速缓存中进行操作,如果线程T1是先于线程T2发生的,那么此时线程T2无法感知到线程T1对缓存中数据做出的修改,导致可线程间的可见性问题。
Tips:虽然硬件层面引入了缓存一致性协议,但仍旧存在可见性问题,另外不同的CPU架构对缓存一致性协议的实现不同导致出现的问题也不相同,这部分内容大家可以自行探索。

上下文切换带来的原子性问题

Java中常常会使用count++的方式来实现计数器的自增操作,直觉上我们认为该操作是“一气呵成”的,但实际上对应的计算机中执行了3条指令:

  • 指令1:将count读入缓存;
  • 指令2:执行自增操作;
  • 指令3:将自增后的count写入内存。

如果运行在同一个核心上的线程T1和线程T2先后执行count++,可能会存在一种情况:
图2:上下文切换带来的原子性问题.png
初始状态下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. 将变量名指向这块内存。

分析数据依赖原则,操作1是要先于操作2和操作3执行的,操作2和操作3之间并没有依赖性,如果操作2和操作3交换了执行顺序,依旧满足单线程环境下的语义,因此,在实际的执行过程中,无论是1->2->3还是1->3->2都是可以接受的。
那么在这个例子中,可能出现如下情况:
图3:指令重排带来的有序性问题.png
这种情况下线程T2拿到的是未经过初始化的instance对象。

JMM的理解

JMM提供了一系列Java内存交互规范,用于规范不同平台下多线程环境中并发访问共享内存的方式,以保证可见性,原子性和有序性,确保程序的可预测性和可靠性。JMM中最重要的两个部分是as-if-serial语义和Happens-Before原则。
Tips:实际上,JMM的出现是早于JSR-133的,在早期的Java语言规范中描述的JMM模型存在一定的缺陷,因此在JSR-133中对早期的JMM模型做了改进,并作为JSR-176(Java 5.0)的一部分。


Happens-Before原则

难易程度:🔥🔥🔥🔥🔥

重要程度:🔥🔥🔥

公司:美团,爱奇艺,阿里巴巴

Happens-Before原则是JMM中的一部分,用于描述多线程环境下操作之间执行结果的顺序,例如:操作A happens-before操作B(记作 A h b → B A\underrightarrow{hb}B A hbB),表示无论在何种情况下,操作A的结果对操作B来说都是可见的。
JSR-133中定义了Happens-Before原则的6项内容:

  • 程序顺序规则:线程中的每个操作happens-before该线程中的任意后续操作。
  • 监视器锁规则:锁的解锁happens-before随后这个锁的加锁。
  • volatile变量规则:volatile变量的写happens-before后续任意对这个volatile变量的读。
  • 传递性:如果 A h b → B A\underrightarrow{hb}B A hbB,且 B h b → C B\underrightarrow{hb}C B hbC,那么 A h b → C A\underrightarrow{hb}C A hbC
  • Thread#start规则:如果线程t1执行操作启动线程t2(执行t2.start),那么t1线程的t2.start操作happens-before于线程t2中的任意操作。
  • Thread#join规则:如果线程t1执行操作t2.join并成功返回,那么线程B中的任意操作happens-before于线程A从t2.join操作成功返回。

Happens-Before原则提供了线程间的可见性保证,描述了线程执行结果间的先后顺序,但Happens-Before原则并不限制指令执行的顺序,即Happens-Before原则并不禁止重排序,只是要求重排序后的结果满足Happens-Before原则的要求

锁的理论篇

这部分我们来讨论在设计锁的过程中的理论基础,了解各种各样的锁的特性。
Tips:《一文看懂并发编程中的锁》中涵盖了大部分Java中锁的理论知识。

什么是读(共享)锁?什么是写(独占/互斥/排他)锁?

难易程度:🔥🔥🔥

重要程度:🔥🔥🔥

公司:无

读写锁之间是对访问共享资源“态度”上的差异。

读锁(Read Lock)

读锁(Read Lock),共享锁(Shared Lock),S锁,指的是允许多个线程同时读取共享资源的并发控制机制,读锁在读操作之间是共享的,一旦涉及到写操作就会发生互斥
图4:共享锁.png

写锁(Write Lock)

写锁(Write Lock),互斥锁(Mutex Lock),排他锁(Mutex Lock),X锁,指的是无论读写同一时间只允许一个线程访问共享资源的并发控制机制
Tips:在并发编程中,你会常常看到临界区这个词,临界区指的是访问共享资源的代码区域,如果不能正确的控制这段代码区域的并发访问,可能会导致很多违背直觉的并发问题。


🔥什么是乐观锁?什么是悲观锁?

难易程度:🔥🔥🔥

重要程度:🔥🔥🔥🔥🔥

公司:蚂蚁金服,联储证券,质数金融

悲观锁(Pessimistic Locking)

悲观锁是一种用于多线程环境中保护共享资源一致性的并发控制机制。悲观锁总是假设共享资源会被修改,因此在访问(包含读和写两种)共享资源前,先获取锁来保护共享资源,防止其它线程访问共享资源,避免了并发导致的问题。
图5:悲观锁.png
悲观锁的工作原理如下:

  • 线程尝试获取保护共享资源的锁;
    • 获取锁成功,允许线程访问共享资源;
    • 获取锁失败,线程阻塞,等待锁的释放;
  • 获取到锁的线程操作完毕后释放锁。
乐观锁(Optimistic Locking)

同样的,乐观锁也是一种用于多线程环境中保护共享资源一致性的并发控制机制。与悲观锁不同,乐观锁认为共享资源不会被修改,所以在读取时并不会对资源进行上锁,只有在更新资源时,才会对资源进行冲突检测。
图6:乐观锁的读.png
乐观锁的工作原理如下:

  • 允许多线程同时读取共享资源;
  • 写入共享资源时通过自身持有资源的标识(版本号,时间戳等)与共享资源的标识进行对比;
    • 标识未发生改变,允许线程写入资源;
    • 标识发生改变,禁止线程陷入资源;
  • 写入资源失败的的线程,允许进行重试或直接抛出异常。

Tips:这里我们略过了通过共享资源的值比较环节,直接使用了版本号或时间戳。


🔥乐观锁一定优于悲观锁吗?

难易程度:🔥🔥🔥

重要程度:🔥🔥🔥🔥🔥

公司:蚂蚁金服,联储证券,质数金融

乐观锁与悲观锁适用的场景并不相同,无法绝对的说孰优孰劣。乐观锁更适用于读多写少的场景,而悲观锁更适用于写多读少的场景

乐观锁

乐观锁的特点是假设冲突不会发生,读取共享资源时不会加锁,允许对多线程同时读取共享资源,只在更新数据时检测冲突。
优点:

  • 允许多线程同时访问共享资源,不会加锁,性能较好
  • 不会引起死锁

缺点:

  • 写多读少的场景中,频繁的重试会带来额外的性能开销
悲观锁

悲观锁的特点是假设冲突总会发生,无论读写操作,都会加锁,同一时间只允许一个线程访问共享资源。
优点:

  • 只允许一个线程访问共享资源,避免了乐观锁在多线程写入时频繁重试带来的性能消耗

缺点:

  • 无论读写都会加锁,实际上相当于单线程访问共享资源,会成为高并发系统的性能瓶颈
  • 如果业务中存在多个锁,可能会导致死锁的发生

什么是CAS?

难易程度:🔥🔥🔥

重要程度:🔥🔥🔥🔥🔥

公司:无

CAS(Compare And Swap),即比较替换。CAS操作3个数:

  • 内存值V,即内存中共享资源的值;
  • 旧的预期值A,即线程从内存中读取到且未进行修改共享资源的值;
  • 要修改的值B,即线程进行修改后的共享资源的值。

只有当V == A时,才会将V的值更新为B,否则什么都不做。
其原理是,如果内存中值与线程取出的值相同时,认为在这个期间并没有线程修改共享资源,因此可以对共享数据进行修改。但如果某个线程先将共享资源从V修改到W,再将共享资源从W修改会回V,此时就导致了ABA问题。


🔥乐观锁如何解决ABA问题?

难易程度:🔥🔥🔥

重要程度:🔥🔥🔥🔥🔥

公司:蚂蚁金服

ABA问题是并发编程中的经典问题,通常在使用CAS时会产生ABA问题:

  • 假设存在共享资源A;
  • 线程T1读取A;
  • 线程T2读取A后,将其修改为B,并写入内存;
  • 线程T3读取B后,将其修改为A,并写入内存;
  • 线程T1将A修改为C,并通过CAS将C写入内存。

图7:ABA问题.png
看起来线程T1的修改路径是从A->C,实际上是从A->B->A->C。
图8:资源修改过程.png
为了解决ABA的问题,可以通过为共享资源添加版本号(或时间戳),写入操作时比较线程获取到共享资源的版本号与内存中共享资源的版本号是否一致,如果一致则允许更新共享资源,同时需要更新共享资源的版本号,否则不允许更新。


🔥什么是公平锁?什么是非公平锁?

难易程度:🔥🔥🔥

重要程度:🔥🔥🔥🔥🔥

公司:美团

公平锁与非公平锁的区别在于获取锁的顺序上。公平锁按照线程申请锁的顺序,依次排队获取锁;非公平锁则不考虑申请顺序,当锁处于空闲状态时,线程可以直接“抢夺”锁的使用权
公平锁按照申请顺序获取锁的使用权,正常情况下每个线程都能在可预期的时间内获取到锁;非公平锁可能会出现虽然线程申请锁的时间非常早,但始终无法抢占到锁,导致线程饥饿
非公平锁的优点在于无需维护等待队列,在加锁和解锁的速度上优于公平锁
Tips:在简单的非公平锁实现中,抢占锁失败的线程调用Object#wait进入阻塞状态;释放锁后,调用Object#notifyAll唤醒所有线程继续抢夺,无需引入等待队列。


🔥什么是可重入锁?

难易程度:🔥🔥🔥

重要程度:🔥🔥🔥🔥🔥

公司:蚂蚁金服,美团

可重入锁,POSIX标准中称为递归锁,指的是允许统一个线程多次获取同一个锁。可重入锁可以解决递归调用或嵌套调用中的死锁问题,例如:递归的方式删除指定路径下的所有文件:

private static void deleteFile(File directory) {
	synchronized (LOCK) {
		File[] files = directory.listFiles();
		for (File subFile : files) {
			if(subFile.isDirectory()) {
				deleteFile(subFile);
			} else {
				subFile.delete();
			}
		}
	}
}

如果synchronized不具备可重入性,那么在遇到第一个子文件夹时程序会被阻塞,导致程序无法继续进行。
可重入锁的设计中,通常会在内部维护一个计数器,每次进入可重入锁时计数器加1,退出时计数器减1,进入和退出的次数需要匹配。


Java中提供了哪些锁?

难易程度:🔥🔥🔥

重要程度:🔥🔥🔥

公司:无

Java中有3个常见的锁,分别是synchronized,ReentrantLock,ReentrantReadWriteLock,以及一个“小众的的StampedLock。

特点synchronizedReentrantLockReentrantReadWriteLockStampedLock
公平锁非公平锁公平模式/非公平模式公平模式/非公平模式非公平锁
可重入性可重入可重入可重入可重入
乐观锁悲观锁悲观锁悲观锁乐观锁
互斥锁ReadLock 共享/WriteLock 互斥ReadLock 共享/WriteLock 互斥

🔥如果让你实现锁,你有什么思路?

难易程度:🔥🔥🔥

重要程度:🔥🔥🔥🔥🔥

公司:蚂蚁金服,美团

首先考虑使用场景,读多写少可以选择读写锁(读共享,写互斥),写多读少可以直接选择互斥锁;其次添加特性,如果需要支持递归加锁或嵌套加锁就需要添加可重入性,选择公平性或非公平性,最后还可以考虑加锁失败时的设计,如果锁定的临界区非常“小”,锁会在极短时间内释放,可以考虑使用自旋,否则可以选择阻塞。
抛砖引玉,我这里使用AQS完成了一个非公平,不可重入的互斥锁:

public class MutexLock {

	public void lock() {
		sync.acquire(1);
	}

	public void unlock() {
		sync.release(0);
	}

	private final Sync sync = new Sync();

	static class Sync extends AbstractQueuedSynchronizer {
		@Override
		protected boolean tryAcquire(int arg) {
			Thread currentThread = Thread.currentThread();
			if (compareAndSetState(0, arg)) {
				setExclusiveOwnerThread(currentThread);
				return true;
			} else {
				return false;
			}
		}

		@Override
		protected boolean tryRelease(int arg) {
			if (getState() != 1) {
				return false;
			}
			setState(arg);
			setExclusiveOwnerThread(null);
			return true;
		}
	}
}

Tips:AQS的问题我们后面再讨论。

参考

  • JSR-133: Java Memory Model and Thread Specification
  • JSR 133 (Java Memory Model) FAQ
  • 深入理解Java内存模型
  • 深入理解JMM和Happens-Before
  • 一文看懂并发编程中的锁

如果本文对你有帮助的话,还请多多点赞支持。如果文章中出现任何错误,还请批评指正。最后欢迎大家关注分享硬核Java技术的金融摸鱼侠[王有志](https://www.yuque.com/wangyouzhi-u3woi/dfhnl0/hqrch62un0cc9sp2,我们下次再见!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1416941.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

网工必备工具:不懂它,何谈高手之位?

点开之前,你脑子里闪出来的工具是什么?ping?又或是arp、tracert、route……? 今天要给你分享的是非常经典的Linux网络抓包工具Tcpdump。 它允许用户拦截和显示发送或收到过网络连接到该计算机的TCP/IP和其他数据包。 Tcpdump 适用…

【语录】岁月

中年 写中年,应该是年少励志三千里 踌躇百步无寸功,转眼高堂已白发 儿女蹒跚学堂中,不如意事常八九,可与人言无二三 可是诸位,不用悲伤,稻盛和夫说, 人生并不是一场物质的盛宴,而是…

12.Golang中类的表示与封装

目录 概述类的表示代码结果 类的封装代码结果 结束 概述 Golang中类的表示与封装 类的表示 代码 注释掉的代码,并不能拿来当赋值或获取值来使用。 package mainimport "fmt"// 类大写则代表,可以被其它包使用 type Hero struct {// 属性方法大…

30岁以就业为目标学前端,快歇着吧;反之50岁都不晚。

Hi,我是贝格前端工场,首先声明声明我们不搞前端培训,有很多老铁在留言中问我关于前端学习的问题,最普遍的一个问题就是30岁以后学前端晚了吗?今天借着此篇文章回答一下。 一、30岁学前端的三种人 首先抛开年龄不说&am…

【计算数组连续值的移动距离】及【计算遥控器按键总次数】

计算数组连续值的移动距离及计算遥控器按键总次数 计算数组连续值的移动距离计算遥控器按键总次数 计算数组连续值的移动距离 /*** 计算数组连续值的移动距离* 给定一个乱序的整数数组,数组中值为1~n,请计算出所有数字移动到比其大1的数字位置的距离和。…

【vue2】路由之 Vue Router

文章目录 一、安装二、基础使用1、简单的示例2、动态路由2.1 定义动态路径参数2.2 获取动态路径的参数2.3 捕获所有路由 3、嵌套路由4、编程式的导航4.1 router.push4.2 router.replace4.3 router.go(n) 5、命名路由6、重定向 三、进阶1、导航守卫1.1 全局前置守卫1.2 全局后置…

1Panel CloudFlare证书申请失败的解决方案

在升级1Panel后,使用 CloudFlare DNS验证时,会提示 [*.biliwind.com] [*.biliwind.com] acme: error presenting token: cloudflare: failed to find zone biliwind.com.: ListZonesContext command failed: Invalid request headers (6003) 为解决此问…

如何在Arxiv上预发表自己的手稿

1. 使用latex编辑好自己的手稿。可以使用latex软件或者overleaf。尽量避免警告,否则会在上传到arxiv时出现意外的错误。 2. https://arxiv.org/登陆并注册arxiv账号 3. 点击 开始新的提交(START NEW SUBMISSION) 4. 主要的问题是参考文献的导…

C++:auto 关键字 范围for

目录 auto 关键字: 起源: auto的使用细则: auto不能推导的场景: 范围for: 范围for的使用条件: C的空指针: 注意: auto 关键字: 起源: 随着程序越…

全角色服务、全场景支撑、全业务应用的新一代智慧教室

新一代智慧教室以“数智化助力高质量人才培养”为核心目标,以AI赋能的智能硬件为基础构建多形态智慧教学环境,以中台为支撑实现数据、设备、系统、业务的互联互通、开放共享,以平台全面覆盖教学应用,采集、汇聚、挖掘、分析课前课…

【周赛】第382场周赛

🔥博客主页: A_SHOWY🎥系列专栏:力扣刷题总结录 数据结构 云计算 数字图像处理 力扣每日一题_ 从这一场(第382场周赛)周赛开始记录,目标是尽快达到准确快速AC前三道题,每场比赛…

【服务器GPT+MJ+GPTs】创建部署GPT+MJ+GPTs程序网站

目录 🌺【前言】 🌺【准备】 🌺【宝塔搭建GPT+MJ+GPTs】 🌼1. 给服务器添加端口 🌼2. 安装宝塔 🌼3. 安装Docker 🌼4. 安装ChatGPT程序 🌼5. 程序更新 🌼6. 修改端口 | 密码 🌼7. 绑定域名+申请SSL证书 🌺【前言】 相信大家都对openai的产品ch…

【2024】Docker部署Redis

1.说明: 因为容器实例的运行是有生命周期的,一些redis的备份、日志和配置文件什么的最好还是放在服务器本地。这样当容器删除时,我们也可以保留备份和日志文件。所以先在本地服务器安装redis并配置文件设置。下面是安装步骤: 2.安装步骤 1…

Unity 命令模式(实例详解)

文章目录 示例1:基础命令类结构示例2:旋转对象命令示例3:增加道具命令示例4:切换场景命令示例5:播放音效命令 在Unity中使用命令模式(Command Pattern)是一种常见的设计模式,用于实现…

LeetCode344反转字符串(java实现)

今天我们来分享的题目是leetcode344反转字符串。题目描述如下: 我们观察题目发现,题目要求使用O(1)的空间解决这一问题。那么我们就不能进行使用开辟新的数组进行反转了。 解题思路:那么该题的我得思路是使用双指针的方法进行题解&#xff0…

深度强化学习(王树森)笔记01

深度强化学习(DRL) 本文是学习笔记,如有侵权,请联系删除。本文在ChatGPT辅助下完成。 参考链接 Deep Reinforcement Learning官方链接:https://github.com/wangshusen/DRL 源代码链接:https://github.c…

80.网游逆向分析与插件开发-背包的获取-自动化助手显示物品数据1

内容参考于:易道云信息技术研究院VIP课 上一个内容:升级Notice类获得背包基址-CSDN博客 码云地址(ui显示角色数据 分支):https://gitee.com/dye_your_fingers/sro_-ex.git 码云版本号:3be017de38c50653b…

【算法专题】二分查找(进阶)

📑前言 本文主要是二分查找(进阶)的文章,如果有什么需要改进的地方还请大佬指出⛺️ 🎬作者简介:大家好,我是青衿🥇 ☁️博客首页:CSDN主页放风讲故事 🌄每日…

【代码随想录】LC 242. 有效的字母异位词

文章目录 前言一、题目1、原题链接2、题目描述 二、解题报告1、思路分析2、时间复杂度3、代码详解 前言 本专栏文章为《代码随想录》书籍的刷题题解以及读书笔记,如有侵权,立即删除。 一、题目 1、原题链接 242. 有效的字母异位词 2、题目描述 二、解题…

【c++】类和对象 - 类的引入和定义

1.类的引入 C语言结构体中只能定义变量,在C中,结构体内不仅可以定义变量,也可以定义函数。比如:之前在数据结构初阶中,用C语言方式实现的栈,结构体中只能定义变量;现在以C方式实现,…