当前位置: 首页 > news >正文

[多线程进阶] 常见锁策略


专栏简介: JavaEE从入门到进阶

题目来源: leetcode,牛客,剑指offer.

创作目标: 记录学习JavaEE学习历程

希望在提升自己的同时,帮助他人,,与大家一起共同进步,互相成长.

学历代表过去,能力代表现在,学习能力代表未来! 


目录:

1. 常见的锁策略

1.1 乐观锁 vs 悲观锁

1.2 读写锁:

1.3 重量级锁 vs 轻量级锁

1.4 自旋锁(Spin Lock)

1.5 公平锁 vs 非公平锁

1.6 可重入锁 vs 不可重入锁

          1.7 相关面试题


1. 常见的锁策略

锁策略之所以被叫做策略 , 是因为它并不是一个具体的锁 , 而是一系列供锁的实现者来参考的特性 , 对普通程序猿合理的使用锁也是有很大的帮助.

1.1 乐观锁 vs 悲观锁

乐观锁:

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

悲观锁:

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

例如: 同学A 和同学B 想请教老师一个问题:

  • 同学A 认为老师一定是比较忙的 , 因此他会先给老师法消息: "老师忙吗? 我上午10点向您请教一个问题."(相当于加锁操作)得到肯定回复后才会来 , 如果得到否定回复 , 那就等一段时间 , 下次再问老师. 这是个悲观锁
  • 同学B 认为老师一定是比较闲的 , 因此他会直接去找老师(没加锁 , 直接访问资源) , 如果老师确实比较闲 , 那么问题就解决了. 如果老师比较忙 , 那么也不会打扰老师下次再来(虽然每加锁 , 但能识别出数据访问冲突). 这是个乐观锁.

这两种思路的优劣要看具体的实现场景:

  • 如果当前老师确实比较忙 , 那么就适合使用悲观锁 , 使用乐观锁会导致"白跑很多趟" , 耗费额外的资源.
  • 如果当前老师比较闲 , 那么就适合使用乐观锁 , 使用悲观锁锁让效率

Synchronized 初始使用乐观锁策略 , 当发生锁竞争比较频繁时 , 就会自动切换成悲观锁策略.

同学C (相当于Synchronized)  , 开始认为"老师应该是比较闲的" , 有问题会直接去问老师.

但直接来找老师几次后 , 发现老师都挺忙的 , 于是下次来问老师会先发消息 , 在决定是否来问问题.


 乐观锁的一个重要功能就是检测出数据是发生访问冲突 , 我们可以引入一个版本号来解决.

假设需要多线程来修改"账户余额"

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

1) 线程 A 此时读出信息( version = 1 , balance = 100) , 线程 B 也读出信息(version = 1 , balance = 100)

2) 线程 A 操作的过程中并从其账户中扣除 50 (100-50) , 线程 B 从其账户中扣除 20(100-20).

3) 线程 A 完成修改工作 , 将数据版本号+1(version = 2) , 连同账户扣除余额(balance = 50)写到内存中.

4) 线程 B 完成操作 , 也将版本号+1(version = 2) , 尝试向内存中提交数据(balance = 80) , 但通过对比版本号发现 , 操作员 B 提交的数据版本号为 2 , 数据库记录的版本号也为 2 , 不满足"提交版本号必须大于记录当前版本才能执行更新" 的乐观锁策略. 于是认为这次操作失败.


1.2 读写锁:

多线程之间 , 数据的读取方之间不会产生线程安全 , 但数据的写入方之间以及读者之间都需要进行互斥 , 如果两种情况下都用同一个锁 , 就会产生极大的性能损耗 , 所以读写锁因此而产生.

读写锁(readers-writer lock) , 顾名思义 , 在执行加锁操作时需要额外表明读写意图 , 读者之间并不互斥 , 而写者则要求与任何人互斥.

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

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

其中:

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

Tips: 只要涉及到"互斥" , 就存在线程的挂起等待 , 一但线程被挂起 , 再次调用就不知在什么时候 , 因此尽可能的减少"互斥"的机会 , 就是提高效率的重要途径.

