【JavaEE初阶】 常见的锁策略详解

news2024/12/26 0:03:55

文章目录

  • 🛬常见的锁策略
    • 🌴乐观锁 vs 悲观锁
    • 🎋读写锁
    • 🌳重量级锁 vs 轻量级锁
    • 🎄自旋锁(Spin Lock)
    • 🍀公平锁 vs 非公平锁
    • 🎍可重入锁 vs 不可重入锁
  • 🛫相关面试题
  • ⭕总结

🛬常见的锁策略

接下来讲解的锁策略不仅仅是局限于 Java . 任何和 “锁” 相关的话题, 都可能会涉及到以下内容. 这些特性主要是给锁的实现者来参考的.

普通的程序猿也需要了解一些, 对于合理的使用锁也是有很大帮助的

🌴乐观锁 vs 悲观锁

悲观锁:

总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。

乐观锁
假设数据一般情况下不会产生并发冲突,所以在数据进行提交更新的时候,才会正式对数据是否产生并发冲突进行检测,如果发现并发冲突了,则让返回用户错误的信息,让用户决定如何去做。

举个栗子: 同学 A 和 同学 B 想请教老师一个问题.

同学 A 认为 “老师是比较忙的, 我来问问题, 老师不一定有空解答”. 因此同学 A 会先给老师发消息: “老师你忙嘛? 我下午两点能来找你问个问题嘛?” (相当于加锁操作) 得到肯定的答复之后, 才会真的来问问题.如果得到了否定的答复, 那就等一段时间, 下次再来和老师确定时间. 这个是悲观锁.

同学 B 认为 “老师是比较闲的, 我来问问题, 老师大概率是有空解答的”. 因此同学 B 直接就来找老师.(没加锁, 直接访问资源) 如果老师确实比较闲, 那么直接问题就解决了. 如果老师这会确实很忙, 那么同学 B也不会打扰老师, 就下次再来(虽然没加锁, 但是能识别出数据访问冲突). 这个是乐观锁

注意

这两种思路不能说谁优谁劣, 而是看当前的场景是否合适.
如果当前老师确实比较忙, 那么使用悲观锁的策略更合适, 使用乐观锁会导致 “白跑很多趟”, 耗费额外的资源.
如果当前老师确实比较闲, 那么使用乐观锁的策略更合适, 使用悲观锁会让效率比较低

在Java中,Synchronized 关键字初始使用乐观锁策略. 当发现锁竞争比较频繁的时候, 就会自动切换成悲观锁策略

就好比同学 C 开始认为 “老师比较闲的”, 问问题都会直接去找老师.
但是直接来找两次老师之后, 发现老师都挺忙的, 于是下次再来问问题, 就先发个消息问问老师忙不忙, 再决定是否来问问题.

乐观锁的一个重要功能就是要检测出数据是否发生访问冲突.

那我们具体是怎么检测的呢?这里我们我们可以引入一个 “版本号” 来解决.

那什么是版本号呢?请看下面的例子:

假设我们需要多线程修改 “用户账户余额”.

设当前余额为 100. 引入一个版本号 version, 初始值为 1. 并且我们规定 "提交版本必须大于记录当前版本”才能执行更新余额

接下来我们进行以下操作:

第一步:线程 A 此时准备将其读出( version=1, balance=100 ),线程 B 也读入此信息( version=1,balance=100 )
在这里插入图片描述

第二步:线程 A 操作的过程中并从其帐户余额中扣除 50( 100-50 ),线程 B 从其帐户余额中扣除 20( 100-20 );
在这里插入图片描述

第三步:线程 A 完成修改工作,将数据版本号加1( version=2 ),连同帐户扣除后余额( balance=50),写回到内存中;
在这里插入图片描述

第四步:线程 B 完成了操作,也将版本号加1( version=2 )试图向内存中提交数据( balance=80),但此时比对版本发现,操作员 B 提交的数据版本号为 2 ,数据库记录的当前版本也为 2 ,不满足 “提交版本必须大于记录当前版本才能执行更新“ 的乐观锁策略。就认为这次操作失败.
在这里插入图片描述

🎋读写锁

