《JavaEE》锁的多种形态
创始人
2025-05-29 05:01:46
0

👑作者主页:Java冰激凌
📖专栏链接:JavaEE

一、悲观锁 vs 乐观锁

        悲观锁:预期锁冲突的概率很高

        乐观锁:预期锁冲突的概率很低

相比而言 悲观锁做的工作更多 但是要付出的成本更多 效率是比较低的

                乐观锁做的工作更少 付出的成本更少 效率是属于高效的

我们应该如何理解我们的悲观锁与乐观锁呢? 

        我们可以假设一个公交车与出租车 公交车属于人满了才会出发 出租车是属于有人要打车即可出发 但是公交车的荷载人数为30位乘客 出租车的荷载为4位乘客 相比较去同一个目的地 乐观锁就相当于是公交车 公交车只需要上车投币即可至于行走什么路线 我们是不需要关心的  悲观锁相当于出租车我们还要选择出发地点 行走的路线 虽然单次出发的速度是比较快的 但是如果30人去一个目的地 比较而言是公交车更为高效的 出租车需要来回不停的奔波 

        也有一个更加形象的例子 例如我们上厕所 我们可以进行上锁操作 关门 上锁 上厕所 开锁 开门 这相当于是悲观锁的操作 乐观锁的操作 开门 上厕所 开门 因为我们开关锁也是需要时间的所以说乐观锁的效率相对而言是比较高的


二、读写锁 vs 普通的互斥锁

        读写锁:一共需要三个操作

                1.加读锁 如果代码只进行读操作 加读锁针对读和读之间不会存在互斥的关系 但是如果存在读和写的操作并行 就会被加锁

                2.加写锁 如果代码只进行了修改操作就加写锁

                3.解锁

        普通的互斥锁:对于普通的互斥锁 操作只有加锁和解锁

为何读写锁在加读锁的时候不会被加锁?因为我们的读取数据本身就是线程安全的 进行多次读取都不会改变其值 (ps:对于现在的编译器例如IDEA 会对频繁的读取数据进行优化 优化过程会从硬盘读取数据变为从寄存器读取数据 因为检测到数据多次读取没有发生改变 优化为从寄存器读取 因为从寄存器读取的效率是从硬盘读取速度的好几个数量级)

import java.time.LocalDateTime;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/*** @Author: guo bing lin* @Date: 2023-02-15 09:22**/
//读写锁的应用
public class demo1 {public static void main(String[] args) {//创建读写锁final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();//创建读锁final ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock();//创建写锁final ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock();//创建线程池ThreadPoolExecutor threadPool = new ThreadPoolExecutor(5,5,0,TimeUnit.SECONDS,new LinkedBlockingQueue<>());//线程池执行任务1【读操作】threadPool.submit(()->{//加锁操作readLock.lock();try {System.out.println("执行读锁1:"+ LocalDateTime.now());TimeUnit.SECONDS.sleep(3);} catch (InterruptedException e) {e.printStackTrace();}finally {readLock.unlock();}});//线程池执行任务2【读操作】threadPool.submit(()->{//加锁操作readLock.lock();try {System.out.println("执行读锁2:"+ LocalDateTime.now());TimeUnit.SECONDS.sleep(3);} catch (InterruptedException e) {e.printStackTrace();}finally {readLock.unlock();}});//线程池执行任务3【写操作】threadPool.submit(()->{//加锁操作writeLock.lock();try {System.out.println("执行写锁1:"+ LocalDateTime.now());TimeUnit.SECONDS.sleep(3);} catch (InterruptedException e) {e.printStackTrace();}finally {writeLock.unlock();}});//线程池执行任务4【写操作】threadPool.submit(()->{//加锁操作writeLock.lock();try {System.out.println("执行写锁2:"+ LocalDateTime.now());TimeUnit.SECONDS.sleep(3);} catch (InterruptedException e) {e.printStackTrace();}finally {writeLock.unlock();}});}
}

 

从上图可以看出读操作是可以并行的 但是写操作是会被加对应的写锁 因此执行时间是不同的  