Synchronized 不是读写锁.


1.3 重量级锁 vs 轻量级锁

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

  • CPU 提供了"原子操作指令".
  • 操作系统基于 CPU 的原子指令 , 实现了 mutex 互斥锁.
  • JVM 基于操作系统提供的互斥锁 , 实现 synchronized ReentrantLock 等关键字和类.

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

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

这两个操作 , 成本比较高 , 一但涉及到用户态内核态的切换 , 就意味着"沧海桑田".

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

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

用户态 vs 内核态

假设去银行办理业务:

在窗口外 , 自己在 ATM 机办理业务就相当于用户态 , 用户态的时间成本是比较可控的.

在窗口内 , 工作人员办理 , 就是内核态 , 内核态的时间成本不可控.

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

synchronized 开始是一个轻量级锁 , 如果锁冲突较为严重 , 就会变成重量级锁. synchronized的轻量级锁是基于自旋锁实现的 , 重量级锁是基于挂起等待锁实现的.


1.4 自旋锁(Spin Lock)

按照上文的结论 , 线程在强锁失败后会进入阻塞状态 , 放弃 CPU , 需要过很久才能再次被调度.

但实际情况下 , 虽然强锁失败 , 但过不了多久锁就会被释放 , 没必要放弃 CPU , 此时就需要自旋锁来处理这样的问题.

自旋锁伪代码:

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

如果获取锁失败 , 立即再次尝试获取锁 , 知道获取锁为止 , 第一次获取锁失败 , 第二次的尝试会在极短的时间内到来.

因此 , 一但锁被其他线程释放 ,  就能第一时间获取锁.

自旋锁 vs 挂起等待锁

当小明去找老师问题 , 老师说: 稍等一会 , 这会已经正在给其他同学讲题.

挂起等待锁: 回去干自己的事 , 过了很久很久之后 , 老师突然发来消息 , "这会有空闲时间"(注意 , 这个很长的时间间隔里 , 老师可能已经给多个同学讲完题了)

自旋锁: 站在老师办公室门口 , 一旦上个同学出来 , 那么就能立即抓住机会问题.

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

  • 优点: 没有放弃 CPU , 不涉及线程的阻塞的调度 , 一旦锁被释放 , 就能第一时间获取到锁
  • 缺点: 如果锁被其他线程占用时间过长 , 那么就会持续的消耗 CPU 资源.(挂起等待的时候不消耗 CPU 资源)

synchronized 中的轻量级锁就是通过自旋锁的方式形成.


1.5 公平锁 vs 非公平锁

假设有三个线程 A , B , C. A成功获取锁 , B和C都尝试获取锁 , 但获取失败 , 阻塞等待. 那么当 A 线程释放锁时 , 会发生什么?

公平锁: 遵循"先来后到". B 比 C 先来 , 那么当 A 释放后 , B 就可能先于 C 获得锁.

非公平锁:不遵循"先来后到". B 和 C 都有可能获得锁.

Tips:

  • 操作系统内部的线程调度就是随机的 , 如果不做任何额外的限制 , 锁就是非公平锁. 如果想要实现公平锁 , 就需要依赖额外的数据结构. 记录线程的先后顺序.
  • 公平锁与非公平锁没有好坏之分 , 关键在于适用场景.

synchronized 是非公平锁.


1.6 可重入锁 vs 不可重入锁

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

例如一个递归函数中有加锁操作 , 递归过程中如果锁不会阻塞自己 , 那么这个锁就是可重入锁(递归锁).

Java 中只要是以 Reentrant 开头的都是可重入锁 , 而且 JDK提供的所有现成的Lock实现类 , 包括synchronized 关键字都是可重入的.

而 LInux 系统提供的 mutex 是不可重入锁.

Synchronized 是可重入锁.


1.7 相关面试题

