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

【多线程-进阶】常⻅的锁策略

多线程-进阶

  • 常⻅的锁策略
    • 乐观锁 vs 悲观锁
    • 重量级锁 vs 轻量级锁
    • ⾃旋锁 vs 挂起等待锁
    • 公平锁 vs ⾮公平锁
    • 可重⼊锁 vs 不可重⼊锁
    • 读写锁 和 普通互斥锁
  • 相关⾯试题
    • synchronized
      • 锁升级
      • 锁消除
      • 锁粗化

多线程初阶~~ 线程的基础知识, 面试要考 +工作要用的
接下来的 多线程进阶,主要讲的是 面试要考的(工作中不太用到)
讨论的每个话题,都是历年比较高频的面试真题~~

常⻅的锁策略

注意: 接下来讲解的锁策略不仅仅是局限于 Java . 任何和 “锁” 相关的话题, 都可能会涉及到以下内容.这些特性主要是给锁的实现者来参考的.(这里的锁策略,主要是"设计锁"的人需要理解. 咱们一般只是"使用锁”,正常来说不需要关注的,但是面试要考,只能也去了解一下~~) 普通的程序猿也需要了解⼀些, 对于合理的使⽤锁也是有很⼤帮助的.
加锁过程中,处理冲突的过程中,涉及到的一些不同的处理方式

乐观锁 vs 悲观锁

悲观锁:
总是假设最坏的情况,每次去拿数据的时候都认为别⼈会修改,所以每次在拿数据的时候都会上锁,这样别⼈想拿这个数据就会阻塞直到它拿到锁。
乐观锁
假设数据⼀般情况下不会产⽣并发冲突,所以在数据进⾏提交更新的时候,才会正式对数据是否产⽣并发冲突进⾏检测,如果发现并发冲突了,则让返回⽤⼾错误的信息,让⽤⼾决定如何去做。
在这里插入图片描述

举个栗⼦: 同学 A 和 同学 B 想请教⽼师⼀个问题.
同学 A 认为 “⽼师是⽐较忙的, 我来问问题, ⽼师不⼀定有空解答”. 因此同学 A 会先给⽼师发消息: “⽼师你忙嘛? 我下午两点能来找你问个问题嘛?” (相当于加锁操作) 得到肯定的答复之后, 才会真的来问问题.如果得到了否定的答复, 那就等⼀段时间, 下次再来和⽼师确定时间. 这个是悲观锁.
同学 B 认为 “⽼师是⽐较闲的, 我来问问题, ⽼师⼤概率是有空解答的”. 因此同学 B 直接就来找⽼师.(没加锁, 直接访问资源) 如果⽼师确实⽐较闲, 那么直接问题就解决了. 如果⽼师这会确实很忙, 那么同学 B也不会打扰⽼师, 就下次再来(虽然没加锁, 但是能识别出数据访问冲突). 这个是乐观锁.
这两种思路不能说谁优谁劣, ⽽是看当前的场景是否合适.
如果当前⽼师确实⽐较忙, 那么使⽤悲观锁的策略更合适, 使⽤乐观锁会导致 “⽩跑很多趟”, 耗费额外的资源.
如果当前⽼师确实⽐较闲, 那么使⽤乐观锁的策略更合适, 使⽤悲观锁会让效率⽐较低.

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

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

重量级锁 vs 轻量级锁

锁的核⼼特性 “原⼦性”, 这样的机制追根溯源是 CPU 这样的硬件设备提供的.
• CPU 提供了 “原⼦操作指令”.
• 操作系统基于 CPU 的原⼦指令, 实现了 mutex 互斥锁.
• JVM 基于操作系统提供的互斥锁, 实现了 synchronized 和 ReentrantLock 等关键字和类.
在这里插入图片描述

注意, synchronized 并不仅仅是对 mutex 进⾏封装, 在 synchronized 内部还做了很多其他的⼯作

重量级锁: 加锁机制重度依赖了 OS 提供了 mutex
• ⼤量的内核态⽤⼾态切换
• 很容易引发线程的调度

这两个操作, 成本⽐较⾼. ⼀旦涉及到⽤⼾态和内核态的切换, 就意味着 “沧海桑⽥”.

轻量级锁: 加锁机制尽可能不使⽤ mutex, ⽽是尽量在⽤⼾态代码完成. 实在搞不定了, 再使⽤ mutex.
• 少量的内核态⽤⼾态切换.
• 不太容易引发线程调度.

理解⽤⼾态 vs 内核态
想象去银⾏办业务.
在窗⼝外, ⾃⼰做, 这是⽤⼾态. ⽤⼾态的时间成本是⽐较可控的.
在窗⼝内, ⼯作⼈员做, 这是内核态. 内核态的时间成本是不太可控的.
如果办业务的时候反复和⼯作⼈员沟通, 还需要重新排队, 这时效率是很低的.

在这里插入图片描述
synchronized 开始是⼀个轻量级锁. 如果锁冲突⽐较严重, 就会变成重量级锁.

⾃旋锁 vs 挂起等待锁

⾃旋锁(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 是不可重⼊锁.

理解 “把⾃⼰锁死”
⼀个线程没有释放锁, 然后⼜尝试再次加锁.

// 第⼀次加锁, 加锁成功
lock();
// 第⼆次加锁, 锁已经被占⽤, 阻塞等待. 
lock();

按照之前对于锁的设定, 第⼆次加锁的时候, 就会阻塞等待. 直到第⼀次的锁被释放, 才能获取到第⼆个
锁. 但是释放第⼀个锁也是由该线程来完成, 结果这个线程已经躺平了, 啥都不想⼲了, 也就⽆法进⾏解
锁操作. 这时候就会 死锁.
在这里插入图片描述
这样的锁称为 不可重⼊锁.

在这里插入图片描述

synchronized 是可重⼊锁

读写锁 和 普通互斥锁

普通互斥锁
类似于 synchronized,操作涉及到 加锁 和 解锁
读写锁
把加锁分成两种情况
1)加读锁
2)加写锁

