Java多线程:常见的锁策略

news2024/11/24 16:47:16

在这里插入图片描述

  • 👑专栏内容:Java
  • ⛪个人主页:子夜的星的主页
  • 💕座右铭:前路未远,步履不停

目录

  • 一、乐观锁 vs 悲观锁
    • 1、乐观锁
    • 2、悲观锁
  • 二、重量级锁 vs 轻量级锁
    • 1、重量级锁
    • 2、轻量级锁
    • 3、理解用户态 vs 内核态
  • 三、自旋锁 vs 挂起等待锁
    • 1、自旋锁
    • 2、挂起等待锁
    • 3、总结
  • 四、读写锁 vs 互斥锁
    • 1、读写锁
    • 2、互斥锁
  • 五、公平锁 vs 非公平锁
    • 1、公平锁
    • 2、非公平锁
  • 六、可重入锁 vs 不可重入锁
    • 1、可重入锁
    • 2、不可重入锁
  • 七、常见问题
    • 1、如何理解乐观锁和悲观锁,具体怎么实现呢?
    • 2、介绍下读写锁?
    • 3、`synchronized` 是可重入锁么?
    • 4、什么是自旋锁,为什么要使用自旋锁策略呢,缺点是什么?


一、乐观锁 vs 悲观锁

乐观锁和悲观锁是两种常见的并发控制策略,用于处理多线程环境下的数据一致性和同步问题。它们从不同的角度出发,针对锁冲突的可能性采取不同的预防措施。Synchronized 初始使用乐观锁策略,当发现锁竞争比较频繁的时候,就会自动切换成悲观锁策略。

1、乐观锁

乐观锁基于这样一个假设:在大多数情况下,锁冲突是不会发生的。因此,它允许多个线程在没有直接锁定资源的情况下并发访问数据,仅在数据提交更新时检查是否存在冲突。如果发现数据在读取后被其他线程修改,操作会被回滚,要求重试。乐观锁的实现通常依赖于数据版本号或时间戳。这种机制适合读操作频繁而写操作相对较少的场景,可以减少锁的开销,提高系统的并发性能。

2、悲观锁

悲观锁则是基于一个相反的假设:认为冲突在多线程访问中是常态,因此在操作数据前,会先尝试锁定资源,确保一次只有一个线程能操作数据。这种策略通过直接避免并发访问来防止冲突,直到当前持有锁的线程完成操作并释放锁后,其他线程才能访问数据。悲观锁适用于写操作频繁的场景,虽然它可能引入更高的等待时间和系统开销,但可以有效保障数据的一致性和完整性。

二、重量级锁 vs 轻量级锁

锁的核心特性”原子性“,这样的机制追根溯源是 CPU 这样的硬件设备提供的。

  • CPU 提供了 “原子操作指令”。
  • 操作系统基于 CPU 的原子指令,实现了 mutex 互斥锁。
  • JVM 基于操作系统提供的互斥锁, 实现了 synchronizedReentrantLock 等关键字和类。

在这里插入图片描述
注意, synchronized 并不仅仅是对 mutex 进行封装,在 synchronized 内部还做了很多其他的工作

1、重量级锁

重量级锁:加锁机制重度依赖了操作系统提供了mutex

  • 大量的内核态用户态切换
  • 很容易引发线程的调度

2、轻量级锁

轻量级锁:加锁机制尽可能不使用 mutex,而是尽量在用户态代码完成。实在搞不定了,再使用 mutex

  • 少量的内核态用户态切换
  • 不太容易引发线程调度

3、理解用户态 vs 内核态

用户态:

  • 相当于你在银行窗口外办理业务,所有操作由你自己完成。
  • 你需要填写表格、准备材料、排队等候,所有流程和时间成本都在你的掌控之中。
  • 办理简单业务时,用户态效率高,因为你无需等待他人协助。

内核态:

  • 相当于你在银行窗口内办理业务,由工作人员为你服务。
  • 你需要向工作人员说明需求,等待他们处理,并可能需要反复沟通确认。
  • 办理复杂业务时,内核态效率更高,因为专业的工作人员可以帮你处理繁琐的事务。