1. 你是怎么理解乐观锁和悲观锁的 , 具体怎么实现?

  • 悲观锁认为多个线程访问同一个共享变量冲突概率较大 , 会在每次访问共享变量前真正加锁.
  • 乐观锁认为多个线程访问同一个共享变量的冲突概率不大 , 并不会真的加锁 , 而是直接尝试访问数据. 在访问的同时识别当前数据是否发生访问冲突.
  • 乐观锁的实现可以引入一个版本号 , 借助版本号识别当前数据是否访问冲突.

2. 介绍下读写锁?

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

读操作和读操作之间不互斥.

读操作和写操作之间互斥.

写操作和写操作之间互斥.

读写锁的最长用场景就是 "频繁读 , 不频繁写".

3. 什么是自旋锁 , 为什么要使用自旋锁策略 , 缺点是什么?

自旋锁是一种轻量级锁 , 如果获取锁失败 , 会无限循环不停的获取锁 , 直到获取到锁为止 , 因此一但锁被其他线程释放可以第一时间获取.

相比于挂起等待锁:

优点: 没有放弃 CPU 资源 , 一但锁被释放就能第一时间获取到锁 ,  更高效. 在锁持有时间比较短的情况下非常高效.

缺点: 如果锁长时间的持有机会浪费 CPU 资源.

4. synchronized 是可重入锁吗?

是可重入锁 , 可重入锁指的是连续两次加锁不会导致死锁.

实现方式是在锁中记录持有该锁的线程身份 , 以及一个计数器(记录加锁的次数) , 如果发现当前加锁的线程就是持有锁的线程 , 则直接计数器自增.

相关文章:

[多线程进阶] 常见锁策略

专栏简介: JavaEE从入门到进阶 题目来源: leetcode,牛客,剑指offer. 创作目标: 记录学习JavaEE学习历程 希望在提升自己的同时,帮助他人,,与大家一起共同进步,互相成长. 学历代表过去,能力代表现在,学习能力代表未来! 目录: 1. 常见的锁策略 1.1 乐观锁 vs 悲观锁 1.2 读写…...

Scala - Idea 项目报错 Cannot resolve symbol XXX

一.引言 Idea 编译 Scala 项目大面积报错 Cannot resolve symbol xxx。 二.Cannot resolve symbol xxx 1.问题描述 Idea 内的 Scala 工程打开后显示下述异常: 即 Scala 常规语法全部失效,代码出现大面积红色报错。 2.尝试解决方法 A.设置 Main Sourc…...

信息化发展与应用的新特点

一、信息化发展与应用二、国家信息化发展战略三、电子政务※四、电子商务五、两化融合(工业和信息化)六、智慧城市 一、信息化发展与应用 我国在“十三五”规划纲要中,将培育人工智能、移动智能终端、第五代移动通信(5G)先进传感器等作为新…...

软件测试】测试时间不够了,我很慌?项目马上发布了......

目录:导读前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结(尾部小惊喜)前言 常见的几种情况&…...

MapReduce编程规范

MapReduce编程规范 MapReduce的开发一共有八个步骤,其中Map阶段分为2个步骤,Shuffle阶段4个步骤,Reduce阶段分为2个步骤。 Map阶段2个步骤 设置InputFormat类,将数据切分为Key-Value(K1和V1)对,输入到第二步。 自定义Map逻辑,将第一步的结果转换成另外的…...

Unity 如何实现游戏Avatar角色头部跟随视角转动

文章目录功能简介实现步骤获取看向的位置获取头部的位置修改头部的朝向限制旋转角度超出限制范围时自动回正如何让指定动画不受影响功能简介 如图所示,当相机的视角转动时,Avatar角色的头部会同步转动,看向视角的方向。 实现步骤 获取看向的…...

深度学习优化算法总结

深度学习的优化算法 优化的目标 优化提供了一种最大程度减少深度学习损失函数的方法,但本质上,优化和深度学习的目标不同。 优化关注的是最小化目标;深度学习是在给定有限数据量的情况下寻找合适的模型。 优化算法 gradient descent&#xf…...

CMake详细使用

