使用细粒度锁可能会导致死锁
死锁:一组互相竞争资源的线程因互相等待,导致永久阻塞的现象
如何产生死锁
- 互斥,共享资源X和Y只能被一个线程占用
- 占有且等待,线程T1已经取得了共享资源X,在等待共享资源Y的时候,不释放共享资源X
- 不可抢占,其他线程不能强行抢占线程T1占有的资源
- 循环等待,线程T1等待线程T2占有的资源,线程T2等待线程T1占有的资源
避免死锁(只要我们能够破坏上条的一个条件就可以成功避免)
- 互斥无法避免,因为锁用的就是互斥锁
- 对于占有且等待,可以一次性申请全部资源
- 对于不可抢占,占有一部分资源的线程想继续申请其他资源时,如果申请不到,可以主动释放他占有的资源,以退为进(如果申请不到是因为其他线程占用并且等待当前线程所占有的资源时,释放掉资源把他要的给他,等他执行完了咱们在执行)
- 对于循环等待,可以靠按序申请资源来预防。所谓按需申请是指资源是存在线性顺序的,申请的时候可以先申请资源序号小的,在申请大的,这样线性化后就不会循环等待了
案例:
破坏占用且等待条件
- “同时申请”这个操作是一个临界区,我们用 Allocator这个类来管理这个临界区。它有两个重要功能:同时申请资源 apply() 和同时释放资源 free()。
- 账户 Account 类里面持有一个 Allocator 的单例(必须是单例,只能由一个人来分配资源)。当账户 Account 在执行转账操作的时候,首先向 Allocator 同时申请转出账户和转入账户这两个资源,成功后再锁定这两个资源;当转账操作执行完,释放锁之后,我们需通知 Allocator 同时释放转出账户和转入账户这两个资源。
破坏不可抢占条件
- 主动释放本线程占有的资源,这一点synchronized无法做到。
- 原因是 synchronized 申请资源的时候,如果申请不到,线程直接进入阻塞状态了,而线程进入阻塞状态,啥都干不了,也释放不了线程已经占有的资源。
- 不过在 SDK 层面还是解决了的,java.util.concurrent 这个包下面提供的 Lock 是可以轻松解决这个问题的。(提供tryLock(long, TimeUnit) 方法,在一段时间尝试获取锁。)
破坏循环等待条件
- 破坏这个条件,需要对资源进行排序,然后按序申请资源。我们假设每个账户都有不同的属性 id,这个 id 可以作为排序字段,申请的时候,我们可以按照从小到大的顺序来申请。比如对转出账户(this)和转入账户(target)排序,然后按照序号从小到大的顺序锁定账户。这样就不存在“循环”等待了。