jvm与锁

news2024/11/16 0:00:32

今天是《面霸的自我修养》的第二弹,内容是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. 将变量名指向这块内存。

分析数据依赖原则,操作1是要先于操作2和操作3执行的,操作2和操作3之间并没有依赖性,如果操作2和操作3交换了执行顺序,依旧满足单线程环境下的语义,因此,在实际的执行过程中,无论是1->2->3还是1->3->2都是可以接受的。

那么在这个例子中,可能出现如下情况:

这种情况下线程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的结果对操作B来说都是可见的。

JSR-133中定义了Happens-Before原则的6项内容:

  • 程序顺序规则:线程中的每个操作happens-before该线程中的任意后续操作。
  • 监视器锁规则:锁的解锁happens-before随后这个锁的加锁。
  • volatile变量规则:volatile变量的写happens-before后续任意对这个volatile变量的读。
  • 传递性:如果

    ,且,那么。
  • 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锁,指的是允许多个线程同时读取共享资源的并发控制机制,读锁在读操作之间是共享的,一旦涉及到写操作就会发生互斥

写锁(Write Lock)

写锁(Write Lock),互斥锁(Mutex Lock),排他锁(Mutex Lock),X锁,指的是无论读写同一时间只允许一个线程访问共享资源的并发控制机制

Tips:在并发编程中,你会常常看到临界区这个词,临界区指的是访问共享资源的代码区域,如果不能正确的控制这段代码区域的并发访问,可能会导致很多违背直觉的并发问题。


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

难易程度:🔥🔥🔥

重要程度:🔥🔥🔥🔥🔥

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

悲观锁(Pessimistic Locking)

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

悲观锁的工作原理如下:

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

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

乐观锁的工作原理如下:

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

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写入内存。

看起来线程T1的修改路径是从A->C,实际上是从A->B->A->C。

为了解决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。

特点

synchronized

ReentrantLock

ReentrantReadWriteLock

StampedLock

公平锁

非公平锁

公平模式/非公平模式

公平模式/非公平模式

非公平锁

可重入性

可重入

可重入

可重入

可重入

乐观锁

悲观锁

悲观锁

悲观锁

乐观锁

互斥锁

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技术的金融摸鱼侠王有志,我们下次再见!

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

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

相关文章

CAC2.0准实时威胁检测,“无人化”防暴力破解

客户背景 上海微盟企业发展有限公司(以下简称“微盟”),香港主板上市企业,成立于2013年,致力于为商家打造去中心化的数字化转型SaaS产品及全链路增长服务,助力商家经营可持续增长。 在这个快节奏的网络时…

《发电厂电气部分》进出线停送电倒闸操作理解

在《发电厂电气部分》(部分学校也叫《供配电技术》)中,停电/送电时的倒闸操作在笔者看来是比较难理解的一件事,即使是在bilibili上看了实际架空线路上的倒闸操作,还是感觉云里雾里。这里分享一下自己的理解。 这里以最…

对于前端模块化的理解与总结(很全乎)

目录 模块化的好处 模块化的commonJS导入导出 暴露(导出)模块:module.exports value或exports.xxx value 导入模块——使用 es6模块化 方法一逐个导出 方法二默认导出 方法三 方法四 方法五 export 和import 同时存在 多个文件导出到一个文件后在相关文件…

解决 quill Cannot import modules/imageResize. Are you sure it was registered?

这个插件是在富文本编辑器中调整图片大小的,发现拉下来的依赖会报错,于是替换了nodejs版本,没有解决,但是用同时之前拉下来的,莫名其妙正常,后来经过尝试,发现删除demo和node_modules文件夹后正常. 删除后,不报错,正常了

python遍历文件夹下的所有子文件夹,并将指定的文件复制到指定目录

python遍历文件夹下的所有子文件夹,并将指定的文件复制到指定目录 需求复制单个文件夹遍历所有子文件夹中的文件,并复制代码封装 需求 在1文件夹中有1,2两个文件夹 将这两个文件夹中的文件复制到 after_copy中 复制单个文件夹 # coding: ut…

电源管理(PMIC)TPS63070RNMR、TPS650942A0RSKR、LM5175RHFR器件介绍、应用及特点。

一、TPS63070RNMR,降压升压 开关稳压器 IC 正 可调式 2.5V 1 输出 3.6A(开关) 15-PowerVFQFN 1、概述 TPS63070高输入电压降压-升压转换器是一款高效的低静态电流降压-升压转换器。这些器件适用于输入电压高于或低于输出电压的应用。升压模式…

Java-day12(泛型)

泛型 解决元素存储的安全性问题 解决获取数据元素时,需要类型强转的问题 核心思想:把一个集合中的内容限制为一个特定的数据类型 静态方法中不能使用类的泛型 不能在catch中使用泛型 如果泛型类是一个接口或抽象类,则不可创建泛型类的对…

