Java并发 -- 死锁
Account.class
- 在《Java并发 – 互斥锁》中,使用了Account.class作为互斥锁来解决银行业务的转账问题
- 虽然不存在并发问题,但所有账户的转账操作都是串行的,性能太差
- 例如账户A给账户B转账,账户C给账户D转账,在现实世界中是可以并行的,但该方案中只能串行
账户和账本
- 每个账户都对应一个账本,账本统一存放在文件架上
- 银行柜员进行转账操作时,需要到文件架上取出转出账本和转入账本,然后转账操作,会遇到三种情况
- 如果文件架上有转出账本和转入账本,都同时拿走
- 如果文件架上只有转出账本或只有转入账本,那需要等待那个缺失的账本
- 如果文件架上没有转出账本和转入账本,那需要等待两个账本
两把锁
1 | public class Account { |
死锁
- 两把锁是细粒度锁的方案,使用细粒度锁可以提高并发度,是性能优化的一个重要手段,但可能会导致死锁
- 死锁:_一组相互竞争资源的线程因互相等待,导致永久阻塞的现象_
- 场景
- 假设线程T1执行账户A给账户B转账的操作,同时线程T2执行账户B给账户A转账的操作
- 即A.transfer(B),B.transfer(A)
- 当T1和T2同时执行完1处的代码,此时,T1获得了账户A的锁,T2获得了账户B的锁
- 之后T1和T2在执行2处的代码时
- T1试图获取账户B的锁,发现账户B已经被锁定,T1等待
- T2试图获取账户A的锁,发现账户A已经被锁定,T2等待
- T1和T2会无限期地等待,形成死锁
- 假设线程T1执行账户A给账户B转账的操作,同时线程T2执行账户B给账户A转账的操作
规避死锁
- 并发程序一旦死锁,一般只能重启应用,解决死锁问题最好的办法是_规避死锁_
- 死锁发生的条件
- 互斥:共享资源X和共享资源Y只能被一个线程占用
- 占有且等待:线程T1占有共享资源X,在等待共享资源Y的时候,不会释放共享资源X
- 不可抢占:其他线程不能强行抢占线程已经占有的共享资源
- 循环等待:线程T1等待线程T2占有的资源,线程T2等待线程T1占有的资源
- 规避死锁的思路:破坏死锁发生的条件
- 互斥:无法破坏,因为用锁的目的就是为了互斥
- 占有且等待:一次性申请所有的共享资源,不存在等待
- 不可抢占:占有部分共享资源的线程进一步申请其他共享资源时,如果申请不到,可以主动释放它所占用的共享资源
- 循环等待:按序申请共享资源(共享资源是有线性顺序的)
破坏 – 占有且等待
Allocator
1 | public class Allocator { |
Account
1 | public class Account { |
破坏 – 不可抢占
- 破坏不可抢占条件的核心是主动释放它所占有的共享资源,这一点synchronized是做不到的
- synchronized在申请资源的时候,如果申请不到,线程直接进入阻塞状态,并不会释放已占有的共享资源
- Java在语言层次并没有解决该问题,但在SDK层面解决了(JUC提供的LOCK)
破坏 – 循环等待
比破坏占有且等待条件的成本低
1 | public class Account { |
参考资料
All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.