        举一个简单的例子 假设课代表收作业 课代表在收作业的时候是可以同时收取多本作业的 但是课代表在进行批改作业的时候 只能一本一本进行判断  收作业就可以看做是加了读锁 批改作业可以看做是加写锁 


 三、轻量级锁 vs 重量级锁

        轻量级锁:做了更少的事情 开销更小

        重量级锁:做了更多的事情 开销更大

通常情况下 一般悲观锁都是重量级锁 一般悲观锁都是重量级锁 乐观锁一般都是轻量级锁(不绝对)

那么他们之间有什么区别呢?

        首先我们要先了解

        锁的核心特性 "原子性"(我们默认原子是不可被分割的 也就是最小的单位), 这样的机制追根溯源是 CPU 这样的硬件设备提供的.

        ·CUP提供了“原子操作指令(CAS 全称 compare and swap  compare是比较的意思 and 和  swap 交换 也就是比较和交换(后面会详细展开))”

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

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

        

 此处的synchronized不仅仅是对mutex进行了封装 在synchronized还做了很多工作

        轻量级锁:加锁机制尽量在用户态代码完成 但是如果遇到膨胀 还是会使用mutex  在使用mutex之后 轻量级锁会膨胀为重量级锁  

        ·会进行少量的 用户态内核态切换 

        ·不容易引发线程调度

        重量级锁:加锁机制完全要依赖mutex 

        ·会进行大量的 用户态内核态切换

        ·容易引发线程调度


我们这边解释一下什么叫做用户态和内核态:

        用户态和内核态我们可以理解为吃饭 我们可以选择自己做饭与点外卖  如果选择自己做饭的话 我们可以考虑做的简单一点 快速一点 这个时间或者口味是可以自己把握的 但是如果是点外卖的话 我们不清楚商家会不会第一时间接到单 也不清楚商家在接单 后会不会直接开始做饭 口味也要随着商家的口味来进行制作 也不清楚外卖小哥配送外卖需要多久 用户态和内核态的区别就在这里 用户态中的 时间是可以自己掌握的 但是内核态 我们是将任务交给了别人 我们无法准确的控制时间 所以说 用户态是比较可控的 内核态是比较不可控的 在计算机中 用户态就是实现在代码之上的 但是内核态是交给计算机来完成 我们不清楚计算机当前是否有其他任务要优先做 所以相对而言是比较不可控的


 四、挂起等待锁 vs 自旋锁

        挂起等待锁: 挂起等待锁往往是通过内核的一些机制实现的 往往代价较重

        自旋锁:往往就是通过用户态代码来实现的 往往较轻 锁冲突不激烈可以使用 但是激烈的话 使用代价比较大

挂起等待锁 就类似于要去获取一个锁 如果没有获取到 就去慢慢沉沦  但是如果主动去呼唤给予锁 会立即被唤醒

自旋锁 也是要获取一个锁但是如果没有获取到就会一直等待 一直到成功获取到锁

优点:没有进行放弃 仍然一直在进行获取 一旦可以获取到调度 可以第一时间获取到

缺点:如果锁被其他的线程持有时间较久 那么自旋锁会持续的消耗CPU资源(相比较挂起等待锁是不会持续消耗CPU的)

自旋锁可以配合一个代码来进行理解 while(抢锁(lock)== false);

我们来举一个形象的 例子 

        有一天 挂机等待锁去找女神去表白 跟女神说 女神 做我女朋友吧  女神说 我考虑考虑 那么挂起等待锁听了 就回家去等女神消息了

 可能在过了几天的某一天 女神给挂起等待锁说 我同意了 那么挂起等待锁就会被立即唤醒 做为女神的男朋友  但是也有一种可能 这个事情就石沉大海了

 也是同一天 自旋锁跟自己的女神表白

 听了女神的话 自旋锁并没有回家等消息 而是一直等待女神的回复  不得到女神的恢复誓不罢休 一直对女神发起表白,那么如果女神同意了  自旋锁就能第一时间得到回应