多线程之间,数据的读取⽅之间不会产⽣线程安全问题,但数据的写⼊⽅互相之间以及和读者之间都需要进⾏互斥。如果两种场景下都⽤同⼀个锁,就会产⽣极⼤的性能损耗。所以读写锁因此⽽产⽣。
读写锁(readers-writer lock),看英⽂可以顾名思义,在执⾏加锁操作时需要额外表明读写意图,复数读者之间并不互斥,⽽写者则要求与任何⼈互斥。
⼀个线程对于数据的访问, 主要存在两种操作: 读数据 和 写数据.
• 两个线程都只是读⼀个数据, 此时并没有线程安全问题. 直接并发的读取即可.
• 两个线程都要写⼀个数据, 有线程安全问题.
• ⼀个线程读另外⼀个线程写, 也有线程安全问题.
读写锁就是把读操作和写操作区分对待. Java 标准库提供了 ReentrantReadWriteLock 类, 实现了读写锁
• ReentrantReadWriteLock.ReadLock 类表⽰⼀个读锁. 这个对象提供了 lock / unlock ⽅法进⾏加锁解锁.
• ReentrantReadWriteLock.WriteLock 类表⽰⼀个写锁. 这个对象也提供了 lock / unlock⽅法进⾏加锁解锁.
其中,
• 读加锁和读加锁之间, 不互斥.
• 写加锁和写加锁之间, 互斥.
• 读加锁和写加锁之间, 互斥.
一个线程加读锁的时候,另一个线程,只能读,不能写;一个线程加写锁的时候,另一个线程,不能写,也不能读

注意, 只要是涉及到 “互斥”, 就会产⽣线程的挂起等待. ⼀旦线程挂起, 再次被唤醒就不知道隔了多久了.
因此尽可能减少 “互斥” 的机会, 就是提⾼效率的重要途径.

为啥要引入读写锁??

在这里插入图片描述

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

⽐如教务系统.
每节课⽼师都要使⽤教务系统点名, 点名就需要查看班级的同学列表(读操作). 这个操作可能要每周执⾏好⼏次.
⽽什么时候修改同学列表呢(写操作)? 就新同学加⼊的时候. 可能⼀个⽉都不必改⼀次.
再⽐如, 同学们使⽤教务系统查看作业(读操作), ⼀个班级的同学很多, 读操作⼀天就要进⾏⼏⼗次上百次.
但是这⼀节课的作业, ⽼师只是布置了⼀次(写操作)

Synchronized 不是读写锁.

相关⾯试题

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

悲观锁认为多个线程访问同⼀个共享变量冲突的概率较⼤, 会在每次访问共享变量之前都去真正加锁.
乐观锁认为多个线程访问同⼀个共享变量冲突的概率不⼤. 并不会真的加锁, ⽽是直接尝试访问数据. 在访问的同时识别当前的数据是否出现访问冲突.
悲观锁的实现就是先加锁(⽐如借助操作系统提供的 mutex), 获取到锁再操作数据. 获取不到锁就等待.
乐观锁的实现可以引⼊⼀个版本号. 借助版本号识别出当前的数据访问是否冲突. (实现细节参考上⾯的图).

  1. 介绍下读写锁?

