[锁]:乐观锁、悲观锁与死锁

这篇具有很好参考价值的文章主要介绍了[锁]:乐观锁、悲观锁与死锁。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

摘要

摘要:乐观锁;悲观锁;实现方法;本地锁;分布式锁;死锁;行级锁;表级锁

1 锁的相关概念

1.1 为什么需要锁?

问题

  • ① 在多个线程访问共享资源时,会发生线程安全问题,例如:在根据订单号生成订单时,若用户第一次由于某种原因(网络连接不稳定)请求失败,则会再次发生请求,此时便会产生同一订单号生成多个订单,这显然是有问题的。

解决

  • ① 针对上述问题,我们有一个解决思想,给用户第一次的请求加锁,只有当前第一次请求拥有锁,请求线程在拥有锁时,方可执行,其他线程必须在拥有锁的线程执行完毕后,方可执行。

1.2 本地锁

问题

  • ① 目前的系统架构,大体分为两类:一类是单体架构,另一类是分布式架构(在分布式架构中为保障系统的高可用,我们又会搭集群),
  • ② 针对上述两类架构的特点,锁又分成两种不同的类别:一类是针对单体架构的锁,称之为本地锁,另一类是针对分布式架构的锁,称之为分布式锁。
  • ③ 针对本地锁,又有两类:一类是在高并发场景下,编程语言实现对自己多线程控制的本地锁,诸如:Java语言中synchronized、Lock本地锁(同时也是悲观锁),另一类是在数据库中实现的锁思想,诸如:乐观锁、悲观锁、共享锁、排它锁、记录锁、间隙锁、表锁等本地锁,其都可以称之为本地锁,保证业务数据的准确性。

解决

  • ① 本地锁:针对单体架构项目高并发特点,有两类解决方案:一类是语言自己实现的,例如Java语言的synchronized、Lock锁,另一类是在数据库中实现的锁思想,例如乐观锁与悲观锁,本文也着重于此两点说明。
  • ② 分布式锁:由于编程语言自己实现的锁,无法满足在分布式架构中多链路调用情况,因而出现分布式锁的思想他的解决主要有:Redisson、zookeeper、数据库(数据库性能低,使用场景少),详情请参阅另一篇文章:[分布式锁]:Redis与Redisson

2 乐观锁与悲观

通俗理解:乐观锁,对一件事持乐观态度,认为大概率不会发生;悲观锁,对一件事持悲观态度,认为大概率会发生。

2.1 乐观锁

2.1.1 乐观锁的概念

概念:认为大概率不会发生线程安全问题。

2.1.2 乐观锁的解决思想

解决思想:通过在数据上添加标识(如版本号或时间戳)来进行并发控制,实现线程安全的共享数据访问。

2.1.2.1 数据版本号机制思想

① 首先,给数据库添加一个字段version(int)的标记字段,
② 随后,当多个线程同时访问数据库时,都会获得version的值,
③ 然后,在提交更新时若刚才读取到的version为当前数据库中中version值时才更新,伴随着更新过后version的值也会发生变化,
④ 最后,当其他线程需要提交更新时,获取到的version值和当前数据库version值不一样,提交更新失败,从而实现对线程安全的控制。

2.1.2.1.1 数据版本号机制实现——基于mybatis

引入业务场景:假设数据库中账户有一version字段(值为1),且当前账户余额balance字段(值为100)

  • ① 操作员A此时将其读出(此时version=1),并从账户余额中扣除50(100-50),
  • ② 操作员A操作的同时,操作员B也读出此账户信息(此时version=1),并从账户余额扣除20(100-20),
  • ③ 操作员A先完成了修改工作,并且,将数据版本号(version=1)和账户扣除后余额(balance=50),提交至数据库更新,此时由于提交数据版本=当前数据库记录版本,数据被成功更新,数据版本更新为2(version=2),
  • ④ 操作员B完成操作后,也将读出到的数据版本(version=1)和账户扣除后余额(balance=80),提交至数据库请求更新,此时数据库数据版本已经被更新(version=2),不满足数据版本号相同时,才能更新数据的策略,因此操作员B请求被驳回。从而保证数据的的准确。
2.1.2.1.1.1 实体类中添加响应字段,并设定当前字段用于记录数据的版本信息
@Configuration
public class MpConfig {
    @Bean
    public MybatisPlusInterceptor mpInterceptor() {
        //1.定义Mp拦截器
        MybatisPlusInterceptor mpInterceptor = new MybatisPlusInterceptor();

        //2.添加乐观锁拦截器
        mpInterceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
        
        return mpInterceptor;
    }
}

2.1.2.1.1.2 使用乐观锁前必须先获取对应数据版本号