多线程之间,数据的读取方之间不会产生线程安全问题,但数据的写入方互相之间以及和读者之间都需要进行互斥。如果两种场景下都用同一个锁,就会产生极大的性能损耗。

所以读写锁因此而产生。读写锁(readers-writer lock),看英文可以顾名思义,在执行加锁操作时需要额外表明读写意图,复数读者之间并不互斥,而写者则要求与任何人互斥

一个线程对于数据的访问, 主要存在两种操作: 读数据写数据.

  • 两个线程都只是读一个数据, 此时并没有线程安全问题. 直接并发的读取即可.

  • 两个线程都要写一个数据, 有线程安全问题.

  • 一个线程读另外一个线程写, 也有线程安全问题

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

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

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

我们需要明白的是,这其中

  • 读加锁和读加锁之间, 不互斥

  • 写加锁和写加锁之间, 互斥

  • 读加锁和写加锁之间, 互斥

需要注意是:

  • 只要是涉及到 “互斥”, 就会产生线程的挂起等待. 一旦线程挂起, 再次被唤醒就不知道隔了多久了.

  • 因此尽可能减少 “互斥” 的机会, 就是提高效率的重要途径

读写锁的应用场景:

读写锁特别适合于 “频繁读, 不频繁写” 的场景中. (这样的场景其实也是非常广泛存在的)

比如学校的教务系统.
每节课老师都要使用教务系统点名, 点名就需要查看班级的同学列表(读操作). 这个操作可能要每天就要执行好几次.
而什么时候修改同学列表呢(写操作)? 就新同学加入的时候. 可能一个月都不必改一次.
再比如, 同学们使用教务系统查看自己课表的时候(读操作), 一个班级的同学很多, 读操作一天就要进行几十次,一学期可能就几百次几千次.但是这一学期的课表, 学校可能只用发布一次(写操作)

最后需要注意的是:

Synchronized 不是读写锁

🌳重量级锁 vs 轻量级锁

谈起这两个锁,不得不说一下锁的核心特性

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

  • CPU 提供了 “原子操作指令”.

  • 操作系统基于 CPU 的原子指令, 实现了 mutex 互斥锁.

  • JVM 基于操作系统提供的互斥锁, 实现了 synchronized 和 ReentrantLock 等关键字和类.

在这里插入图片描述

值得注意的是:注意, synchronized 并不仅仅是对 mutex 进行封装, 在 synchronized 内部还做了很多其他的工作

重量级锁是基于挂起等待锁方式实现的

加锁机制重度依赖了 OS 提供了 mutex

这个锁的特点是:

  • 大量的内核态用户态切换

  • 很容易引发线程的调度

怎么理解呢?

就是如果发现该锁是重量级锁,那么获取锁线程就会进入阻塞等待状态,当锁释放时,该线程就需要重新被CPU调用,这个过程时非常耗费时间的,而且什么是时候被调用也是个问题

轻量级锁是基于自旋锁的方式实现的

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

  • 少量的内核态用户态切换.

  • 不太容易引发线程调度

这个过程就相当于,我们发现是轻量级锁的时候,并不会进入阻塞态,CPU也不会暂时放下该线程,而是处于等待一会儿,如果该时间内锁释放了,就可以立即获取该锁,如果超过一定时间锁还没有释放,那么这个锁就会变为重量级锁

这样一来,虽然占用了CPU,但是提升了效率

上述再讲解中提到两个名词用户态与内核态

其实博主在【JavaEE初阶】 线程池详解与实现也有提到,这里简单再提一下:

想象去银行办业务.

  • 在窗口外, 自己做, 这是用户态. 用户态的时间成本是比较可控的.

  • 在窗口内, 工作人员做, 这是内核态. 内核态的时间成本是不太可控的.

这时候我们想,如果办业务的时候反复和工作人员沟通, 还需要重新排队, 这时效率是很低的.

上述讲的频繁切换用户态和内核态很耗费时间,就是这个道理

最后值得注意的是:

synchronized 开始是一个轻量级锁. 如果锁冲突比较严重, 就会变成重量级锁

至于怎么变,博主会在下一篇博文中讲到

🎄自旋锁(Spin Lock)

按之前的方式,线程在抢锁失败后进入阻塞状态(重量级锁),放弃 CPU,需要过很久才能再次被调度.