读写锁就是把读操作和写操作分别进⾏加锁.
读锁和读锁之间不互斥.
写锁和写锁之间互斥.
写锁和读锁之间互斥.
读写锁最主要⽤在 “频繁读, 不频繁写” 的场景中.

  1. 什么是⾃旋锁,为什么要使⽤⾃旋锁策略呢,缺点是什么?

如果获取锁失败, ⽴即再尝试获取锁, ⽆限循环, 直到获取到锁为⽌. 第⼀次获取锁失败, 第⼆次的尝试会在极短的时间内到来. ⼀旦锁被其他线程释放, 就能第⼀时间获取到锁.
相⽐于挂起等待锁,
优点: 没有放弃 CPU 资源, ⼀旦锁被释放就能第⼀时间获取到锁, 更⾼效. 在锁持有时间⽐较短的场景下⾮常有⽤.
缺点: 如果锁的持有时间较⻓, 就会浪费 CPU 资源.

  1. synchronized 是可重⼊锁么?

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

synchronized

上述"锁策略"就是名词解释,针对这些词,有一个概念上的认识即可
面试官进一步延伸问,一般还是基于某个特定的锁(synchronized)
原则上说, 回答问题的时候,越细越好!! 面试官问 1,你能回答出3,最合适的!!
在这里插入图片描述
synchronized 内部的工作原理
在这里插入图片描述
synchronized 背后涉及到了很多的"优化手段"
在这里插入图片描述

锁升级

synchronized 的加锁过程,尤其是"自适应"是咋回事
当线程执行到 synchronized 的时候,如果这个对象当前处于未加锁的状态, 就会经历以下过程~~

  1. 偏向锁阶段
    在这里插入图片描述
  2. 轻量级锁阶段
    在这里插入图片描述
  3. 重量级锁阶段
    在这里插入图片描述

此处锁 只能 升级,不能降级.自适应这个词,严格的说不算很严谨
不可考~~ 保不齐未来某个版本就能降级了

锁消除

也是 synchronized 中内置的优化策略
编译器优化的一种方式.编译器编译代码的时候,如果发现这个代码,不需要加锁,就会自动把锁给干掉
在这里插入图片描述

锁粗化

会把多个细粒度的锁,合并成一个粗粒度的锁
synchronized { } 大括号里 包含的代码越少,就认为锁的粒度越细;包含的代码越多,就认为锁的粒度越粗
通常情况下,是更偏好于让锁的粒度细一些,更有利于多个线程并发执行的~~ 但是有的时候,是希望锁的粒度粗点也挺好
在这里插入图片描述
小结:
synchronized 背后涉及到了很多的"优化手段"
在这里插入图片描述

http://www.dtcms.com/a/450108.html

相关文章:

  • 通过类比理解TCP\IP五层协议
  • R脚本--PCA分析系列1_v1.0
  • 大模型面试题剖析:深入解析 Transformer 与 MoE 架构
  • wordpress首页没有显示文章图片绵阳网站建设优化
  • VR大空间资料 04 —— VRAF使用体验和源码分析
  • LabVIEW定时循环中止功能
  • 南昌中企动力做的网站怎么样宁波妇科
  • Async++ 源码分析10--ref_count.h
  • 单页面竞价网站网站+建设设计
  • 基于MATLAB的物理层算法原型验证
  • PHP网站开发程序员招聘一站式做网站哪家专业
  • 绵阳网站建设哪家好微信下拉小程序怎么关闭
  • 软件设计师——08 算法设计与分析
  • 炫酷企业网站网上买东西有哪些平台
  • DAY 42 Grad-CAM与Hook函数-2025.10.6
  • 绵阳网站建设培训学校隐私空间
  • 淮安网站建设做北京电梯招标的网站
  • 专业企业网站建设定制百度如何做网站
  • Net-Tools工具包详解:Linux网络管理经典工具集
  • 极路由做网站无锡网站推广公司排名
  • registrateAPI——非空函数
  • 环境设计案例网站基于html5动画的网站
  • CCF编程能力等级认证GESP—C++4级—20250927
  • 网站收录率怎样建立自己网站多少钱
  • 电商平台网站设计公司企业建站搭建
  • 【数据结构】链栈的基本操作
  • 实战分享:股票数据API接口在量化分析中的应用与体验
  • 个人建设网站还要备案么wordpress建站详细教程视频
  • Vue2 和 Vue3 View
  • 乐趣做网站厦门做网站的公司