【多线程】常见锁策略详解(面试常考题型)
目录
- 🌴 乐观锁 vs 悲观锁
- 🎍重量级锁 vs 轻量级锁
- 🍀自旋锁(Spin Lock)
- 🎋公平锁 vs ⾮公平锁
- 🌳可重⼊锁 vs 不可重⼊锁
- 🎄读写锁
- ⭕相关面试题
常⻅的锁策略
注意: 接下来讲解的锁策略不仅仅是局限于 Java . 任何和 “锁” 相关的话题, 都可能会涉及到以下内容.
 这些特性主要是给锁的实现者来参考的.
普通的程序猿也需要了解⼀些, 对于合理的使⽤锁也是有很⼤帮助的.
🌴 乐观锁 vs 悲观锁
悲观锁:
 总是假设最坏的情况,每次去拿数据的时候都认为别⼈会修改,所以每次在拿数据的时候都会上锁,
 这样别⼈想拿这个数据就会阻塞直到它拿到锁。
乐观锁:
 假设数据⼀般情况下不会产⽣并发冲突,所以在数据进⾏提交更新的时候,才会正式对数据是否产⽣
 并发冲突进⾏检测,如果发现并发冲突了,则让返回⽤⼾错误的信息,让⽤⼾决定如何去做。
乐观锁的一个重要功能就是要检测出数据是否发生访问冲突.
那我们具体是怎么检测的呢?这里我们我们可以引入一个 “版本号” 来解决.
那什么是版本号呢?请看下面的例子:
 假设我们需要多线程修改 “用户账户余额”.
设当前余额为 100. 引入一个版本号 version, 初始值为 1. 并且我们规定 "提交版本必须大于记录当前版本”才能执行更新余额
接下来我们进行以下操作:
第一步:线程 A 此时准备将其读出( version=1, balance=100 ),线程 B 也读入此信息( version=1,balance=100 )
  第二步:线程 A 操作的过程中并从其帐户余额中扣除 50( 100-50 ),线程 B 从其帐户余额中扣除 20( 100-20 );
第二步:线程 A 操作的过程中并从其帐户余额中扣除 50( 100-50 ),线程 B 从其帐户余额中扣除 20( 100-20 );
 
 第三步:线程 A 完成修改工作,将数据版本号加1( version=2 ),连同帐户扣除后余额( balance=50),写回到内存中;
 
 第四步:线程 B 完成了操作,也将版本号加1( version=2 )试图向内存中提交数据( balance=80),但此时比对版本发现,操作员 B 提交的数据版本号为 2 ,数据库记录的当前版本也为 2 ,不满足 “提交版本必须大于记录当前版本才能执行更新“ 的乐观锁策略。就认为这次操作失败.
 
在Java中,Synchronized 初始使⽤乐观锁策略. 当发现锁竞争⽐较频繁的时候, 就会⾃动切换成悲观锁策略.
🎍重量级锁 vs 轻量级锁
锁的核⼼特性 “原⼦性”, 这样的机制追根溯源是 CPU 这样的硬件设备提供的.
 • CPU 提供了 “原⼦操作指令”.
 • 操作系统基于 CPU 的原⼦指令, 实现了 mutex 互斥锁.
 • JVM 基于操作系统提供的互斥锁, 实现了 synchronized 和 ReentrantLock 等关键字和类.

 注意, synchronized 并不仅仅是对 mutex 进⾏封装, 在 synchronized 内部还做了很多其
 他的⼯作
重量级锁: 加锁机制重度依赖了 OS 提供了 mutex
• ⼤量的内核态⽤⼾态切换
 • 很容易引发线程的调度
这两个操作, 成本⽐较⾼. ⼀旦涉及到⽤⼾态和内核态的切换, 就意味着 “沧海桑⽥”.
轻量级锁: 加锁机制尽可能不使⽤ mutex, ⽽是尽量在⽤⼾态代码完成. 实在搞不定了, 再使⽤ mutex.
• 少量的内核态⽤⼾态切换.
 • 不太容易引发线程调度.