注意:由于在使用乐观锁时需要跟数据库频繁进行交互,因而在高并发场景下,建议使用分布式锁和悲观锁(是的,乐观锁也可以作为分布式锁来使用)。

@Test
public void testUpdate() {
    /*User user = new User();
    user.setId(3L);
    user.setName("Jock666");
    user.setVersion(1);
    userDao.updateById(user);*/
    
    //1.先通过要修改的数据id将当前数据查询出来
    //User user = userDao.selectById(3L);
    //2.将要修改的属性逐一设置进去
    //user.setName("Jock888");
    //userDao.updateById(user);
    
    //1.先通过要修改的数据id将当前数据查询出来
    User user = userDao.selectById(3L);     //version=3
    User user2 = userDao.selectById(3L);    //version=3
    user2.setName("Jock aaa");
    userDao.updateById(user2);              //version=>4
    user.setName("Jock bbb");
    userDao.updateById(user);               //verion=3?条件还成立吗?
}

2.1.2.2 CAS算法思想
  • 实现思想:CAS算法基于ccompare-and-swap(比较和交互)操作,类似于Junit的断言机制,其通过比较当前值和期望值的方式,实现乐观锁的并发控制机制。
  • 使用说明:在使用时,先读取数据的原值,根据规则计算出新的期望值,随后,使用CAS操作把期望值写入数据的存储位置,若操作成功,说明没有发生冲突,更新操作可以提交,否则,操作失败,需要重新读取数据并重复以上操作。
  • 问题:一方面是数据库性能问题,另一当面是ABA问题。
  • ABA问题:当前有ABC三个线程,初始数据版本号为V1,线程AB分别查询到该数据的版本号是V1,线程A先更新数据,把版本改为A2,然后线程C也执行一次操作把版本号从V1更改成V3随后又改回V1,此时当线程B更新时发现数据版本匹配,更新操作成功,但实际数据已经被C修改,因此为避免此问题又需借助时间戳、版本号机制来解决。

2.2 悲观锁

2.2.1 悲观锁的概念

悲观锁:认为大概率会发生线程安全问题。

2.2.2 悲观锁的解决思想

悲观锁的核心思想:在操作共享数据之前对其进行加锁,保证同一时刻只有一个线程可以访问,避免数据的并发修改和读取。

2.2.3 悲观锁的实现方式

2.2.3.1 基于数据库机制

基于数据库机制,诸如:行级锁和表级锁等,通过在数据库上对共享数据进行加锁,保证数据的一致性和完整性。

2.2.3.1.1 行级锁

行级锁:是在对数据库表进行操作第,对操作进行的行加锁,即SQL语句在修改每行记录时,只会锁定该行数据,不会影响其他行,其他记录仍可被修改,行级锁可以有效提高并发性,然而由于加锁粒度小,因此在短事务(诸如简单的update、delete、insert操作)并发场景下会影响性能。

  • 如:在MySQL的InnoDB中提供的就是行级锁。
  • 注:长事务(占用锁的粒度比较大,时间长,通常会涉及多个表,多次修改,对数据库影响性能较大)
2.2.3.1.2 表级锁

表级锁:MySQL中MyISAM存储引擎使用的是表级锁,当一个事务对一个表进行操作时,MyISAM会给整个表加锁,其它事务无法对该表进行读取或修改操作,直至锁释放,然而由于加锁粒度大,因此会带来性能上的损失,不适用于并发更新操作比较多的场景。

2.2.3.2 基于应用层面的锁机制

基于应用层面的锁机制,如synchronized锁和lock锁等,通过在代码层面对共享数据进行加锁,实现数据的并发控制。

2.2.3 悲观锁缺点

2.2.3.1 性能瓶颈

性能瓶颈:需要早操作共享数据前加锁,阻塞其它线程对数据的访问,造成性能瓶颈。

2.2.3.1 死锁问题

死锁题锁:在锁定期间可能会出现死锁问题,即在加锁操作过程中出现异常或造成线程长时间未释放,就可能发生死锁问题,导致应用程序崩溃。

2.3 死锁

2.3.1 死锁的概念

死锁:两个或多个进程,因相互申请对方占用的资源而造成互相等待的现象,导致所有进程都在等待彼此释放资源而无法继续向前推荐,最终都无法完成任务。

2.3.2 死锁出现原因

死锁出现原因:由于多个线程或进程在运行过程中,因相互申请对方占用资源而造成互相等待的现象,从而,出现一种无法解决的状态。

案例:文章来源地址https://www.toymoban.com/news/detail-482287.html

  • 两个线程AB同时占用一些资源,现在A需要获取B占用资源,而B同时需要获取A占用资源,两个线程都不释放已占用资源,从而造成死锁状态。