1、CMake简介CMake是一个用于管理源代码的跨平台构建工具可以方便地根据目标平台和编译工具产生对应的编译文件主要用于C/C语言的构建,但是也可以用于其它编程语言的源代码。如同使用make命令工具解析Makefile文件一样cmake命令工具依赖于一个CMakeLists.txt的文件该…...

【数据结构与算法】前缀树的实现

🌠作者:阿亮joy. 🎆专栏:《数据结构与算法要啸着学》 🎇座右铭:每个优秀的人都有一段沉默的时光,那段时光是付出了很多努力却得不到结果的日子,我们把它叫做扎根 目录👉…...

canvas 制作2048

效果展示 对UI不满意可以自行调整,这里只是说一下游戏的逻辑,具体的API调用不做过多展示。 玩法分析 2048 的玩法非常简单,通过键盘的按下,所有的数字都向着同一个方向移动,如果出现两个相同的数字,就将…...

playwright: 全局修改页面等待超时时间

等待超时时间默认是30s, 可以通过以下几个方法设置: browser_context.set_default_navigation_timeout()browser_context.set_default_timeout()page.set_default_navigation_timeout()page.set_default_timeout() set_default_navigation_timeout set_default_n…...

C++类和对象(中)

✨个人主页: Yohifo 🎉所属专栏: C修行之路 🎊每篇一句: 图片来源 I do not believe in taking the right decision. I take a decision and make it right. 我不相信什么正确的决定。我都是先做决定,然后把…...

Docker安装EalasticSearch、Kibana,安装Elasticvue插件

使用Docker快速安装部署ES和Kibana的前提:首先需要确保已经安装了Docker环境。 如果没有安装Docker的话,先在Linux上安装Docker。 有了Docker环境后,就可以使用Docker安装部署ES和Kibana了 一、安装ES 1、拉取EalasticSearch镜像 docker p…...

算法训练营 day39 贪心算法 无重叠区间 划分字母区间 合并区间

算法训练营 day39 贪心算法 无重叠区间 划分字母区间 合并区间 无重叠区间 435. 无重叠区间 - 力扣(LeetCode) 给定一个区间的集合 intervals ,其中 intervals[i] [starti, endi] 。返回 需要移除区间的最小数量,使剩余区间互…...

c/c++开发,无可避免的文件访问开发案例

一、缓存文件系统 ANSI C标准中的C语言库提供了fopen, fclose, fread, fwrite, fgetc, fgets, fputc, fputs, freopen, fseek, ftell, rewind等标准函数,这些函数在不同的操作系统中应该调用不同的内核API,从而支持开发者跨平台实现对文件的访问。 在Lin…...

MySQL学习笔记

MySQL学习笔记一、基础配置二、数据库操作三、表的操作1.创建表2.表选项3.查看表4.修改表5.删除表6.复制表7.检查优化修复表四、数据操作基础增删改查五、字符集编码六、数据类型(列类型)1.数值类型2.字符串类型3.日期时间类型4.枚举和集合七、列属性&am…...

ccs导入工程失败的处理方法

文章目录当导入CCS新工程时出现下述错误怎么办?方法一 从TI官网下载安装包进行安装,下载链接:软件下载完成 安装路径为上面的文件夹点击安装完成后,导入安装路径,并点击Refresh按钮,依据路径进行更新&#…...

探针台常见的故障及解决方法

症状、 可能原因、 解决方法 移动样品后画面变模糊 —显微镜不垂直,调垂直显微镜 样品台不水平 —调水平样品台 显微镜视场亮度不足,边缘切割或看不到像—转换器不在定位位置上 把转换器转到定位位置上 管镜转盘不在定位位置上 —把管镜转盘转到定…...

域内资源探测

✅作者简介:CSDN内容合伙人、信息安全专业在校大学生🏆 🔥系列专栏 :内网安全 📃新人博主 :欢迎点赞收藏关注,会回访! 💬舞台再大,你不上台,永远是…...

c# 将数据导出到EXCEL文件