什么是用户态什么是内核态?
理解⽤⼾态 vs 内核态 想象去银⾏办业务. 在窗⼝外, ⾃⼰做, 这是⽤⼾态. ⽤⼾态的时间成本是⽐较可控的. 在窗⼝内, ⼯作⼈员做,
这是内核态. 内核态的时间成本是不太可控的. 如果办业务的时候反复和⼯作⼈员沟通, 还需要重新排队, 这时效率是很低的.
synchronized 开始是⼀个轻量级锁. 如果锁冲突比较严重, 就会变成重量级锁.
🍀自旋锁(Spin Lock)
按之前的⽅式,线程在抢锁失败后进⼊阻塞状态,放弃 CPU,需要过很久才能再次被调度.
但实际上, ⼤部分情况下,虽然当前抢锁失败,但过不了很久,锁就会被释放。没必要就放弃 CPU. 这个时候就可以使⽤⾃旋锁来处理这样的问题.
⾃旋锁伪代码:
while (抢锁(lock) == 失败) {}
如果获取锁失败, ⽴即再尝试获取锁, ⽆限循环, 直到获取到锁为⽌. 第⼀次获取锁失败, 第⼆次的尝试会在极短的时间内到来.
⼀旦锁被其他线程释放, 就能第⼀时间获取到锁
理解⾃旋锁 vs 挂起等待锁
想象⼀下, 去追求⼀个⼥神. 当男⽣向⼥神表⽩后, ⼥神说: 你是个好⼈, 但是我有男朋友了~~
挂起等待锁: 陷⼊沉沦不能⾃拔… 过了很久很久之后, 突然⼥神发来消息, “咱俩要不试试?” (注意, 这
个很⻓的时间间隔⾥, ⼥神可能已经换了好⼏个男票了).
⾃旋锁: 死⽪赖脸坚韧不拔. 仍然每天持续的和⼥神说早安晚安. ⼀旦⼥神和上⼀任分⼿, 那么就能⽴刻
抓住机会上位.
==⾃旋锁是⼀种典型的 轻量级锁 的实现⽅式.
 • 优点: 没有放弃 CPU, 不涉及线程阻塞和调度, ⼀旦锁被释放, 就能第⼀时间获取到锁.
 • 缺点: 如果锁被其他线程持有的时间⽐较久, 那么就会持续的消耗 CPU 资源. (⽽挂起等待的时候是不消耗 CPU 的).
synchronized 中的轻量级锁策略⼤概率就是通过⾃旋锁的⽅式实现的.
🎋公平锁 vs ⾮公平锁
假设三个线程 A, B, C. A 先尝试获取锁, 获取成功. 然后 B 再尝试获取锁, 获取失败, 阻塞等待; 然后 C
 也尝试获取锁, C 也获取失败, 也阻塞等待.
当线程 A 释放锁的时候, 会发⽣啥呢?
公平锁: 遵守 “先来后到”. B ⽐ C 先来的. 当 A 释放锁的之后, B 就能先于 C 获取到锁.
⾮公平锁: 不遵守 “先来后到”. B 和 C 都有可能获取到锁.
这就好⽐⼀群男⽣追同⼀个⼥神. 当⼥神和前任分⼿之后, 先来追⼥神的男⽣上位, 这就是公平锁; 如果
 是⼥神不按先后顺序挑⼀个⾃⼰看的顺眼的, 就是⾮公平锁.
 
注意:
• 操作系统内部的线程调度就可以视为是随机的. 如果不做任何额外的限制, 锁就是⾮公平锁. 如果要
 想实现公平锁, 就需要依赖额外的数据结构, 来记录线程们的先后顺序.
• 公平锁和⾮公平锁没有好坏之分, 关键还是看适⽤场景.
synchronized 是⾮公平锁.
🌳可重⼊锁 vs 不可重⼊锁
可重⼊锁的字⾯意思是“可以重新进⼊的锁”,即允许同⼀个线程多次获取同⼀把锁。
⽐如⼀个递归函数⾥有加锁操作,递归过程中这个锁会阻塞⾃⼰吗?如果不会,那么这个锁就是可重⼊锁(因为这个原因可重⼊锁也叫做递归锁)。
Java⾥只要以Reentrant开头命名的锁都是可重⼊锁,⽽且JDK提供的所有现成的Lock实现类,包括synchronized关键字锁都是可重⼊的。
⽽ Linux 系统提供的 mutex 是不可重⼊锁.
理解 “把⾃⼰锁死”
 ⼀个线程没有释放锁, 然后⼜尝试再次加锁.
