【多线程】常见的锁策略

常见的锁策略

  • 1. 乐观锁 vs 悲观锁
  • 2. 读写锁 vs 普通互斥锁
  • 3. 重量级锁 vs 轻量级锁
  • 4. 自旋锁(Spin Lock)vs 挂起等待锁
  • 5. 公平锁 vs 非公平锁
  • 6. 可重入锁 vs 不可重入锁
  • 7. Synchronized
  • 8. 相关面试题

1. 乐观锁 vs 悲观锁

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

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

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

假设我们需要多线程修改 “用户账户余额”.
设当前余额为 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 ,不满足 “提交版本必须大于记录当前版本才能执行更新“ 的乐观锁策略。就认为这次操作失败.

悲观锁预期锁冲突概率很高,做的工作更多,付出成本更多,更低效
乐观锁预期锁冲突概率很低,做的工作更少,付出成本更少,更高效

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

2. 读写锁 vs 普通互斥锁

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

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

其中:

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

读写锁特别适合于 “频繁读, 不频繁写” 的场景中
Synchronized 不是读写锁, 就是普通的互斥锁

3. 重量级锁 vs 轻量级锁

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

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

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

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

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

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

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

重量级锁加锁解锁开销比较大,效率较低,多数情况下悲观锁是重量级锁
轻量级锁加锁解锁开销比较小,效率较高,多数情况下乐观锁是轻量级锁

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

4. 自旋锁(Spin Lock)vs 挂起等待锁

挂起等待锁(典型的重量级锁):线程在抢锁失败后进入阻塞状态,放弃 CPU,需要过很久才能再次被调度.

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

自旋锁伪代码:

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

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

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

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

synchronized 中的轻量级锁策略大概率就是通过自旋锁的方式实现的.

5. 公平锁 vs 非公平锁

公平锁: 遵守 “先来后到”. B 比 C 先来的. 当 A 释放锁的之后, B 就能先于 C 获取到锁.

非公平锁: 不遵守 “先来后到”. B 和 C 都有可能获取到锁.

注意:

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

synchronized 是非公平锁

6. 可重入锁 vs 不可重入锁

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

举个栗子:

        Object locker = new Object();synchronized (locker) {System.out.println("第一次加锁");synchronized (locker) {System.out.println("第二次加锁");}}

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

如果上述这种情况能造成死锁就是不可重入锁, 否则就是可重入锁。

synchronized 是可重入锁

7. Synchronized

  1. 既是乐观锁,又是悲观锁, 当锁冲突比较激烈时,从乐观锁变为悲观锁
  2. 既是轻量级锁,又是重量级锁,
  3. Synchronized的轻量级锁基于自旋锁实现, 重量级锁基于挂起等待锁实现
  4. 不是读写锁, 只是普通的互斥锁
  5. 是可重入锁
  6. 是非公平锁

8. 相关面试题

  1. 你是怎么理解乐观锁和悲观锁的,具体怎么实现呢?
  • 悲观锁认为多个线程访问同一个共享变量冲突的概率较大, 会在每次访问共享变量之前都去真正加锁.

  • 乐观锁认为多个线程访问同一个共享变量冲突的概率不大. 并不会真的加锁, 而是直接尝试访问数据. 在访问的同时识别当前的数据是否出现访问冲突.

  • 悲观锁的实现就是先加锁(比如借助操作系统提供的 mutex), 获取到锁再操作数据. 获取不到锁就等待.

  • 乐观锁的实现可以引入一个版本号. 借助版本号识别出当前的数据访问是否冲突.

  1. 介绍下读写锁?
  • 读写锁就是把读操作和写操作分别进行加锁.
  • 读锁和读锁之间不互斥.
  • 写锁和写锁之间互斥.
  • 写锁和读锁之间互斥.

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

  1. 什么是自旋锁,为什么要使用自旋锁策略呢,缺点是什么?
  • 如果获取锁失败, 立即再尝试获取锁, 无限循环, 直到获取到锁为止. 第一次获取锁失败, 第二次的尝试会在极短的时间内到来. 一旦锁被其他线程释放, 就能第一时间获取到锁.

相比于挂起等待锁,

  • 优点: 没有放弃 CPU 资源, 一旦锁被释放就能第一时间获取到锁, 更高效. 在锁持有时间比较短的场景下非常有用.
  • 缺点: 如果锁的持有时间较长, 就会浪费 CPU 资源.
  1. synchronized 是可重入锁么?
  • 是可重入锁.
  • 可重入锁指的就是连续两次加锁不会导致死锁.
    实现的方式是在锁中记录该锁持有的线程身份, 以及一个计数器(记录加锁次数). 如果发现当前加锁的线程就是持有锁的线程, 则直接计数自增.