第一步:项目中加入引用。 在鼠标右击项目,点击【添加】弹出菜单列表,选择【项目引用】弹出【引用管理器】对话框,选择【COM】-【Microsoft Excel 16.0 Object Library】,如图所示: 第二步,编辑…...

uniapp 对接腾讯云IM群组成员管理(增删改查)

UniApp 实战:腾讯云IM群组成员管理(增删改查) 一、前言 在社交类App开发中,群组成员管理是核心功能之一。本文将基于UniApp框架,结合腾讯云IM SDK,详细讲解如何实现群组成员的增删改查全流程。 权限校验…...

C++:std::is_convertible

C++标志库中提供is_convertible,可以测试一种类型是否可以转换为另一只类型: template <class From, class To> struct is_convertible; 使用举例: #include <iostream> #include <string>using namespace std;struct A { }; struct B : A { };int main…...

Linux简单的操作

ls ls 查看当前目录 ll 查看详细内容 ls -a 查看所有的内容 ls --help 查看方法文档 pwd pwd 查看当前路径 cd cd 转路径 cd .. 转上一级路径 cd 名 转换路径 …...

电脑插入多块移动硬盘后经常出现卡顿和蓝屏

当电脑在插入多块移动硬盘后频繁出现卡顿和蓝屏问题时&#xff0c;可能涉及硬件资源冲突、驱动兼容性、供电不足或系统设置等多方面原因。以下是逐步排查和解决方案&#xff1a; 1. 检查电源供电问题 问题原因&#xff1a;多块移动硬盘同时运行可能导致USB接口供电不足&#x…...

C++ 基础特性深度解析

目录 引言 一、命名空间&#xff08;namespace&#xff09; C 中的命名空间​ 与 C 语言的对比​ 二、缺省参数​ C 中的缺省参数​ 与 C 语言的对比​ 三、引用&#xff08;reference&#xff09;​ C 中的引用​ 与 C 语言的对比​ 四、inline&#xff08;内联函数…...

unix/linux,sudo,其发展历程详细时间线、由来、历史背景

sudo 的诞生和演化,本身就是一部 Unix/Linux 系统管理哲学变迁的微缩史。来,让我们拨开时间的迷雾,一同探寻 sudo 那波澜壮阔(也颇为实用主义)的发展历程。 历史背景:su的时代与困境 ( 20 世纪 70 年代 - 80 年代初) 在 sudo 出现之前,Unix 系统管理员和需要特权操作的…...

CMake 从 GitHub 下载第三方库并使用

有时我们希望直接使用 GitHub 上的开源库,而不想手动下载、编译和安装。 可以利用 CMake 提供的 FetchContent 模块来实现自动下载、构建和链接第三方库。 FetchContent 命令官方文档✅ 示例代码 我们将以 fmt 这个流行的格式化库为例,演示如何: 使用 FetchContent 从 GitH…...

关于 WASM:1. WASM 基础原理

一、WASM 简介 1.1 WebAssembly 是什么&#xff1f; WebAssembly&#xff08;WASM&#xff09; 是一种能在现代浏览器中高效运行的二进制指令格式&#xff0c;它不是传统的编程语言&#xff0c;而是一种 低级字节码格式&#xff0c;可由高级语言&#xff08;如 C、C、Rust&am…...

浅谈不同二分算法的查找情况

二分算法原理比较简单&#xff0c;但是实际的算法模板却有很多&#xff0c;这一切都源于二分查找问题中的复杂情况和二分算法的边界处理&#xff0c;以下是博主对一些二分算法查找的情况分析。 需要说明的是&#xff0c;以下二分算法都是基于有序序列为升序有序的情况&#xf…...

Linux 中如何提取压缩文件 ?

Linux 是一种流行的开源操作系统&#xff0c;它提供了许多工具来管理、压缩和解压缩文件。压缩文件有助于节省存储空间&#xff0c;使数据传输更快。本指南将向您展示如何在 Linux 中提取不同类型的压缩文件。 1. Unpacking ZIP Files ZIP 文件是非常常见的&#xff0c;要在 …...