1 // 第⼀次加锁, 加锁成功
2 lock();
3 // 第⼆次加锁, 锁已经被占⽤, 阻塞等待. 
4 lock();
按照之前对于锁的设定, 第⼆次加锁的时候, 就会阻塞等待. 直到第⼀次的锁被释放, 才能获取到第⼆个
 锁. 但是释放第⼀个锁也是由该线程来完成, 结果这个线程已经躺平了, 啥都不想⼲了, 也就⽆法进⾏解
 锁操作. 这时候就会 死锁.

 这样的锁称为 不可重⼊锁.
最后,要记得
synchronized 是可重入锁
🎄读写锁
多线程之间,数据的读取⽅之间不会产⽣线程安全问题,但数据的写⼊⽅互相之间以及和读者之间都
 需要进⾏互斥。如果两种场景下都⽤同⼀个锁,就会产⽣极⼤的性能损耗。所以读写锁因此⽽产⽣。
读写锁(readers-writer lock),看英⽂可以顾名思义,在执⾏加锁操作时需要额外表明读写意图,复数读者之间并不互斥,⽽写者则要求与任何⼈互斥。
⼀个线程对于数据的访问, 主要存在两种操作: 读数据 和 写数据.
• 两个线程都只是读⼀个数据, 此时并没有线程安全问题. 直接并发的读取即可.
 • 两个线程都要写⼀个数据, 有线程安全问题.
 • ⼀个线程读另外⼀个线程写, 也有线程安全问题.
读写锁就是把读操作和写操作区分对待. Java 标准库提供了 ReentrantReadWriteLock 类, 实现
 了读写锁
- ReentrantReadWriteLock.ReadLock 类表⽰⼀个读锁. 这个对象提供了 lock / unlock ⽅法
 进⾏加锁解锁.
- ReentrantReadWriteLock.WriteLock 类表⽰⼀个写锁. 这个对象也提供了 lock / unlock
 ⽅法进⾏加锁解锁
其中,
- 读加锁和读加锁之间, 不互斥.
- 写加锁和写加锁之间, 互斥.
- 读加锁和写加锁之间, 互斥.
注意, 只要是涉及到 “互斥”, 就会产⽣线程的挂起等待. ⼀旦线程挂起, 再次被唤醒就不知道隔了多久
 了.
 因此尽可能减少 “互斥” 的机会, 就是提⾼效率的重要途径
