一、死锁
1.1 定义:
当线程 A 持有独占锁a,并尝试去获取独占锁 b 的同时,线程 B 持有独占锁 b,并尝试获取独占锁 a 的情况下,就会发生 AB 两个线程由于互相持有对方需要的锁,而发生的阻塞现象,我们称为死锁。
1.2 死锁产生的条件:
- 互斥条件:进程对所分配到的资源具有排他性,即一个资源只能被一个进程占用,直到被该进程释放
- 请求和保持条件:一个进程因请求被占用资源而发生阻塞时,对已获得的资源保持不放
- 不剥夺条件:任何一个资源在没有被该进程释放之前,对已获得的资源保持不放
- 循环等待条件:当发生死锁时,所等待的进程必定会形成一个环路,造成永久阻塞
1.3 如何防止死锁:
- 尽量使用 tryLock(long timeout, TimeUnit unit)的方法(ReentrantLock、ReentrantReadWriteLock),设置超时时间,超时可以退出防止死锁。
- 尽量使用 Java. util. concurrent 并发类代替自己手写锁。
- 尽量降低锁的使用粒度,尽量不要几个功能用同一把锁。
- 尽量减少同步的代码块。
- 死锁检测:每当一个线程获得了锁,会在线程和锁相关的数据结构中(map、graph)将其记下。每当线程请求锁,也需要记录在这个数据结构中。针对不能实现按序加锁并且锁超时不可行的场景
二、ReentrantLock
2.1 synchronized 和 Lock的区别(如ReentrantLock)
- synchronized 可以给
类、方法、代码块
加锁;而 lock 只能给代码块
加锁。 - synchronized 不需要手动获取锁和释放锁,使用简单,发生异常会自动释放锁,不会造成死锁;而 lock 需要自己加锁和释放锁,如果使用不当没有 unLock()去释放锁就会造成死锁。
- 通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到。
2.2 特点
可重入性:若一个程序或子程序可以“安全的被并行执行(Parallel computing)”,则称其为可重入(reentrant或re-entrant)的。即当该子程序正在运行时,可以再次进入并执行它(并行执行时,个别的执行结果,都符合设计时的预期)。
即线程可以重复获取同一把锁
三、数据库锁
行锁:基于索引加载,锁一行或多行,锁定时其他事务无法访问。缺点:可能会造成死锁
表锁:全表扫描,锁定时其他事务不能对表进行访问
记录锁:锁表中的某一条数据,命中索引为唯一索引
间隙锁:锁定一个区间,查询条件命中索引,并且没有查询到符合条件的记录,此时就会将查询条件中的范围数据进行锁定(即使是范围库中不存在的数据也会被锁定)。只适用于可重复读
的事务隔离级别
临界锁:查询条件命中索引,并且查询到符合条件的记录。临键锁锁定区间和查询范围后匹配值很重要,如果后匹配值存在,则只锁定查询区间,否则锁定查询区间和后匹配值与它的下一个值的区间。(原因:当我们的索引树上只有1、5、7时,我们查询1-8,这个时候由于树节点关键字中并没有8,所以就把8到正无穷的区间范围都给锁定了;如果我们数据库中id有1、5、7、10,此时我们再模糊匹配id为1~8的时候,由于关键字中并没有8,所以找比8大的,也就找到了10,根据左开右闭原则,此时10也是被锁定的,但是id为11的记录还是可以正常进行插入的。)
四、分布式锁
4.1 分布式锁的作用
- 避免不同节点重复工作
- 避免多节点同时操作同一资源,导致结果不一致
4.2 分布式锁的特点
- 互斥性:和我们本地锁一样互斥性是最基本,但是分布式锁需要保证在不同节点的不同线程的互斥。
- 可重入性:同一个节点上的同一个线程如果获取了锁之后那么也可以再次获取这个锁。
- 锁超时:和本地锁一样支持锁超时,防止死锁。
高效,高可用:加锁和解锁需要高效,同时也需要保证高可用防止分布式锁失效,可以增加降级。 - 支持阻塞和非阻塞:和ReentrantLock一样支持lock和trylock以及tryLock(long timeOut)。
- 支持公平锁和非公平锁(可选):公平锁的意思是按照请求加锁的顺序获得锁,非公平锁就相反是无序的。
4.3 常见的分布式锁
- MySql
- Zk
- Redis
- 自研分布式锁:如谷歌的Chubby
redis分布式锁:
// 设置过期时间,并对资源加锁
// Redis2.8之后支持nx和ex是原子操作,之前需要使用lua脚本
set resourceName value ex 5 nx
Redisson:为Redis的客户端,整合了分布式锁功能。实现像操作本地锁一样操作Redis的分布式锁。
RedLock:大多数集群加锁成功。避免Redis宕机,一个节点获取锁后,第二个节点获取同一把锁的问题。