效率对比:

  • 用户态的操作时间成本可控,因为你掌控着所有环节。
  • 内核态的操作时间成本不可控,因为你需要等待工作人员处理,并可能受其他因素影响。

过度切换的代价:

  • 如果你在办理业务过程中,频繁地在窗口外和窗口内之间切换,例如需要反复与工作人员沟通、重新排队等,那么效率会大幅降低
  • 在程序运行中,如果频繁地在用户态和内核态之间切换,也会导致效率降低,因为切换本身需要消耗时间和资源。

三、自旋锁 vs 挂起等待锁

1、自旋锁

按之前的方式,线程在抢锁失败后进入阻塞状态,放弃 CPU,需要过很久才能再次被调度。但实际上,大部分情况下,虽然当前抢锁失败,但过不了很久,锁就会被释放。

没必要就放弃 CPU,这个时候就可以使用自旋锁来处理这样的问题。synchronized 中的轻量级锁策略大概率就是通过自旋锁的方式实现的。

自旋锁伪代码:while (抢锁(lock) == 失败) {}

如果获取锁失败,立即再尝试获取锁,无限循环,直到获取到锁为止。第一次获取锁失败,第二次的尝试会在极短的时间内到来。一旦锁被其他线程释放,就能第一时间获取到锁。

自旋锁是一种典型的 轻量级锁 的实现方式:

自旋锁的优势: 没有放弃 CPU,不涉及线程阻塞和调度,一旦锁被释放,就能第一时间获取到锁。

自旋锁的缺点: 如果锁被其他线程持有的时间比较久,那么就会持续的消耗 CPU 资源。(而挂起等待的时候是不消耗 CPU)

2、挂起等待锁

挂起等待锁是一种重量级锁的实现方式,它与自旋锁相比,在锁竞争激烈的情况下,能够更加有效地避免CPU资源浪费,但同时也带来了线程调度的开销。

工作原理: 当线程尝试获取锁时,如果锁已经被其他线程持有,则当前线程会进入挂起状态,放弃CPU资源,并等待锁被释放。当锁被释放后,操作系统会唤醒所有处于挂起状态的线程,并重新进行锁竞争。

挂起等待锁伪代码:

if (抢锁(lock) == 失败) {
    挂起线程;
    等待锁被释放;
}
// 获取到锁

挂起等待锁的优势:

  • 在锁竞争激烈的情况下,能够有效避免CPU资源浪费。
  • 线程不会一直占用CPU资源,提高了系统的整体效率。

挂起等待锁的劣势:

  • 涉及线程阻塞和调度,带来了额外的开销。
  • 在锁竞争不激烈的情况下,性能可能不如自旋锁。

3、总结

特性自旋锁挂起等待锁
加锁方式用户态,自旋循环内核态,系统锁机制
资源消耗低 (锁空闲时) / 高 (锁竞争激烈时)低 (锁竞争激烈时)
适用场景竞争不激烈竞争激烈
性能表现快速灵动稳定可靠

四、读写锁 vs 互斥锁

1、读写锁

读写锁是一种特殊的锁机制,它针对读写操作进行区分,允许多个线程同时进行读操作,但只能有一个线程进行写操作,从而提高并发效率。

读写锁就是把读操作和写操作区分对待。Java 标准库提供了 ReentrantReadWriteLock 类,实现了读写锁。

  • ReentrantReadWriteLock.ReadLock 类表示一个读锁。这个对象提供了lock / unlock方法进行加锁解锁。
  • ReentrantReadWriteLock.WriteLock 类表示一个写锁。这个对象也提供了lock / unlock方法进行加锁解锁。

其中:

  • 读读不互斥:多个线程可以同时获取读锁,进行读操作。
  • 写写互斥:多个线程不能同时获取写锁,进行写操作。
  • 读写互斥:读线程和写线程不能同时获取锁。

读写锁的优势:

  • 在读操作远多于写操作的情况下,读写锁可以大幅提高并发效率。
  • 避免了互斥锁过度锁定的问题,提高了程序的整体性能。