好啦! 以上就是对 常见的锁策略 的讲解,希望能帮到你 !
评论区欢迎指正 !

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

obsstudio下载使用

官网 Open Broadcaster Software | OBS 介绍 OBS是Open Broadcaster Software的简称,是一款开源,用于视频录制以及直播串流的软件,它支持Windows、Mac以及Linux操作系统。OBS使用容易、操作简单,对于新手小白来说非常友好。如果…

Python的pandas库来实现将Excel文件转换为JSON格式的操作

✅作者简介:2022年博客新星 第八。热爱国学的Java后端开发者,修心和技术同步精进。 🍎个人主页:Java Fans的博客 🍊个人信条:不迁怒,不贰过。小知识,大智慧。 💞当前专栏…

STM32纯中断方式发送接收数据(串行通信;keil arm5;)

除了main文件其他文件均无修改,正常运行--在keil arm5内

[.NET 6] IHostedService 的呼叫等等我的爱——等待Web应用准备就绪

📢欢迎点赞 :👍 收藏 ⭐留言 📝 如有错误敬请指正,赐人玫瑰,手留余香!📢本文作者:由webmote 原创📢作者格言:新的征程,我们面对的不是技术而是人心,人心不可测,海水不可量,唯有技术,才是深沉黑夜中的一座闪烁的灯塔 !序言 在这篇文章中,我将介绍如何等…

使用maven idea环境

目录 idea三种方式执行maven命令 工程导入 生命周期lifecycle 插件和目标 常用命令 创建模块工程后 idea三种方式执行maven命令 想在哪个工程模块上执行就点开哪一个 如果觉得双击完clean再双击install麻烦,可以 如果有需要还可以给命令后面加参数 ​​​ 第三种…

第27章_瑞萨MCU零基础入门系列教程之freeRTOS实验

本教程基于韦东山百问网出的 DShanMCU-RA6M5开发板 进行编写,需要的同学可以在这里获取: https://item.taobao.com/item.htm?id728461040949 配套资料获取:https://renesas-docs.100ask.net 瑞萨MCU零基础入门系列教程汇总: ht…

使用Java登录校验

会话技术 会话:用户打开浏览器,访问web服务器的资源,会话建立,直到有一方断开连接,会话结束。在一次会话可以包含多次请求和响应。 会话跟踪:一种维护浏览器状态的方法,服务器需要识别多次请求是…

iptables 目标地址转换

目录 一、实验准备 二、配置web服务器 三、配置web防火墙网卡 四、配置客户机网卡 五、测试 1、开启防火墙功能,设置源地址转换,通过改变我客户机的地址身份为web服务器同网段来实现访问 2、通过改变目标地址(客户机)的地址…

算法通关村18关 | 透析回溯的模板

回溯有清晰的解题模板, void backtracking(参数){if (终止条件){存放结果;return;}for (选择本层中的集合元素(画成树,就是树节点孩子的大小) {处理节点;backtracking();回溯,撤销处理结果;}} 1. 从N叉树说起 在回溯之前&#x…

【C++】常用排序算法

0.前言 1.sort #include <iostream> using namespace std;// 常用排序算法 sort #include<vector> #include<algorithm>//利用仿函数 打印输出 class myPrint { public:void operator()(int val){cout << val << " ";} };//利用普通函…

OSCP系列靶场-Esay-Vegeta1保姆级

OSCP系列靶场-Esay-Vegeta1保姆级 目录 OSCP系列靶场-Esay-Vegeta1保姆级总结准备工作信息收集-端口扫描目标开放端口收集目标端口对应服务探测 信息收集-端口测试22-SSH端口的信息收集22-SSH端口版本信息与MSF利用22-SSH协议支持的登录方式22-SSH手动登录尝试(无)22-SSH弱口令…

GDB的C++调试方法

本文记录基础的GDB调试过程&#xff0c;包含指令如下&#xff1a; 文章目录 准备编译文件GDB启动GDB开启代码行设置断点运行程序查看pc的指令查看监视的变量以及断点设置快照checkpoint实验1实验2 nextnextistepx/i $pcfinishinfo break 和 delete Numrefbreak col. if condit…