但实际上, 大部分情况下,虽然当前抢锁失败,但过不了很久,锁就会被释放。没必要就放弃 CPU. 这个时候就可以使用自旋锁来处理这样的问题

自旋锁伪代码

while (抢锁(lock) == 失败) {}

上述伪代码思想为:

  • 如果获取锁失败, 立即再尝试获取锁, 无限循环, 直到获取到锁为止.

  • 第一次获取锁失败, 第二次的尝试会在极短的时间内到来

这样的话,一旦锁被其他线程释放, 就能第一时间获取到锁.

这里呢,博主对上述重量级锁与轻量级锁的两种实现方式(自旋锁挂起等待锁)进行一个区分

比如,想象一下, 去追求一个女神. 当男生向女神表白后, 女神说: 你是个好人, 但是我有男朋友了~~

  • 挂起等待锁: 陷入沉沦不能自拔… 过了很久很久之后, 突然女神发来消息, “咱俩要不试试?” (注意,这个很长的时间间隔里, 女神可能已经换了好几个男票了).
  • 自旋锁: 死皮赖脸坚韧不拔. 仍然每天持续的和女神说早安晚安. 一旦女神和上一任分手, 那么就能立刻抓住机会上位

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

  • 优点: 没有放弃 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 是不可重入锁

什么叫把自己锁死呢?

一个线程没有释放锁, 然后又尝试再次加锁

// 第一次加锁, 加锁成功
lock();
// 第二次加锁, 锁已经被占用, 阻塞等待.
lock();

按照之前对于锁的设定, 第二次加锁的时候, 就会阻塞等待. 直到第一次的锁被释放, 才能获取到第二个锁. 但是释放第一个锁也是由该线程来完成, 结果这个线程已经躺平了, 啥都不想干了, 也就无法进行解锁操作. 这时候就会 死锁

在这里插入图片描述
这样的锁称为 不可重入锁

最后值得注意

synchronized 是可重入锁

🛫相关面试题

  1. 你是怎么理解乐观锁和悲观锁的,具体怎么实现呢?
  • 悲观锁认为多个线程访问同一个共享变量冲突的概率较大, 会在每次访问共享变量之前都去真正加锁.
  • 乐观锁认为多个线程访问同一个共享变量冲突的概率不大. 并不会真的加锁, 而是直接尝试访问数据. 在访问的同时识别当前的数据是否出现访问冲突.
  • 悲观锁的实现就是先加锁(比如借助操作系统提供的 mutex), 获取到锁再操作数据. 获取不到锁就等待.
  • 乐观锁的实现可以引入一个版本号. 借助版本号识别出当前的数据访问是否冲突. (实现细节参考上面的图).
  1. 介绍下读写锁?
  • 读写锁就是把读操作和写操作分别进行加锁.
  • 读锁和读锁之间不互斥.
  • 写锁和写锁之间互斥.
  • 写锁和读锁之间互斥.
  • 读写锁最主要用在 “频繁读, 不频繁写” 的场景中
  1. 什么是自旋锁,为什么要使用自旋锁策略呢,缺点是什么?
  • 如果获取锁失败, 立即再尝试获取锁, 无限循环, 直到获取到锁为止. 第一次获取锁失败, 第二次的尝试会在极短的时间内到来. 一旦锁被其他线程释放, 就能第一时间获取到锁.
  • 相比于挂起等待锁,
    优点: 没有放弃 CPU 资源, 一旦锁被释放就能第一时间获取到锁, 更高效. 在锁持有时间比较短的场景下非常有用.
    缺点: 如果锁的持有时间较长, 就会浪费 CPU 资源
  1. synchronized 是可重入锁么?
  • 是可重入锁.
  • 可重入锁指的就是连续两次加锁不会导致死锁.
  • 实现的方式是在锁中记录该锁持有的线程身份, 以及一个计数器(记录加锁次数). 如果发现当前加锁的线程就是持有锁的线程, 则直接计数自增

⭕总结

关于《【JavaEE初阶】 常见的锁策略详解》就讲解到这儿,感谢大家的支持,欢迎各位留言交流以及批评指正,如果文章对您有帮助或者觉得作者写的还不错可以点一下关注,点赞,收藏支持一下!

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

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