读写锁的应用场景:

  • 缓存系统:读操作远多于写操作,使用读写锁可以提高缓存命中率。
  • 数据库系统:读操作远多于写操作,使用读写锁可以提高数据库的并发能力。
  • 配置文件:读操作远多于写操作,使用读写锁可以提高配置文件的读取效率。

2、互斥锁

互斥锁则是传统的锁机制,它不允许任何线程同时获取锁,无论读写操作。

五、公平锁 vs 非公平锁

公平锁和非公平锁也是两种常见的锁机制,它们在锁的分配策略上存在差异,从而影响程序的运行效率和公平性。

下面以追女神为例:
在这里插入图片描述

1、公平锁

公平锁是线程按照申请锁的顺序获取锁,就像排队一样,先来后到。即使线程在竞争锁时处于休眠状态,也不会影响其获取锁的机会。可以保证每个线程都有平等的机会获取锁,避免线程饥饿问题。

当女神和前任分手之后,先来追女神的男生上位,这就是公平锁。
在这里插入图片描述

2、非公平锁

如果是女神不按先后顺序挑一个自己看的顺眼的,就是非公平锁。synchronized是非公平锁。
在这里插入图片描述

六、可重入锁 vs 不可重入锁

1、可重入锁

可重入锁的字面意思是“可以重新进入的锁”,即允许同一个线程多次获取同一把锁。

可重入锁允许同一个线程在递归调用过程中多次获取锁,而不会发生阻塞。就像一个人拿着自己家的钥匙,可以多次打开自己的房门一样。Java 中的 ReentrantLock 类、synchronized 关键字锁都是可重入锁。

理解可重入锁:

  • 可重入锁通过计数器来实现,记录当前线程获取锁的次数。
  • 当线程第一次获取锁时,计数器加 1;
  • 当线程再次获取锁时,计数器再次加 1;
  • 当线程释放锁时,计数器减 1;
  • 只有当计数器为 0 时,锁才会被释放,其他线程才能获取锁。

可重入锁的优势:

  • 避免了递归调用时死锁的发生。
  • 提高了代码的简洁性。

2、不可重入锁

不可重入锁不允许同一个线程在递归调用过程中再次获取锁,否则会发生阻塞。就像一个人拿着别人的钥匙,只能打开一次门,再次尝试打开门时会因为钥匙不匹配而被阻拦。Linux 系统提供的 mutex锁是不可重入锁。

理解不可重入锁:

  • 不可重入锁没有计数器,只记录锁的持有者。
  • 当线程获取锁后,其他线程无法再次获取锁,直到该线程释放锁。

七、常见问题

1、如何理解乐观锁和悲观锁,具体怎么实现呢?

锁类型特点实现方式
悲观锁认为多个线程访问同一共享变量冲突概率大,每次都会加锁借助操作系统提供的锁机制,如 mutex,获取锁后操作数据
乐观锁认为冲突概率不大,不加锁直接尝试访问数据,识别访问冲突引入版本号,通过版本号判断数据访问是否冲突

悲观锁认为多个线程访问同一个共享变量冲突的概率较大,会在每次访问共享变量之前都去真正加
锁;乐观锁认为多个线程访问同一个共享变量冲突的概率不大,并不会真的加锁,而是直接尝试访问数据。在访问的同时识别当前的数据是否出现访问冲突。

悲观锁的实现就是先加锁(比如借助操作系统提供的 mutex),获取到锁再操作数据,获取不到锁就等待。乐观锁的实现可以引入一个版本号,借助版本号识别出当前的数据访问是否冲突。

2、介绍下读写锁?

读写锁就是把读操作和写操作分别进行加锁。

  • 读锁和读锁之间不互斥。
  • 写锁和写锁之间互斥。
  • 写锁和读锁之间互斥。

读写锁最主要用在 “频繁读,不频繁写” 的场景中。

3、synchronized 是可重入锁么?

是可重入锁,可重入锁指的就是连续两次加锁不会导致死锁。
实现的方式是在锁中记录该锁持有的线程身份,以及一个计数器(记录加锁次数)。如果发现当前加锁
的线程就是持有锁的线程,则直接计数自增。

4、什么是自旋锁,为什么要使用自旋锁策略呢,缺点是什么?

