锁详解
# 锁详解
# 死锁问题
# 什么是线程死锁?
线程死锁是指:两个或多个线程互相持有对方所需要的资源而互相等待的状态,导致程序无法继续执行下去,进而陷入死循环,无法完成任务。
# 死锁产生的原因
通常情况下,线程死锁产生的原因是: 两个或多个线程对资源的竞争和不当的资源分配。
# 死锁产生的条件
线程死锁的产生通常需要同时满足以下四个条件:
- 互斥条件:该资源任意一个时刻只由一个线程占用。
- 请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件:线程已获得的资源在未使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
- 循环等待条件: 若干线程之间形成一种头尾相接的循环等待资源关系。
# 如何预防死锁?
预防死锁只要破坏死锁产生的必要条件即可:
- 破坏请求与保持条件:一次性申请所有的资源。
- 破坏不剥夺条件:占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
- 破坏循环等待条件:靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。
# 如何避免死锁?
避免死锁就是在资源分配时,借助于算法(比如银行家算法)对资源分配进行计算评估,使其进入安全状态。
# 乐观锁和悲观锁
# 什么是乐观锁?
乐观锁总是假设最好的情况,认为共享资源每次被访问的时候不会出现问题,线程可以不停地执行,无需加锁也无需等待,只是在提交修改的时候去验证对应的资源是否被其它线程修改了。
优点
不会造成线程阻塞
缺点
在并发更新的情况下,可能会出现 ABA 问题,需要使用版本号或时间戳等机制来解决。
ABA 问题是: 在使用 CAS 算法时可能出现的一个问题。
它的本质是: 由于线程之间的竞争,导致共享数据的值在某个时间点被修改为 A,然后又被修改为 B,最后再被修改回 A,
这时候使用 CAS 算法时,比较的是共享数据的值是否等于 A,如果等于 A,则执行操作,但实际上共享数据的值已经被修改过了。
简单来说,就是在使用 CAS 算法的时候发生了误判。
典型代表
比如:使用版本号机制、CAS 算法
# 什么是悲观锁?
悲观锁总是假设最坏的情况,认为共享资源每次被访问的时候就会出现问题(比如共享数据被修改),所以每次在获取资源操作的时候都会上锁,这样其他线程想拿到这个资源就会阻塞直到锁被上一个持有者释放。也就是说,共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程。
优点
安全,能够保证数据操作的正确性和一致性。
缺点
悲观锁的缺点是在高并发的情况下,会造成大量的线程阻塞,降低系统的性能。
典型代表
比如:Java 中 的 synchronized
和 ReentrantLock
等独占锁,数据库中的行级锁和表级锁。
# 如何实现乐观锁?
# 版本控制
在操作共享资源之前,先读取数据的版本号,然后将操作结果与当前版本号进行比较,如果版本号一致,则可以进行操作,如果版本号不一致,则说明数据已被其他线程修改,需要回滚并重试。
# CAS 算法
CAS 的全称是 Compare And Swap(比较与交换),用于实现乐观锁,被广泛应用于各大框架中。
CAS 的思想很简单,就是用一个预期值和要更新的变量值进行比较,两值相等才会进行更新。
是一个原子操作,底层依赖于一条 CPU 的原子指令。
原子操作 即最小不可拆分的操作,也就是说操作一旦开始,就不能被打断,直到操作完成。
# 如何实现悲观锁?
悲观锁的实现方式主要有两种:基于数据库的悲观锁和基于代码的悲观锁。
基于数据库的悲观锁
基于数据库的悲观锁是通过数据库的锁机制来实现的。在数据库中,可以通过
SELECT ... FOR UPDATE
语句或SELECT ... FOR SHARE
语句来获取悲观锁。- 当一个事务执行
SELECT ... FOR UPDATE
语句时,数据库会将所选的行加上排他锁,其他事务不能修改这些行; - 当一个事务执行
SELECT ... FOR SHARE
语句时,数据库会将所选的行加上共享锁,其他事务只能读取这些行,不能修改。在使用完锁后,需要及时释放锁,避免长时间占用数据库资源。
- 当一个事务执行
基于代码的悲观锁
基于代码的悲观锁是通过程序代码来实现的。在 Java 中,可以使用
synchronized
关键字或Lock
接口来实现悲观锁。- 使用
synchronized
关键字时,需要在方法或代码块上加锁,以确保同一时间只有一个线程可以执行这段代码; - 使用
Lock
接口时,需要先获取锁(调用lock()
方法),然后执行操作,最后释放锁(调用unlock()
方法),以确保同一时间只有一个线程可以操作共享资源。
- 使用