JavaEE之常见的锁策略

server/2025/1/17 19:36:15/

前面我们学习过线程不安全问题,我们通过给代码加锁来解决线程不安全问题,在生活中我们也知道有很多种类型的锁,同时在代码的世界当中,也对应着很多类型的锁,今天我们对锁一探究竟!

1. 常见的锁策略

注意: 接下来介绍的锁策略不仅仅是局限于Java.任何和"锁"相关的话题,都可能会涉及到以下内容.这些特性主要是给锁的实现者来参考的.
我们普通的程序猿也需要了解⼀些,对于合理的使用锁也是有很大帮助的.

1.1 乐观锁vs悲观锁

乐观锁: 在执行任务之前预期竞争不激烈,那就可以先不加锁,等后面如果真实发生了锁竞争再加锁

举个例子: 我很喜欢我的女友,在学校时,我们天天在一起,我不担心有冲突别人会占用她(并没有对她上锁),但是偶尔她会和她的好朋友出去,此时,我感觉到有人在和我竞争她,我就加锁,不让她离开我

悲观锁: 在执行任务之前预期竞争非常激烈(表示在执行任务的时候会发生锁冲突),必须先加锁再执行任务

举个例子: 我很喜欢我的女友,为了和她天天在一起,我对她上锁,这样当别人想要和她玩的时候,就会和我竞争,但是我持有锁,别人就会得不到她,显示她一直被我持有,哈哈哈

1.2 重量级锁vs轻量级锁

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

  • CPU提供了"原子操作指令".
  • 操作系统基于CPU的原子指令,实现了mutex 互斥锁.
  • JVM基于操作系统提供的互斥锁,实现了synchronizedReentrantLock 等关键字和类.
    在这里插入图片描述

轻量级锁: 加锁机制尽可能不使用mutex,而是尽量在用户态代码完成.实在搞不定了,再使用mutex.加锁的过程比较简单,用到的资源比较少,典型就是用户态的一些操作(JAVA 层面就可以完成加锁)

举个例子: 有对象的男同胞,都会遇到的问题,就是和女友出去玩的时候,会等待女友的精心打扮(化妆),轻量级锁就是不停的自旋,一会问一下“宝宝,化好妆了吗”(只是一昧的催促女友,不干别的事情,消耗资源较少),可以第一时间知道女友啥时候化好妆可以出发

重量级锁: 加锁机制重度依赖了OS提供了mutex,加锁的过程比较复杂,用到的资源比较多,典型的就是内核态的一些操作

举个例子: 当女友在化妆的时候,我们不去过问,而是做好自己的事情,准备好出去玩的所有东西(一直在帮忙做事,消耗了很多资源),等待女友的召唤(唤醒),不能第一时间知道女友啥时候化好妆

理解用户态和内核态:
想象去银行办业务.
在窗口外,自己做,这是用户态.用户态的时间成本是比较可控的.
在窗口内,工作人员做,这是内核态.内核态的时间成本是不太可控的. 如果办业务的时候反复和工作人员沟通,还需要重新排队,这时效率是很低的.

乐观锁是能不加锁就不加锁,从而导致他干活少,消耗资源也少,所以可以说乐观锁就是一种轻量级锁
悲观锁是任何时候都加锁,从而导致他干活多,消耗资源也多,所以可以说悲观锁就是一种重量级锁

1.3 自旋锁vs挂起等待锁

自旋锁: 不停的检查锁是否被释放,如果一旦锁被释放就可以直接获取锁资源
挂起等待锁: 阻塞等待,等待到被唤醒

举个例子: 每周末,我要和我对象一起吃饭,我到了她宿舍楼下打电话问她啥时候下来,她说等一会,我就不停的打电话(一直在自旋,不停的检查锁的状态,她下楼了,我会第一时间发现)——>自旋锁,我不打电话了,然后去小亭子坐下来,等待(我就不能第一时间发现她下楼如果她下楼之后就需要喊我一声相当于通知我(唤醒),获取锁资源)——>阻塞:挂起等待锁

优缺点:

  1. 自旋锁:
    纯用户态的操作,可以第一时间获取到锁,
    有自旋次数和时间的限制,通过这个限制可以控制对系统资源的消耗,可以第一时间获取到锁
  2. 挂起等待锁:
    内核态的操作,会生成对应的加锁指令,要等待唤醒,在等待的过程中会释放CPU资源

自旋锁详情请看后续CAS详细介绍

1.4 公平锁vs非公平锁

公平锁: 先来后到,先排队的线程先拿到锁,后排队的线程后拿到锁,JAVA的JUC中有一个类专门实现了公平锁
非公平锁: 大家去抢,谁先抢到是谁的,synchronized是一个非公平锁

注意:

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

