常见的锁策略

news2025/1/20 7:18:40

在这里插入图片描述

文章目录

  • 一、常见的锁策略
    • 乐观锁 vs 悲观锁
    • 轻量级锁 vs 重量级锁
    • 自旋锁 vs 挂起等待锁
    • 互斥锁 vs 读写锁
    • 公平锁 vs 非公平锁
    • 可重入锁 vs 不可重入锁
  • 二、CAS
    • 原子类
    • 实现自旋锁
    • ABA问题

一、常见的锁策略

我们这里所介绍到的锁策略,不仅仅是java中的,任何涉及到锁的地方,比如数据库,操作系统等等,都会设计到以下特性

乐观锁 vs 悲观锁

乐观锁: 认为锁竞争不是很激烈(做的工作更少,开销更小)
悲观锁: 认为锁锁竞争比较激烈(做的工作更多,开销更大)

举个例子: 你想要去你朋友家找你朋友玩。
有两种做法: 1. 你认为你朋友是比较忙的,不一定在家。所以你会先向你朋友发消息确认一下你朋友在家吗,我来找你玩?(相当于加锁),如果在家就去找,如果不在家就等一段时间,在向朋友确定时间,这是悲观锁
2. 你认为你朋友是比较闲的,大概率是在家的,所以你会直接去你朋友家(没有加锁,直接访问资源),如果确实在家,目的就达成了,如果不在家,下次再来(尽管没加锁,但能识别数据访问冲突),这是乐观锁。

这两种锁策略,不能去分个高度,只能说不同的场景,有不同的用处。

我们的Synchronized初始使用乐观锁,当发现锁竞争比较频繁的时候,就会自动切换为悲观锁。
我们乐观锁有一个很重要的功能,就是要检测数据访问是否冲突,我们这里引入一个: "版本号"来解决。

比如两个线程去操作一个共享数据A初始为50,版本号为001,规定版本号的提交版本必须大于记录版本号才能更新操作。
在这里插入图片描述
比如我们的线程一将A修改为100,然后提交版本号为002,当我们的线程二将A修改为80时,修改版本号为002提交时,我们发现版本号并没有大于记录版本号,所以我们就认为此次的操作失败。
在这里插入图片描述

轻量级锁 vs 重量级锁

我们锁的核心特性: “原子性”,这样的机制追溯到根源是CPU硬件设备提供的
1.CPU提供了"原子操作指令”
2.操作系统基于CPU原子指令,实现了mutex互斥锁
3.JVM基于操作系统提供的互斥锁,实现了synchronized和reentrantLock等关键字和类
轻量级锁: 轻量级锁的加锁解锁的开销比较小(加锁尽量不使用mutex,而是尽量在用户态(我们人为可控制的状态)代码完成,实在完成不了在使用mutex)
重量级锁: 重量级锁的加锁解锁的开销比较大(加锁严重依赖操作系统的mutex,大量的内核态用户态的切换,线程调度的开销)
大多数情况下,乐观锁也是一个轻量级锁。
大多数情况下,悲观锁也是一个重量级锁。

自旋锁 vs 挂起等待锁

自旋锁(Spin Lock):在线程抢锁失败后,不是阻塞等待,而是快速的再循环一次,一旦锁被其他线程释放,就能第一时间获取到锁
挂起等待锁: 往往是内核实现,线程在抢锁失败之后进入阻塞状态,不占用CPU,直到操作系统调度之后被唤醒。

//自旋锁的伪代码
while(获取锁() == 失败) {
}

逻辑比较简单,先去获取锁,如果获取失败,就无限循环,直到获取到锁为止,一旦其他线程释放锁,会第一时间获取到锁。