如果获取锁失败,立即再尝试获取锁,无限循环直到获取到锁为止。第一次获取锁失败,第二次的尝试会在极短的时间内到来。一旦锁被其他线程释放,就能第一时间获取到锁。

相比于挂起等待锁:

  • 优点:没有放弃 CPU 资源,一旦锁被释放就能第一时间获取到锁,更高效。在锁持有时间比较短的场景下非常有用。
  • 缺点:如果锁的持有时间较长。就会浪费 CPU 资源。

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

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

相关文章

LibreOffice Calc 取消首字母自动大写 (Capitalize first letter of every sentence)

LibreOffice Calc 取消首字母自动大写 [Capitalize first letter of every sentence] 1. Tools -> AutoCorrect Options2. AutoCorrect -> Options -> Capitalize first letter of every sentenceReferences 1. Tools -> AutoCorrect Options 2. AutoCorrect ->…

SpringCloud-高级篇(二十一)

前面解决了消息的可靠性、消息的延迟问题,下面研究一下消息的堆积的问题: (1)消息堆积问题 消息产生堆积,上限后,最早的消息成为死信,有消息被丢弃,这对安全性较高的业务中是不行的…

Uniapp真机调试:手机端访问电脑端的后端接口解决

Uniapp真机调试:手机端访问电脑端的后端接口解决 1、前置操作 HBuilderX -> 运行 -> 运行到手机或模拟器 -> 运行到Android App基座 少了什么根据提示点击下载即可 使用数据线连接手机和电脑 手机端:打开开发者模式 -> USB调试打开手机端&…

小白速成法:剖析一个Android项目以快速上手

这是一个基于Tasmota的设备、用MQTT协议来通信控制的安卓应用程序。支持ON/OFF命令插座和基本的RGB LED控制。 源码点击此处 只需要关注SmartController-main\app\src的代码 项目解压之后如图 只需要关注“app”文件夹里的东西即可,“gradle”是配置文件&#xf…

【前端web入门第五天】03 清除默认样式与外边距问题【附综合案例产品卡片与新闻列表】

文章目录: 1.清除默认样式 1.1清除内外边距1.2清除列表圆点(项目符号) 3.外边距问题-合并现象4.外边距问题–塌陷问题5.行内元素垂直内外边距6.圆角与盒子阴影 6.1圆角 6.2 盒子模型-阴影(拓展) 综合案例一 产品卡片 综合案例二 新闻列表 1.清除默认样式 在实际设计开发中,要…

Pandas深度解析GroupBy函数的妙用技巧【第75篇—GroupBy函数】

Pandas深度解析GroupBy函数的妙用技巧 数据处理和分析中,Pandas是一款非常强大的Python库,提供了丰富的数据结构和功能,使得数据分析变得更加简便高效。其中,GroupBy函数是Pandas中一个重要且常用的功能,通过它我们可…

第四节课[XTuner微调]作业

文章目录 前言作业基础作业-XTuner InternLM-Chat 个人小助手认知微调实践 前言 XTuner 做在第三节课LangChain作业之前,因为第三节课没想好找哪个领域,等第三节课作业做了一起部署。 作业 基础作业-XTuner InternLM-Chat 个人小助手认知微调实践 然…

Ps:直接从图层生成文件(图像资源)

通过Ps菜单:文件/导出/将图层导出到文件 Layers to Files命令,我们可以快速地将当前文档中的每个图层导出为同一类型、相同大小和选项的独立文件。 Photoshop 还提供了一个功能,可以基于文档中的图层或图层组的名称,自动生成指定大…

渗透专用虚拟机(公开版)

0x01 工具介绍 okfafu渗透虚拟机公开版。解压密码:Mrl64Miku,压缩包大小:15.5G,解压后大小:16.5G。安装的软件已分类并在桌面中体现,也可以使用everything进行查找。包含一些常用的渗透工具以及一些基本工…

opencv图像像素的读写操作