2.3.3 死锁解决方法

  • ① 预防死锁:通过合理资源申请和释放策略避免死锁发生。
  • ② 检测死锁:采用算法检测死锁状态,并及时采取相应措施,诸如撤销一些进程、杀掉进程等。
  • ③ 解除死锁:采用一定策略释放资源、终止进程等以打破死锁状态。
  • ④ 避免死锁:从资源分配角度出发,在分配每一个资源时都要判断是否会出现死锁,若会出现,则不分配资源。

到了这里,关于[锁]:乐观锁、悲观锁与死锁的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【Redis】秒杀业务设计、悲观锁与乐观锁

    一些情境下,使用数据库的ID自增将会产生一些问题。 一方面,自增ID规律性明显,可能被猜测出来并产生一些漏洞 另一方面,当数据量很大很大很大时,单表数据量可能会受到限制,需要分表,多个表之间的ID自增策略受限 测试: Runnable接口是一个函数式接口,即只有一个

    2024年02月13日
    浏览(26)
  • JavaEE 初阶篇-深入了解 CAS 机制与12种锁的特征(如乐观锁和悲观锁、轻量级锁与重量级锁、自旋锁与挂起等待锁、可重入锁与不可重入锁等等)

    🔥博客主页: 【 小扳_-CSDN博客】 ❤感谢大家点赞👍收藏⭐评论✍ 文章目录         1.0 乐观锁与悲观锁概述         1.1 悲观锁(Pessimistic Locking)         1.2 乐观锁(Optimistic Locking)         1.3 区别与适用场景         2.0 轻量级锁与重量级锁概述         2.1 真正加

    2024年04月16日
    浏览(26)
  • django实现悲观锁乐观锁

    前期准备 1.原生mysql悲观锁 2.orm实现上述(悲观锁)  3 乐观锁秒杀--》库存还有,有的人就没成功  

    2024年02月12日
    浏览(32)
  • MySQL锁(读锁、共享锁、写锁、S锁、排它锁、独占锁、X锁、表锁、意向锁、自增锁、MDL锁、RL锁、GL锁、NKL锁、插入意向锁、间隙锁、页锁、悲观锁、乐观锁、隐式锁、显示锁、全局锁、死锁)

    本文说明的是MySQL锁,和操作系统或者编程语言的锁无关。 作用:在并发情况下让数据正确的读写。 优点:并发情况下对数据读写可控,防止出错。 缺点:降低性能、增加难度。 数据操作类型划分 读锁(共享锁、S锁) 写锁(排它锁、独占锁、X锁) 粒度划分 表级锁 S锁、

    2024年03月10日
    浏览(37)
  • django中实现事务/django实现悲观锁乐观锁案例

    原生mysql悲观锁 orm实现上述 乐观锁秒杀--》库存还有,有的人就没成功

    2024年02月12日
    浏览(24)
  • 乐观锁和悲观锁的理解及如何实现,有哪些实现方式

    该文章专注于面试,面试只要回答关键点即可,不需要对框架有非常深入的回答,如果你想应付面试,是足够了,抓住关键点 悲观锁的思想是“总是假设最坏的情况会发生”,悲观锁的核心理念是假设数据库中的资源很可能会发生冲突,因此在访问资源之前会先对其进行加锁

    2024年04月11日
    浏览(22)
  • 悲观锁&乐观锁

    1.悲观锁 悲观锁介绍(百科): 悲观锁,正如其名,它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度,因此,在整个数据处理过程中,将数据处于锁定状态。悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层

    2024年02月08日
    浏览(27)
  • 悲观锁和乐观锁、缓存

    悲观锁: 悲观锁的实现通常依赖于数据库提供的机制,在整个处理的过程中数据处于锁定状态,session的load方法有一个重载方法,该重载方法的第三个参数可以设置锁模式,load(object.class , int id,LockMode.?),该方法的?就是具体的锁模式。 乐观锁: 乐观锁使用版本号或者时间戳

    2024年02月09日
    浏览(34)
  • 悲观锁和乐观锁(易懂)

    这里可以把悲观锁看作悲观的人,啥事都往最坏的方向想。乐观锁看作乐观的人,啥事都往最好的方向想。 首先,说一下悲观锁。 悲观锁就是假设并发情况下一定会有其他线程来修改数据,因此在处理数据之前,先将数据锁住,确保其他线程不能进行修改 。感觉像一个过于

    2024年02月08日
    浏览(24)
  • 什么是乐观锁和悲观锁?

    乐观锁和悲观锁是并发控制的两种不同策略,用于在多线程环境下管理共享资源的访问。它们有不同的思想和实现方式: 悲观锁(Pessimistic Locking) : 思想 :悲观锁的思想是,它假定在并发访问中会发生冲突,因此在访问共享资源之前会先加锁,以防止其他线程访问。悲观

    2024年02月10日
    浏览(26)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包