读写锁特别适合于 “频繁读, 不频繁写” 的场景中. (这样的场景其实也是⾮常⼴泛存在的).
⽐如学校的教务系统.
每节课⽼师都要使⽤教务系统点名, 点名就需要查看班级的同学列表(读操作). 这个操作可能要每周执
⾏好⼏次.
⽽什么时候修改同学列表呢(写操作)? 就新同学加⼊的时候. 可能⼀个⽉都不必改⼀次.
再⽐如, 同学们使⽤教务系统查看作业(读操作), ⼀个班级的同学很多, 读操作⼀天就要进⾏⼏⼗次上
百次.
但是这⼀节课的作业, ⽼师只是布置了⼀次(写操作)
Synchronized 不是读写锁.、
⭕相关面试题
- 你是怎么理解乐观锁和悲观锁的,具体怎么实现呢?
悲观锁认为多个线程访问同⼀个共享变量冲突的概率较⼤, 会在每次访问共享变量之前都去真正加锁.
乐观锁认为多个线程访问同⼀个共享变量冲突的概率不⼤. 并不会真的加锁, ⽽是直接尝试访问数据.
在访问的同时识别当前的数据是否出现访问冲突.
悲观锁的实现就是先加锁(⽐如借助操作系统提供的 mutex), 获取到锁再操作数据. 获取不到锁就等待.
乐观锁的实现可以引⼊⼀个版本号. 借助版本号识别出当前的数据访问是否冲突. (实现细节参考上⾯
的图).
2.介绍下读写锁?
读写锁就是把读操作和写操作分别进⾏加锁.
读锁和读锁之间不互斥
写锁和写锁之间互斥.
写锁和读锁之间互斥.
读写锁最主要⽤在 “频繁读, 不频繁写” 的场景中.、
3.什么是⾃旋锁,为什么要使⽤⾃旋锁策略呢,缺点是什么?
如果获取锁失败, ⽴即再尝试获取锁, ⽆限循环, 直到获取到锁为⽌. 第⼀次获取锁失败, 第⼆次的尝试
会在极短的时间内到来. ⼀旦锁被其他线程释放, 就能第⼀时间获取到锁.
相⽐于挂起等待锁,
优点: 没有放弃 CPU 资源, ⼀旦锁被释放就能第⼀时间获取到锁, 更⾼效. 在锁持有时间⽐较短的场景
下⾮常有⽤.
缺点: 如果锁的持有时间较⻓, 就会浪费 CPU 资源.
4.synchronized 是可重⼊锁么?
是可重⼊锁.
可重⼊锁指的就是连续两次加锁不会导致死锁.
实现的⽅式是在锁中记录该锁持有的线程⾝份, 以及⼀个计数器(记录加锁次数). 如果发现当前加锁的
线程就是持有锁的线程, 则直接计数⾃增.
相关文章:
 
【多线程】常见锁策略详解(面试常考题型)
目录 🌴 乐观锁 vs 悲观锁🎍重量级锁 vs 轻量级锁🍀自旋锁(Spin Lock)🎋公平锁 vs ⾮公平锁🌳可重⼊锁 vs 不可重⼊锁🎄读写锁⭕相关面试题 常⻅的锁策略 注意: 接下来讲解的锁策略不…...
Python列表操作函数
在Python中,列表(list)是一种可变的数据类型,它包含一系列有序的元素。Python提供了一系列内置的函数和方法来操作列表。以下是一些常用的Python列表操作函数和方法: 列表方法 append(x) 将元素x添加到列表的末尾。 …...
 
Qt注册类对象单例与单类型区别
1.实现类型SingletonTypeExample #ifndef SINGLETONTYPEEXAMPLE_H #define SINGLETONTYPEEXAMPLE_H#include <QObject>class SingletonTypeExample : public QObject {Q_OBJECT public://只能显示构造类对象explicit SingletonTypeExample(QObject *parent nullptr);//…...
 
Rocky Linux 运维工具yum
一、yum的简介 yum是用于在基于RPM包管理系统的包管理工具。用户可以通过 yum来搜索、安装、更新和删除软件包,自动处理依赖关系,方便快捷地管理系统上的软件。 二、yum的参数说明 1、install 用于在系统的上安装一个或多个软件包 2、seach 用…...
linux下的ollama
refs: https://github.com/ollama/ollama/blob/main/docs/linux.md 1)安装 curl -fsSL https://ollama.com/install.sh | sh 2)修改服务配置,打开端口允许所有IP地址 refs(https://github.com/ollama/ollama/blob/main/docs/faq.md#where-are-models-stored) C…...
 
YOLOv9详细解读,改进提升全面分析(附YOLOv9结构图)
🥑 Welcome to Aedream同学 s blog! 🥑 文章目录 1. 概要1.1 模型结构上的改动:1.2 训练脚本上的改动: 2. 介绍2.1 背景2.2 主要贡献 3. 总体框架3.1 可编程梯度信息(PGI)3.1.1 辅助可逆分支3.1.2 多级辅助信息 3.2 Ge…...
 