void QuickDemo::pixel_visit_demo(Mat & image) {int w image.cols;//宽度int h image.rows;//高度int dims image.channels();//通道数 图像为灰度dims等于一 图像为彩色时dims等于三 for (int row 0; row < h; row) {for (int col 0; col < w; col) {if…

RabbitMQ之五种消息模型

1、 环境准备 创建Virtual Hosts 虚拟主机&#xff1a;类似于mysql中的database。他们都是以“/”开头 设置权限 2. 五种消息模型 RabbitMQ提供了6种消息模型&#xff0c;但是第6种其实是RPC&#xff0c;并不是MQ&#xff0c;因此不予学习。那么也就剩下5种。 但是其实3、4…

【数学建模】【2024年】【第40届】【MCM/ICM】【E题 财产保险的可持续性】【解题思路】

一、题目 &#xff08;一&#xff09; 赛题原文 2024 ICM Problem E: Sustainability of Property Insurance Extreme-weather events are becoming a crisis for property owners and insurers. The world has endured “more than $1 trillion in damages from more than …

尚硅谷 Vue3+TypeScript 学习笔记(下)

目录 五、组件通信 5.1. 【props】 5.2. 【自定义事件】 5.3. 【mitt】 5.4.【v-model】 5.5.【$attrs】 5.6. 【$refs、$parent】 5.7. 【provide、inject】 5.8. 【pinia】 5.9. 【slot】 1. 默认插槽 2. 具名插槽 3. 作用域插槽 六、其它 API 6.1.【shallowR…

Python学习从0到1 day16 Python文件操作

2024新年快乐&#xff01;&#xff01;&#xff01; 今天是大年初三&#xff0c;祝大家万事胜意&#xff0c;一切都会好的&#xff0c;休息了一段时间&#xff0c;从今天开始继续学习~ ​​​​​​​——24.2.12 一、文件的编码 计算机只能识别0和1&#xff0c;那么我们丰富的…

Spring Boot 笔记 002 整合mybatis做数据库读取

概念 MyBatis 是一款优秀的持久层框架&#xff0c;它支持自定义 SQL、存储过程以及高级映射。MyBatis 免除了几乎所有的 JDBC 代码以及设置参数和获取结果集的工作。MyBatis 可以通过简单的 XML 或注解来配置和映射原始类型、接口和 Java POJO&#xff08;Plain Old Java Objec…

并发CPU伪共享及优化

目录 伪共享 解决 伪共享 缓存系统中是以缓存行&#xff08;cache line&#xff09;为单位存储的。缓存行是2的整数幂个连续字节&#xff0c;一般为32-256个字节。最常见的缓存行大小是64个字节。当多线程修改互相独立的变量时&#xff0c;如果这些变量共享同一个缓存行&am…

如何在 Windows 10/11 上恢复回收站永久删除的文件夹?

经验丰富的 Windows 用户将使用 Windows 备份和还原或文件历史记录来恢复不在回收站中的已删除文件夹。这些工具确实有助于 Windows 文件夹恢复&#xff0c;但并不总是有效。现在有许多专用的 Windows 数据恢复软件和免费解决方案可以替代它们&#xff0c;为 Windows 用户提供了…

前端vue 数字 字符串 丢失精度问题

1.问题 后端返回的数据 是这样的 一个字符串类型的数据 前端要想显示这个 肯定需要使用Json.parse() 转换一下 但是 目前有一个问题 转换的确可以 showId:1206381711026823172 有一个这样的字段 转换了以后 发现 字段成了1206381711026823200 精度直接丢了 原本的数据…

分享76个文字特效,总有一款适合您

分享76个文字特效&#xff0c;总有一款适合您 76个文字特效下载链接&#xff1a;https://pan.baidu.com/s/1rIiUdCMQScoRVKhFhXQYpw?pwd8888 提取码&#xff1a;8888 Python采集代码下载链接&#xff1a;采集代码.zip - 蓝奏云 学习知识费力气&#xff0c;收集整理更不…

项目02《游戏-14-开发》Unity3D

基于 项目02《游戏-13-开发》Unity3D &#xff0c; 任务&#xff1a;战斗系统之击败怪物与怪物UI血条信息 using UnityEngine; public abstract class Living : MonoBehaviour{ protected float hp; protected float attack; protected float define; …