【并发篇】深入理解线程死锁、乐观锁与悲观锁

深入理解线程死锁、乐观锁与悲观锁

前言

在多线程编程中,线程死锁、乐观锁和悲观锁是三个重要的概念。本文将深入解析这三个概念,包括它们的定义、产生原因、条件、预防和避免方法,以及如何实现。

死锁问题

什么是线程死锁?

线程死锁是指:两个或多个线程互相持有对方所需要的资源而互相等待的状态,导致程序无法继续执行下去,进而陷入死循环,无法完成任务。

死锁产生的原因

通常情况下,线程死锁产生的原因是: 两个或多个线程对资源的竞争和不当的资源分配

死锁产生的条件

线程死锁的产生通常需要同时满足以下四个条件

  1. 互斥条件:该资源任意一个时刻只由一个线程占用。
  2. 请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放。
  3. 不剥夺条件:线程已获得的资源在未使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
  4. 循环等待条件: 若干线程之间形成一种头尾相接的循环等待资源关系。

如何预防死锁?

预防死锁只要破坏死锁产生的必要条件即可:

  1. 破坏请求与保持条件:一次性申请所有的资源。
  2. 破坏不剥夺条件:占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
  3. 破坏循环等待条件:靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。

如何避免死锁?

避免死锁就是在资源分配时,借助于算法(比如银行家算法)对资源分配进行计算评估,使其进入安全状态

乐观锁和悲观锁

什么是乐观锁?

乐观锁总是假设最好的情况,认为共享资源每次被访问的时候不会出现问题,线程可以不停地执行,无需加锁也无需等待,只是在提交修改的时候去验证对应的资源是否被其它线程修改了。

优点

不会造成线程阻塞

缺点

在并发更新的情况下,可能会出现 ABA 问题,需要使用版本号或时间戳等机制来解决。

ABA 问题是: 在使用 CAS 算法时可能出现的一个问题。

它的本质是: 由于线程之间的竞争,导致共享数据的值在某个时间点被修改为 A,然后又被修改为 B,最后再被修改回 A,

这时候使用 CAS 算法时,比较的是共享数据的值是否等于 A,如果等于 A,则执行操作,但实际上共享数据的值已经被修改过了

简单来说,就是在使用 CAS 算法的时候发生了误判。

典型代表

比如:使用版本号机制、CAS 算法

什么是悲观锁?

悲观锁总是假设最坏的情况,认为共享资源每次被访问的时候就会出现问题(比如共享数据被修改),所以每次在获取资源操作的时候都会上锁,这样其他线程想拿到这个资源就会阻塞直到锁被上一个持有者释放。也就是说,共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程。

优点

安全,能够保证数据操作的正确性和一致性。

缺点

悲观锁的缺点是在高并发的情况下,会造成大量的线程阻塞,降低系统的性能。

典型代表

比如:Java 中 的 synchronizedReentrantLock 等独占锁,数据库中的行级锁和表级锁。

如何实现乐观锁?

版本控制

在操作共享资源之前,先读取数据的版本号,然后将操作结果与当前版本号进行比较,如果版本号一致,则可以进行操作,如果版本号不一致,则说明数据已被其他线程修改,需要回滚并重试。

CAS 算法

CAS 的全称是 Compare And Swap(比较与交换),用于实现乐观锁,被广泛应用于各大框架中。

CAS 的思想很简单,就是用一个预期值和要更新的变量值进行比较,两值相等才会进行更新

是一个原子操作,底层依赖于一条 CPU 的原子指令。

原子操作 即最小不可拆分的操作,也就是说操作一旦开始,就不能被打断,直到操作完成。

如何实现悲观锁?

悲观锁的实现方式主要有两种:基于数据库的悲观锁和基于代码的悲观锁。

  1. 基于数据库的悲观锁

    基于数据库的悲观锁是通过数据库的锁机制来实现的。在数据库中,可以通过 SELECT ... FOR UPDATE 语句或 SELECT ... FOR SHARE 语句来获取悲观锁。

    • 当一个事务执行 SELECT ... FOR UPDATE 语句时,数据库会将所选的行加上排他锁,其他事务不能修改这些行;
    • 当一个事务执行 SELECT ... FOR SHARE 语句时,数据库会将所选的行加上共享锁,其他事务只能读取这些行,不能修改。在使用完锁后,需要及时释放锁,避免长时间占用数据库资源。
  2. 基于代码的悲观锁

    基于代码的悲观锁是通过程序代码来实现的。在 Java 中,可以使用 synchronized 关键字或 Lock 接口来实现悲观锁

    • 使用 synchronized 关键字时,需要在方法或代码块上加锁,以确保同一时间只有一个线程可以执行这段代码;
    • 使用 Lock 接口时,需要先获取锁(调用 lock() 方法),然后执行操作,最后释放锁(调用 unlock() 方法),以确保同一时间只有一个线程可以操作共享资源。

学习参考

  • 4
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值