html基础操练和进阶修炼宝典
文章目录 1.超链接标签2.跳锚点3.图片标签4.表格5.表格的方向属性6.子窗口7.音视频标签8.表单9.文件上传10.input属性 html修炼必经之路—各种类型标签详解加展示,关注点赞加收藏,防止迷路哦 1.超链接标签 <!DOCTYPE html> <html lang"en…...
从Mysql 数据库删除重复记录只保留其中一条(删除id最小的一条)
准备工作:新建表tb_coupon /*Navicat Premium Data TransferSource Server : rootlocalhostSource Server Type : MySQLSource Server Version : 50527Source Host : localhost:3306Source Schema : leyouTarget Server Type : My…...
 
从http到websocket
阅读本文之前,你最好已经做过一些websocket的简单应用 从http到websocket HTTP101HTTP 轮询、长轮询和流化其他技术1. 服务器发送事件2. SPDY3. web实时通信 互联网简史web和httpWebsocket协议1. 简介2. 初始握手3. 计算响应健值4. 消息格式5. WebSocket关闭握手 实…...
 
UE5 C++ Widget练习 Button 和 ProgressBar创建血条
一. 1.C创建一个继承Widget类的子类, 命名为MyUserWidget 2.加上Button 和 UserWidget的头文件 #include "CoreMinimal.h" #include "Components/Button.h" #include "Blueprint/UserWidget.h" #include "MyUserWidget.genera…...
 
抖店无货源违规频发,不能入驻?这个是真的吗?
我是电商珠珠 还没有踏入抖店这个电商行业的新手,单从别人的口中,听说了抖店无货源特别容易违规,还会被扣除全部的保证金,得不偿失之类的话。有的还专门劝诫新手不要做抖店,做了就会亏本之类的话,这搞得人…...
 
HarmonyOS—开发云数据库
您可以在云侧工程下开发云数据库资源,包括创建对象类型、在对象类型中添加数据条目、部署云数据库。 创建对象类型 对象类型(即ObjectType)用于定义存储对象的集合,不同的对象类型对应的不同数据结构。每创建一个对象类型&#…...
mysql查询某个数据库的数量有多少GB
要查询MySQL数据库中某个数据库(或称为“schema”)所占用的磁盘空间大小(以GB为单位),你可以使用information_schema数据库中的TABLES和DATA_LENGTH、INDEX_LENGTH字段来获取每个表的数据和索引的大小,然后…...
 
table展示子级踩坑
##elemenui中table通过row中是否有children进行判断是否展示子集,通过设置tree-prop的属性进行设置,子级的children的名字可以根据自己的子级名字进行替换,当然同样可以对数据处理成含有chilren的子级list。 问题: 1.如果是根据后…...
 
xss过waf的小姿势
今天看大佬的视频学到了几个操作 首先是拆分发可以用self将被过滤的函数进行拆分 如下图我用self将alert拆分成两段依然成功执行 然后学习另一种姿势 <svg id"YWxlcnQoIlhTUyIp"><img src1 οnerrοr"window[eval](atob(document.getElementsByTagNa…...
 
【六袆 - MySQL】MySQL 5.5及更高版本中,InnoDB是新表的默认存储引擎;
InnoDB 这是一个MySQL组件,结合了高性能和事务处理能力,以确保可靠性、健壮性和并发访问。它体现了ACID设计哲学。它作为一个存储引擎存在,处理使用ENGINEINNODB子句创建的或修改的表。请参阅第14章“InnoDB存储引擎”以获取有关架构细节和管…...
 
可移植性(兼容性)测试指南
可移植性是指应用程序能够安装到不同的环境中,在不同的环境中使用,甚至可以移动到不同的环境中。当然,前两者对所有系统都很重要。就PC软件而言,鉴于操作系统、共存和互操作应用程序、硬件、带宽可用性等方面的快速变化࿰…...
 
软件更新快讯-Obsidian更新-1.5.8 linux Appimage直装
更新内容 1.5.8: 从具有相同属性的文件导航时,固定属性不会显示。 修复了Home和End在导航文档顶部和底部时不总是起作用的问题。 Fixed properties not appearing when navigating from a file that has the same properties.Fixed Home and End not a…...
 
Android Gradle开发与应用 (二) : Groovy基础语法
1. Groovy是什么 Groovy是基于JVM虚拟机的一种动态语言,语法和Java非常相似,并能够无缝地与Java代码集成和互操作,增加了很多动态类型和灵活的特性。(闭包、DSL) 语法和Java非常相似这个特点,意味着,如果我们完全不懂…...
iptables学习
iptables的4表5链的处理流程 一:业务地址请求服务时,首先经过iptables服务,iptables通过校验规则,通过校验是否同意业务访问,规则从上到下,匹配规则都失败了的话,走默认规则 (1&…...
 
网络编程(Modbus进阶)
思维导图 Modbus RTU(先学一点理论) 概念 Modbus RTU 是工业自动化领域 最广泛应用的串行通信协议,由 Modicon 公司(现施耐德电气)于 1979 年推出。它以 高效率、强健性、易实现的特点成为工业控制系统的通信标准。 包…...
java 实现excel文件转pdf | 无水印 | 无限制
文章目录 目录 文章目录 前言 1.项目远程仓库配置 2.pom文件引入相关依赖 3.代码破解 二、Excel转PDF 1.代码实现 2.Aspose.License.xml 授权文件 总结 前言 java处理excel转pdf一直没找到什么好用的免费jar包工具,自己手写的难度,恐怕高级程序员花费一年的事件,也…...
 
从深圳崛起的“机器之眼”:赴港乐动机器人的万亿赛道赶考路
进入2025年以来,尽管围绕人形机器人、具身智能等机器人赛道的质疑声不断,但全球市场热度依然高涨,入局者持续增加。 以国内市场为例,天眼查专业版数据显示,截至5月底,我国现存在业、存续状态的机器人相关企…...
多模态商品数据接口:融合图像、语音与文字的下一代商品详情体验
一、多模态商品数据接口的技术架构 (一)多模态数据融合引擎 跨模态语义对齐 通过Transformer架构实现图像、语音、文字的语义关联。例如,当用户上传一张“蓝色连衣裙”的图片时,接口可自动提取图像中的颜色(RGB值&…...
Frozen-Flask :将 Flask 应用“冻结”为静态文件
Frozen-Flask 是一个用于将 Flask 应用“冻结”为静态文件的 Python 扩展。它的核心用途是:将一个 Flask Web 应用生成成纯静态 HTML 文件,从而可以部署到静态网站托管服务上,如 GitHub Pages、Netlify 或任何支持静态文件的网站服务器。 &am…...
 
12.找到字符串中所有字母异位词
🧠 题目解析 题目描述: 给定两个字符串 s 和 p,找出 s 中所有 p 的字母异位词的起始索引。 返回的答案以数组形式表示。 字母异位词定义: 若两个字符串包含的字符种类和出现次数完全相同,顺序无所谓,则互为…...
 
推荐 github 项目:GeminiImageApp(图片生成方向,可以做一定的素材)
推荐 github 项目:GeminiImageApp(图片生成方向,可以做一定的素材) 这个项目能干嘛? 使用 gemini 2.0 的 api 和 google 其他的 api 来做衍生处理 简化和优化了文生图和图生图的行为(我的最主要) 并且有一些目标检测和切割(我用不到) 视频和 imagefx 因为没 a…...
音视频——I2S 协议详解
I2S 协议详解 I2S (Inter-IC Sound) 协议是一种串行总线协议,专门用于在数字音频设备之间传输数字音频数据。它由飞利浦(Philips)公司开发,以其简单、高效和广泛的兼容性而闻名。 1. 信号线 I2S 协议通常使用三根或四根信号线&a…...
 
pikachu靶场通关笔记19 SQL注入02-字符型注入(GET)
目录 一、SQL注入 二、字符型SQL注入 三、字符型注入与数字型注入 四、源码分析 五、渗透实战 1、渗透准备 2、SQL注入探测 (1)输入单引号 (2)万能注入语句 3、获取回显列orderby 4、获取数据库名database 5、获取表名…...
Spring Security 认证流程——补充
一、认证流程概述 Spring Security 的认证流程基于 过滤器链(Filter Chain),核心组件包括 UsernamePasswordAuthenticationFilter、AuthenticationManager、UserDetailsService 等。整个流程可分为以下步骤: 用户提交登录请求拦…...