举个例子: 现实生活中如果要真正的公平:立法、执法、教育、环境都要发挥作用消耗更大的资源,实现公平锁的过程也是一样,需要用额外的逻辑去管理线程,做到先来后到

1.5.读写锁(readers-writerlock)

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

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

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

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

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

其中,

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

注意,只要是涉及到"互斥",就会产⽣线程的挂起等待.⼀旦线程挂起,再次被唤醒就不知道隔了多久了.因此尽可能减少"互斥"的机会,就是提高效率的重要途径.
适用场景:
读写锁特别适合于"频繁读,不频繁写"的场景中.(这样的场景其实也是非常广泛存在的).

举个例子: 大学生必备学习通,老师会经常上课点名(读操作),发布学习资料(写操作),同学们看详细资料(读操作),读操作会涉及很多次,但是写操作偶尔几周一次,所以很适合读写锁

1.6可重入锁VS不可重入锁

可重入锁: 对一把锁可以连续加多次,多次加锁也要多次解锁,不造成死锁
不可重入锁: 对一把锁可以连续加多次,造成死锁

2. 相关面试题

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

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

2.2 介绍下读写锁?

  1. 读写锁就是把读操作和写操作分别进行加锁.
  2. 读锁和读锁之间不互斥.
  3. 写锁和写锁之间互斥.
  4. 写锁和读锁之间互斥.
  5. 读写锁最主要用在"频繁读,不频繁写"的场景中

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

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

相比于挂起等待锁,

  1. 优点:没有放弃CPU资源,⼀旦锁被释放就能第⼀时间获取到锁,更高效.在锁持有时间比较短的场景下非常有用.
  2. 缺点:如果锁的持有时间较长,就会浪费CPU资源.

2.4 synchronized 是可重入锁么?

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


http://www.ppmy.cn/server/159169.html

相关文章

【Linux】12.Linux进程概念(1)

文章目录 1. 冯诺依曼体系结构2. 操作系统(Operator System)概念设计OS的目的胆小的操作系统定位如何理解 "管理"总结 3. 进程基本概念task_struct-PCB的一种task_ struct内容分类组织进程查看进程通过系统调用获取进程标示符通过系统调用创建进程-fork初识 1. 冯诺依…

探秘 JMeter (Interleave Controller)交错控制器:解锁性能测试的隐藏密码

嘿,小伙伴们!今天咱们要把 JMeter 里超厉害的 Interleave Controller(交错控制器)研究个透,让你从新手直接进阶成高手,轻松拿捏各种性能测试难题! 一、Interleave Controller 深度剖析 所属家族…

四阶龙格库塔法求解二元二阶常微分方程

龙格库塔法(Runge-Kutta methods)是用于非线性常微分方程的解的重要的一类隐式或显式迭代法。在工程领域应用广泛,可用于求解复杂系统的运动方程等问题。 这里采用matlab程序编写代码实现龙格库塔法对于二元二阶常微分方程的求解。 例 { x …

数据分析-使用Excel透视图/表分析禅道数据

背景 禅道,是目前国内用得比较多的研发项目管理系统,我们常常会用它进行需求管理,缺陷跟踪,甚至软件全流程的管理,如果能将平台上的数据结公司的实际情况进行合理的分析利用,相信会给我们的项目复盘总结带来…

【Elasticsearch】搜索类型介绍,以及使用SpringBoot实现,并展现给前端

Elasticsearch 提供了多种查询类型,每种查询类型适用于不同的搜索场景。以下是八种常见的 Elasticsearch 查询类型及其详细说明和示例。 1. Match Query 用途:用于全文搜索,会对输入的文本进行分词,并在索引中的字段中查找这些分…

《零基础Go语言算法实战》【题目 2-25】goroutine 的执行权问题

《零基础Go语言算法实战》 【题目 2-25】goroutine 的执行权问题 请说明以下这段代码为什么会卡死。 package main import ( "fmt" "runtime" ) func main() { var i byte go func() { for i 0; i < 255; i { } }() fmt.Println("start&quo…

《leetcode-runner》如何手搓一个debug调试器——引言

文章目录 背景 仓库地址&#xff1a;leetcode-runner 背景 最近笔者写了个idea插件——leetcode-runner。该插件可以让扣友在本地刷leetcode&#xff0c;并且leetcode提供的和代码相关的编辑功能该插件都提供&#xff0c;具体演示如下 唯一不足的就是代码debug。众所周知&…

OpenCV相机标定与3D重建(59)用于立体相机标定的函数stereoCalibrate()的使用

操作系统&#xff1a;ubuntu22.04 OpenCV版本&#xff1a;OpenCV4.9 IDE:Visual Studio Code 编程语言&#xff1a;C11 算法描述 标定立体相机设置。此函数找到两个相机各自的内参以及两个相机之间的外参。 cv::stereoCalibrate 是 OpenCV 中用于立体相机标定的函数。它通过一…