相关文章

超级马里奥

欢迎来到程序小院 超级马里奥 玩法&#xff1a;点击鼠标左键进行马里奥跳跃&#xff0c;带着马里奥跳跃不同的障碍物&#xff0c;统计分数&#xff0c;快去玩变态超级玛丽吧^^。开始游戏https://www.ormcc.com/play/gameStart/193 html <canvas id"gamescene"&g…

软件测试( 基础篇)

前言 从这篇博文开始&#xff0c;我们将作为一名刚刚加入测试团队的菜鸟&#xff0c;开始一次测试之旅。 在这里我们将讨论以下问题&#xff1a; 软件测试的生命周期 如何描述一个bug 如何定义bug的级别 bug的生命周期 产生争执怎么办 软件测试的生命周期 先回顾一个点&#…

TortoiseSVN安装与配置教程:使用内网穿透实现公网提交文件到本地SVN服务器

文章目录 前言1. TortoiseSVN 客户端下载安装2. 创建检出文件夹3. 创建与提交文件4. 公网访问测试 前言 TortoiseSVN是一个开源的版本控制系统&#xff0c;它与Apache Subversion&#xff08;SVN&#xff09;集成在一起&#xff0c;提供了一个用户友好的界面&#xff0c;方便用…

SpringBoot整合redis实现过期Key监控处理(最简单模式)

前言&#xff1a;写这篇文章的目的主要是这方面的知识我是第一次实际运用到&#xff0c;在项目里面有个功能&#xff0c;需要登录的时候根据手机号发送短信验证码&#xff0c;我看了公司的代码是用redis过期key监控实现的&#xff0c;由于之前没有接触过这类&#xff0c;现在写…

Java CC 解析 SQL 语法示例

示例&#xff1a;SimpleSelectParser 解析 select 11; 输出 2&#xff1b; 0&#xff09;总结 编写 JavaCC 模板&#xff0c;*.jj 文件。 编译生成代码文件。 移动代码文件到对应的包下。 调用生成的代码文件。 1&#xff09;JavaCC 模板 main/javacc/SimpleSelectParse…

C# Socket通信从入门到精通(3)——单个异步TCP客户端C#代码实现

前言: Socket通信中有tcp通信,并且tcp有客户端,tcp客户端程序又分为同步通信和异步通信,所谓同步通信其实就是阻塞式通信,比如客户端调用接收服务器数据函数以后,如果服务器没有发送数据给客户端,则客户端程序会一直阻塞一直到客户端接收到服务器的数据为止;所谓异步通…

电脑文件加密软件

天锐绿盾电脑文件加密软件是一款专业的信息安全防泄密软件。该软件基于核心驱动层的透明加密软件&#xff0c;为企业提供信息化防泄密一体化方案&#xff0c;不改变操作习惯&#xff0c;不降低工作效率&#xff0c;实现数据防泄密管理。 PC访问地址&#xff1a; https://isite…

Redis incr实现流水号自动增长

文章目录 问题描述&#xff1a;实现思路代码案例 问题描述&#xff1a; Java项目实现流水号自动增长&#xff0c;项目需求中有时需要生成一定规则递增编号&#xff1a; eg用户编码自动生成&#xff0c;规则&#xff1a;user7位数字&#xff0c;每次新增自增长&#xff0c;例&…

Nginx安装配置项目部署然后加SSL

个人操作笔记记录 第一步&#xff1a;把 nginx 的源码包nginx-1.8.0.tar.gz上传到 linux 系统 第二步&#xff1a;解压缩 tar zxvf nginx-1.8.0.tar.gz 第三步&#xff1a;进入nginx-1.8.0目录 使用 configure 命令创建一 makeFile 文件。 直接复制过去运行 ./configur…

考过PMP之后,要不要继续学CSPM?

在7年前拿下了PMP证书&#xff0c;但又在今年报名了CSPM中级的学习&#xff0c;身边很多人都疑问&#xff0c;为什么还要继续花钱考一个新出的证书&#xff1f;是不是闲的没事干&#xff1f;下面跟大家说下我的想法&#xff0c;仅作参考。 1&#xff09;了解项目管理行业的新动…