Docker技术--Docker镜像管理

1.Docker镜像特性 ①.镜像创建容器的特点 Docker在创建容器的时候需要指定镜像,每一个镜像都有唯一的标识:image_id,也可也使用镜像名称和版本号做唯一的标识,如果不指定版本号,那么默认使用的是最新的版本标签(laster)。 ②.镜像分层机制 Docker镜像是分层构建的,并通过…

Springboot集成Docker并将镜像推送linux服务器

案例使用springboot项目,在IDEA 中集成Docker生成镜像,并将镜像发布到linux服务器 具体步骤如下: 1、Centos7安装Docker 更新系统的软件包列表 sudo yum update安装Docker所需的软件包和依赖项: sudo yum install docker完成…

vue3中右侧26个英文字母排列,点击字母,平滑到响应内容

效果图如下&#xff1a; 右侧悬浮 <!-- 右侧悬浮组件 --><div class"right-sort"><div v-for"(item, index) in list" :key"index" class"sort-item" :class"index activeIndex ? sort-item-active : " c…

大众汽车ceo施文韬到访中国,与中国团队会面,加速推出纯电产品

根据大众汽车官方公众号8月30日的消息透露&#xff0c;大众汽车乘用车品牌首席执行官施文韬和董事会成员上周到访了中国大众汽车总部V-SPACE。这次访问不仅与大众汽车中国团队和合资公司伙伴会面&#xff0c;还会见了经销商&#xff0c;并与中国核心媒体进行了沟通见面会。 中国…

固定式无线农业气象综合监测站

固定式无线农业气象综合监测站采用无线传输的方式&#xff0c;适合长距离之间的数据传输&#xff0c;用户可以在手机或者电脑登录并查看固定式无线农业气象综合监测站监测到的气象数据&#xff0c;稳定可靠。 技术特点&#xff1a; ①具有一路 ModBus-RTU 主站接口可接入485变…

运营商云强势崛起,互联网云开始艰难“守擂台”

大数据产业创新服务媒体 ——聚焦数据 改变商业 随着云计算的兴起&#xff0c;大佬纷纷入局赛道&#xff0c;阿里巴巴2009年正式成立阿里云&#xff0c;腾讯、百度、华为等企业也在2016年前后加速布局云计算产业。除此之外&#xff0c;中国电信、移动、联通等玩家也各有动作&a…

基于机器学习的fNIRS信号质量控制方法

摘要 尽管功能性近红外光谱(fNIRS)在神经系统研究中的应用越来越广泛&#xff0c;但fNIRS信号处理仍未标准化&#xff0c;并且受到经验和手动操作的高度影响。在任何信号处理过程的开始阶段&#xff0c;信号质量控制(SQC)对于防止错误和不可靠结果至关重要。在fNIRS分析中&…

DNS 协议都没听过?你配做开发?

一、什么是DNS协议&#xff1f; DNS协议是一种用于将域名转换为IP地址的分布式命名系统。它通过将用户提供的域名映射到相应的IP地址&#xff0c;实现了互联网上资源的定位和访问。DNS协议采用了层次化的域名结构&#xff0c;使得域名之间可以建立逻辑上的关联。 二、DNS解析过…

vs2019 ,c++的STD库全局函数 _Pocma 与 _Swap_adl 的思考

&#xff08;1&#xff09;在阅读vs2019上的 STL库的 map 源码时&#xff0c;遇到了这个函数&#xff0c;之前&#xff0c;在别的源码中也经常出现这个函数。那么这个函数起什么作用呢&#xff1f; 在1880行&#xff0c;有对该函数的调用。其定义如下图&#xff1a;&#xff0…

Redis 7 第二讲 数据类型 基础篇

&#x1f339; 引导 Commands | Redishttps://redis.io/commands/Redis命令中心&#xff08;Redis commands&#xff09; -- Redis中国用户组&#xff08;CRUG&#xff09;Redis命令大全&#xff0c;显示全部已知的redis命令&#xff0c;redis集群相关命令&#xff0c;近期也会…

竟大规模放弃复试!你未战先怯,别跑!

一、学校及专业介绍 长沙理工大学简称长沙理工&#xff08;CSUST&#xff09;&#xff0c;位于湖南省长沙市&#xff0c;国家交通运输部和湖南省人民政府共建高校&#xff0c;湖南省“双一流”建设高校&#xff0c;学校由原国家交通部所属的长沙交通学院和原国家电力公司&…

高频面试题:如何分别用三种姿势实现三个线程交替打印0到100

最近面试遇到的一道题&#xff0c;需要三个线程交替打印0-100&#xff0c;当时对多线程并不是很熟悉因此没怎么写出来&#xff0c;网上搜了之后得到现 synchronized wait/notifyAll 实现思路&#xff1a;判断当前打印数字和线程数的取余&#xff0c;不等于当前线程则处于等待…