 是不是很有趣呢 哈哈哈~~


 五、公平锁 vs 非公平锁

        公平锁:多个线程在等待一把锁的时候 谁是先来的 那么当这把锁被释放的 时候 优先来的就能获取到这把锁

        非公平锁:多个线程在等待一把锁的时候 不遵循先来后到的规则 他们去一块竞争(也就是说 每个线程在获取到这把锁的概率都是相等的)

所谓的公平 也就是先来后到这个原则 先到的有优先获取的权利 非公平就是不遵循这个规则 这一块是很好理解的

公平锁相比较非公平锁开销是较大的 因为我们需要有组织一个队列 来对于来等待锁的线程进行排序 使可以遵循先来后到的原则 但是非公平锁完全不需要考虑这个

举例 假设我们在 吃自助餐  这家餐厅提供澳龙 但是是限量供应的 这家餐厅是相当的火爆 澳龙供不应求 其中 如果我们采用公平锁的话 就是大家需要进行排队 依次取走供应处的澳龙 如果采用非公平锁的话  大家就是等待澳龙一上来 大家一起上去哄抢 谁抢到的就是谁的 


六、重入锁 vs 不可重入锁 

        重入锁 : 一个线程可以进行两次或者两次以上的加锁 那么就是可重入锁

        不可重入锁 : 一个线程只能进行一次加锁 那么就是不可重入锁(如果对于不可重入锁进行两次或者两次以上的加锁 就会出现死锁)

我们来解释一下什么叫做死锁

        最近我们刚刚开学 而我们学校重新返校是需要出示学生证的 而恰好 我的学生证在当初离校的时候放在了宿舍没有带 这个时候 死锁就出现了 门卫大哥说 请你出示学生证 我就可以放你进去 我说 我的学生证忘在宿舍忘带了 你让我进去 我取出来给你看 门卫大哥说 你必须出示学生证 我才可以让你进入校园 我说 你不让我进去校园 我就没法给你取到学生证~

        这个相当的滑稽 那么最后还是舍友给我送下来学生证才成功的进入校园


七、synchronized

  1. 既是一个乐观锁 也是一个悲观锁
  2. 不是读写锁 只是一个普通互斥锁
  3. 既是一个轻量级锁 也是一个重量级锁
  4. 轻量级锁的部分基于自旋锁来实现 重量级的部分基于挂起等待锁来实现
  5. 非公平锁
  6. 可重入锁

相关内容

热门资讯

AWSECS:访问外部网络时出... 如果您在AWS ECS中部署了应用程序,并且该应用程序需要访问外部网络,但是无法正常访问,可能是因为...
AWSElasticBeans... 在Dockerfile中手动配置nginx反向代理。例如,在Dockerfile中添加以下代码:FR...
银河麒麟V10SP1高级服务器... 银河麒麟高级服务器操作系统简介: 银河麒麟高级服务器操作系统V10是针对企业级关键业务...
北信源内网安全管理卸载 北信源内网安全管理是一款网络安全管理软件,主要用于保护内网安全。在日常使用过程中,卸载该软件是一种常...
AWR报告解读 WORKLOAD REPOSITORY PDB report (PDB snapshots) AW...
AWS管理控制台菜单和权限 要在AWS管理控制台中创建菜单和权限,您可以使用AWS Identity and Access Ma...
​ToDesk 远程工具安装及... 目录 前言 ToDesk 优势 ToDesk 下载安装 ToDesk 功能展示 文件传输 设备链接 ...
群晖外网访问终极解决方法:IP... 写在前面的话 受够了群晖的quickconnet的小水管了,急需一个新的解决方法&#x...
不能访问光猫的的管理页面 光猫是现代家庭宽带网络的重要组成部分,它可以提供高速稳定的网络连接。但是,有时候我们会遇到不能访问光...
Azure构建流程(Power... 这可能是由于配置错误导致的问题。请检查构建流程任务中的“发布构建制品”步骤,确保正确配置了“Arti...