OpenCV视频车流量识别详解与实践

视频车流量识别基本思想是使用背景消去算法将运动物体从图片中提取出来&#xff0c;消除噪声识别运动物体轮廓&#xff0c;最后&#xff0c;在固定区域统计筛选出来符合条件的轮廓。 基于统计背景模型的视频运动目标检测技术&#xff1a; 背景获取&#xff1a;需要在场景存在…

【Python微信机器人】第二篇:将python注入到其他进程

目录修整 目前的系列目录(后面会根据实际情况变动): 在windows11上编译python将python注入到其他进程并运行使用C写一个python的pyd库&#xff0c;用于实现inline hookPython ctypes库的使用使用ctypes主动调用进程内的任意函数使用汇编引擎调用进程内的任意函数(为了调用不遵…

pdf误删恢复如何恢复?分享4种恢复方法!

如何将pdf误删恢复&#xff1f;使用电脑的时候&#xff0c;经常会需要使用到pdf文件&#xff0c;但是有时候&#xff0c;因为一些操作上的失误&#xff0c;我们会丢失一些重要的文件。如果你不小心将pdf误删了&#xff0c;该如何进行恢复呢&#xff1f; PDF文件丢失的原因可以…

Bertopic 运行中报错记录

1、下载模型报错ConnectionError: (ProtocolError(‘Connection aborted.’, ConnectionResetError(54, ‘Connection reset by peer’)) 运行代码&#xff1a; topics, probabilities model.fit_transform(docs)报错内容&#xff1a; ConnectionError: (ProtocolError(‘C…

【Unity】渲染性能开挂GPU Animation, 动画渲染合批GPU Instance

GPU Instance和SRP Batcher合批渲染只对静态MeshRenerer有效&#xff0c;对SkinMeshRenderer无效。蒙皮动画性能堪忧&#xff0c;对于海量动画物体怎么解决呢&#xff1f;针对这个问题&#xff0c;GPU Animation就是一个常见又简单的解决方案。 GPU动画实现原理&#xff1a; …

【深圳1024开发者城市聚会定向征文】

在这个周末&#xff0c;我有幸参加了1024程序员节活动&#xff0c;这是一个专门为程序员们举办的活动&#xff0c;旨在庆祝程序员这个特殊的群体。在这个活动中&#xff0c;我不仅感受到了浓厚的编程氛围&#xff0c;还收获了许多宝贵的经验和知识。 活动在深圳湾科技生态园举…

漏洞复现--金和OASQL注入

免责声明&#xff1a; 文章中涉及的漏洞均已修复&#xff0c;敏感信息均已做打码处理&#xff0c;文章仅做经验分享用途&#xff0c;切勿当真&#xff0c;未授权的攻击属于非法行为&#xff01;文章中敏感信息均已做多层打马处理。传播、利用本文章所提供的信息而造成的任何直…

坚固可靠的多合一轨道交通天线让您的赏秋路途不再枯燥

今年的法定节假日余额已清零&#xff0c;虽然国庆已过&#xff0c;但秋天正是出游的大好时节。在出游计划中&#xff0c;首先面临的就是交通工具的选择这个大难题&#xff0c;到底是选择自由度更高的自驾前往&#xff1f;还是更省心的公共交通&#xff1f;高铁上的信号质量依旧…

ATA-5510前置微小信号放大器在半导体测试中的具体应用

在现代电子技术领域&#xff0c;半导体器件的测试是非常重要的一环。而前置微小信号放大器在半导体测试中扮演着至关重要的角色。本文将介绍前置微小信号放大器在半导体测试中的原理和应用。 在半导体测试中&#xff0c;通常需要测试非常微弱的信号&#xff0c;这些信号可能受到…

大数据分析平台Splunk Enterprise结合cpolar实现公网远程访问

文章目录 前言1. 搭建Splunk Enterprise2. windows 安装 cpolar3. 创建Splunk Enterprise公网访问地址4. 远程访问Splunk Enterprise服务5. 固定远程地址 前言 Splunk Enterprise是一个强大的机器数据管理平台&#xff0c;可帮助客户分析和搜索数据&#xff0c;以及可视化数据…