自旋锁是一种典型的轻量级锁的实现方式:
优点: 没有放弃CPU,不涉及线程阻塞和调度,锁一旦被释放,就能第一时间获取到锁
缺点: 如果锁被占用的时间较长,那么自旋锁就会持续占有消耗CPU资源(我们挂起等待的状态是不消耗CPU资源的。

synchronized中的轻量级锁策略就很有可能是通过自旋锁方式实现的。

互斥锁 vs 读写锁

互斥锁: 我们之间用到过的synchronized这样的锁,如果一个线程获取到了锁,其他线程也尝试加锁的话,就会阻塞等待。
读写锁: 我们在并发编程的时候,如果仅仅是多个线程去读一个共享变量是不会产生线程安全问题的,但写和读之间或者写和写之间是互斥的。如果我们所有场景都使用同一种锁,就会产生很大的性能消耗,因此我们需要把读写锁分开。
读写锁就是把读操作和写操作分别加锁,我们java标准库提供了ReetrantReadWriterLock类,分别实现了读写锁。
ReentrantReadWriteLock.ReadLock 类表示一个读锁. 这个对象提供了 lock / unlock 方法进行加锁解锁.
ReentrantReadWriteLock.WriteLock 类表示一个写锁. 这个对象也提供了 lock / unlock 方法进行加锁解锁

我们所有的锁策略并没有优劣之分,我们的synchronized是互斥锁,我们的读写锁适合于"高频读,低频写"这样的场景。

公平锁 vs 非公平锁

我们来举例说明下公平锁和非公平锁。
我们有三个人在图书馆等开门。
在这里插入图片描述
我们的公平锁就是,最先来的先进,按顺序进。
在这里插入图片描述
非公平锁,就是三个人抢着给进冲,谁先进到图书馆,充满不确定性。
在这里插入图片描述
我们操作系统内部的线程调度可以认为是随机的,如果没有任何干涉,就是非公平锁。如果想要实现公平锁,需要加额外的数据结构(比如队列),来记录线程的先后顺序。

synchronized是非公平锁

可重入锁 vs 不可重入锁

可重入锁: 一个线程针对同一把锁连续加锁两次,不会出现死锁
不可重入锁: 一个线程针对同一把锁连续加锁两次,会出现死锁

按照我们之前对于锁的设定,当我们第二次加锁的时候,就会阻塞等待,直到第一次的锁被释放,才能获取到第一把锁,但是我们的第一个锁的释放也是由该线程完成,但是该线程正在尝试加锁,也就无法进行解锁操作,就会产生死锁。我们synchronized内部做了处理,判断时候第一次加锁和第二次加锁是同一个线程就不会产生死锁。

我们java当中synchronized是可重入的,以及Reentrant开头命名的锁都是可重入的,但是我们操作系统提供的mutex是不可重入锁。

二、CAS

CAS(Compare and swap):比较并交换,简单的来说就是CAS通过一条CPU指令(原子操作)在一定程度上可以回避线程安全问题,这是加锁之外,另一种解决线程安全的思路.
我们的CAS大致思路:
在这里插入图片描述
这里的old是我们内存的原数据,new是我们旧的预期值,expect是我们需要将旧的值改为的值。
1.先比较 old 和 new是否相等(比较)
2.如果相等,将expect赋给old,否则什么都不发生(交换)
3.返回操作是否成功

//CAS伪代码,并非真实
boolean CAS(address, expectValue, swapValue) {
 if (&address == expectedValue) {
   &address = swapValue;
        return true;
   }
    return false;
}

我们上述代码只是CAS大致思路,并不是原子的,真正的CAS是通过CPU指令完成的,硬件给予了支持,我们的软件层面才达到了此行为。

原子类

我们的JUC包下为我们提供了一些原子类,就是通过CAS的方式实现的。
在这里插入图片描述

方法作用
addAndGet(int delta)i += delta
decrementAndGet()–i
getAndDecrement()i–
incrementAndGet()++i
getAndIncrement()i++

我们举例说明是如何保障线程安全的,我们这里以AtomicInteger的getAndIncrement()方法为例:

class AtomicInteger {
    private int value;
    public int getAndIncrement() {
        int oldValue = value;
        while ( CAS(value, oldValue, oldValue+1) != true) {
            oldValue = value;
       }
        return oldValue;
   }
}

我们的线程一,线程二根据同一个AtomicInteger对象A进行getAndIncrement()操作。
在这里插入图片描述
我们线程一和线程二对A进行操作,线程一对A进行了CAS操作,先比较是否与内存的值是否相等,我们发现是相等的,于是进行getAndIncrement操作。
在这里插入图片描述
当我们线程二进行getAndIncrement操作时,进行比较时发现与内存的值并不相同,于是它就会从新加载一份内存的值到自己的工作内存,然后在进行比较,比较相等在进行getAndIncremenet操作,如果不相等在加载,比较,一直循环到操作完成为止。

我们可以发现通过CAS的方式就可以实现一个原子类,而且不用使用重量级锁,就可以实现多线程对于共享变量的操作。

实现自旋锁

我们在上述已经给大家已经介绍了自旋锁的情况,基于CAS能够更灵活的实现自旋锁

public class SpinLock {
    private Thread owner = null;
    public void lock(){
        // 通过 CAS 看当前锁是否被某个线程持有. 
        // 如果这个锁已经被别的线程持有, 那么就自旋等待. 
        // 如果这个锁没有被别的线程持有, 那么就把 owner 设为当前尝试加锁的线程. 
        while(!CAS(this.owner, null, Thread.currentThread())){
       }
   }
    public void unlock (){
        this.owner = null;
   }
}

简单的来说我们的自旋锁需要判断锁是否空闲,如果空闲就获取,否则继续判断,直到获取为止,这和我们的CAS的比较,赋值干的活是一致的,所以我们的判断锁是否空闲,并且获取锁这一操作就可以基于CAS的操作来实现。

ABA问题

我们的CAS操作,大致流程就是判断old和new的值是否一致,如果一致,就认为这个变量是没有进行过操作,于是之间赋值。
ABA这个问题,就相当于我们买苹果手机,确实我们是拆了新机,但我们拆的这个手机,可能是翻新机,也可能是新机,对于我们正常使用来讲是无法区分的。
但是我们这里有一个非常极端的情况。
在这里插入图片描述
我们的狗头老铁去银行取钱,卡里总共有两百,想取100去网吧冲冲浪,于是他在自动取款机上按下了取100的操作,然后自动取款机卡死了,已经扣费了100,但是屏幕没有任何显示,也没有出钱,与此同时狗头老铁的好朋友给他还钱转了100,然后等了一会,然后自动取款机恢复了,狗头老铁发现余额是200,然后又输了取款100的操作,这次取款成功了。
尽管这种情况的发生的概率是很低的,但是我们仍然需要防患于未然,针对这里的操作我们引入一个版本号的概念,比如我们进行CAS操作的时候,初始的版本号是001,每次发生变化是我们的版本号都加1,进行CAS操作时,去判断版本号,如果版本号没变,证明我们的变量是没有变化的,如果版本号变化了,证明我们的变量进行过修改。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

异常检测-缺陷检测-论文精读PaDiM

Abstract 我们提出了一个新的 Patch 分布建模框架,在单类学习的设置下,PaDiM 同时检测和定位图像中的异常。PaDiM 利用一个预先训练好的卷积神经网络 (CNN) 进行 patch 嵌入,利用多元高斯分布得到正常类的概率表示。它还利用了 CNN 的不同语…

基础1-用于癫痫发作预测的卷积门控递归神经网络

A Convolutional Gated Recurrent Neural Network for Epileptic Seizure Prediction ABSTRACT 在本文中,我们提出了一种卷积门控递归神经网络(CGRNN)来预测癫痫发作,基于从EEG数据中提取的代表信号的时间方面和频率方面的特征。使用波士顿儿童医院收集…

priority_queue的介绍及使用(18)

目录 1、priority_queue的介绍 2、priority_queue的使用 3、priority_queue的模拟实现 1、简单实现一个大堆的 2、带仿函数的 1、初识仿函数 2、priority_queue带仿函数版本 3、其他 1、priority_queue的介绍 1、优先队列是一种容器适配器,根据严格的弱排序…

Unity 3D 物理引擎简介 || Unity 3D 物理材质|| Unity 3D 力(AddForce)

Unity 3D 物理引擎简介 早期的游戏并没有强调物理引擎的应用,当时无论是哪一种游戏,都是用极为简单的计算方式做出相应的运算就算完成物理表现,如超级玛丽和音速小子等游戏。 较为常见的物理处理是在跳跃之后再次落到地上,并没有…

百万级数据的导出解决方案

一、传统POI的的版本优缺点比较首先我们知道POI中我们最熟悉的莫过于WorkBook这样一个接口,我们的POI版本也在更新的同时对这个几口的实现类做了更新;HSSFWorkbook : 这个实现类是我们早期使用最多的对象,它可以操作Excel2003以前&#xff08…

SAP 系统内核版本详解

前言:之前两篇简单介绍了ABAP 7.40的新特性,那么有的时候会有这样的情况,根据新特性写代码的时候校验会不通过,这又是为啥呢?会不会跟SAP的版本有关系呢? 那么,首先我们就来看一下在SAP系统中如…

基于TC397的Bootloader开发过程中遇到的问题记录

问题11 现象:刷新流程结束之后上位机通过22服务AFFC读取刷新计数时,刷新计数会偶发地置12 分析思路:尝试用单步调试的方法复现该现象,程序中涉及到刷新计数的更新有两处,一是在34服务中擦flash前,二是在31服…

【数据结构】栈及其经典面试题详解

目录前言一、栈的介绍二、数据类型重定义三、栈的结构四、栈中的常见操作五、测试栈六、栈的常见面试题前言 前面学习的线性表中包含顺序表和链表,这两种数据结构允许在任意位置进行插入和删除,那么有没有一种数据结构是不能在任意位置进行插入删除&…

全面解读MinION纳米孔测序技术及应用

全面解读MinION纳米孔测序技术及应用 link:https://www.seqchina.cn/467.html 【测序中国】 paper:The Oxford Nanopore MinION: delivery of nanopore sequencing to the genomics community https://pubmed.ncbi.nlm.nih.gov/27887629/ 纳米孔测序技术…

知识蒸馏 Knowledge distillation(学习笔记)

知识蒸馏概述 蒸馏:把大的 复杂的东西变成小的纯净的东西 在知识蒸馏中 大的模型为 教师模型(teacher)臃肿 集成 牛逼 复杂的 小的 为 学生模型(student)小的精干的 轻量化的 这里有一个知识的迁移 因为落地实…

相关性模型与回归模型(例题代码)

一、相关性模型(SPSS) 相关性模型涉及到两种最为常用的相关系数: 皮尔逊person相关系数斯皮尔曼spearman等级相关系数 1、皮尔逊相关系数 相关性可视化 总结: 1.如果两个变量本身就是线性的关系,那么皮尔逊相关系…

儿子小伟再婚,新儿媳紧锁眉头,农民歌唱家大衣哥有些过分了

虽然都知道大衣哥儿子小伟结婚,这一天早晚都要到来,但是却没有想到来得那么快,大衣哥儿子小伟的婚礼,在悄无声息中结束了。说起大衣哥儿子小伟,这已经不是第一次结婚了,因为结过婚有经验,这一次…

Linux CFS调度器之pick_next_task函数

文章目录前言一、pick_next_task二、pick_next_task_fair参考资料前言 在内核执行__schedule函数,进程任务切换的时候,__schedule函数函数会调用pick_next_task让调度器从就绪队列中选择最合适的一个进程运行,如下所示: static …

Nerdctl 原生支持 Nydus 加速镜像

文|李楠(GitHub ID : loheagn) 北京航空航天大学 21 级研究生 云原生底层系统的开发和探索工作。 本文 6369 字 阅读 16 分钟 OSPP 开源之夏是由中科院软件研究所“开源软件供应链点亮计划”发起并长期支持的一项暑期开源活动。旨在鼓励在…

关于whl,你想知道的

一、whl是什么?whl文件时以wheel格式保存的python安装包,Wheel是Python发行版的标准内置包格式。WHL文件包含Python安装的所有文件和元数据,其中还包括所使用的Wheel版本和打包的规范。WHL文件使用Zip压缩进行压缩,实际上也是一种…

二、TCO/IP---Ethernet和IP协议

TCP/ip协议栈 OSI模型TCP/IP协议栈应用层,表示层,会话层应用层传输层主机到主机层(传输层)网络层网络层数据链路层,物理层网络接入层 Ethernet协议 以太网,实现链路层的数据传输和地址封装(MA…

【Qt】Qt中的拖放操作实现——拖放文件以及自定义拖放操作

文章目录Qt的拖放操作使用拖放打开文件自定义拖放操作文章参考《Qt Creator快速入门(第三版)》。 Qt的拖放操作 拖放操作分为拖动Drag和放下Drop,Qt提供了强大的拖放机制,可在帮助文档中通过Drag and Drop关键字查看。 在Qt中&a…

ArcGIS基础实验操作100例--实验78按栅格分区统计路网

本实验专栏参考自汤国安教授《地理信息系统基础实验操作100例》一书 实验平台:ArcGIS 10.6 实验数据:请访问实验1(传送门) 高级编辑篇--实验78 按栅格分区统计路网 目录 一、实验背景 二、实验数据 三、实验步骤 (…

【数据结构】队列详解

前言 前面我们学习了一种数据结构:栈,栈是一种只允许在一端尽进行插入删除的数据结构,而今天我们将学习另一种数据结构:队列,队列是一种支持在一端进行插入,在另一端进行删除的数据结构。 一、队列的介绍…

PHP反序列化字符串逃逸

PHP反序列化字符串逃逸 提示:写完文章后,目录可以自动生成,如何生成可参考右边的帮助文档 文章目录PHP反序列化字符串逃逸前言一、关于反序列化和序列化二、[0ctf 2016]unserialize二、prize_p5[NSSCTF]前言 例如:最近日常刷题玩…