过度地使用加锁,可能导致锁顺序死锁;
过渡的使用线程池和信号量来限制对资源的使用,可能导致资源死锁;
10.1 死锁
java无法从死锁恢复;
数据库设计一般可以恢复,会选择一个牺牲者放弃这个事务。
10.1.1 锁顺序死锁
最常见的活跃性问题
如果所有线程以固定的顺序来获得锁,那么程序中就不会出现锁顺序死锁问题。
right -》left
left -》right
10.1.2 动态的锁顺序死锁
有时候并不清楚是否在锁顺序上有足够的控制权来避免死锁发生。
10.1.3 在协作对象之间发生的死锁
如果在持有锁时调用某个外部方法,那么将出现活跃性问题。在这个外部方法中可能会获取其他锁(这可能会产生死锁),或者阻塞时间过长,导致其他线程无法及时获得当前被持有的锁。
10.1.4 开放调用
最好的解决办法
如果在调用某个方法时不需要持有锁,那么这种调用被称为开放调用。
在程序中应尽可能使用开放调用,与持有锁时调用外部方法的程序相比,更易于对依赖于开房调用的程序进行死锁分析。
10.1.5 资源死锁
多个线程相互持有彼此正等待的锁而又不释放自己已持有的锁;
线程饥饿死锁:一个任务提交另一个任务,并等待被提交的任务在单线程的Executor中执行完成。有界线程池和相互依赖的任务不能一起使用。
10.2 死锁的避免与诊断
如果一个程序每次至多只能获得一个锁,那么就不会产生锁顺序死锁。
如果必须获得多个锁,那么设计时必须考虑锁的顺序:尽量减少潜在的加锁交互数量。
细粒度锁程序中,可以使用两阶段策略来检查代码中的死锁。
10.2.1 支持定时的锁
Lock中的tryLock代替内置锁,避免永远等待下去。显示锁可指定一个超时timeout
10.2.2 通过线程转储信息来分析死锁
Thread Dump。JVM将在等待关系图中通过搜索循环来找出死锁。
Lock转储没有内置锁信息精确度高,只能和线程相关联,内置锁可以做到栈帧关联。
10.3 其他活跃性危险
除了死锁,还有饥饿、丢失信号和活锁。
10.3.1 饥饿
避免使用线程优先级,会增加平台依赖性,可能导致活跃性问题。在大多数并发应用程序中,都可以使用默认的线程优先级。
10.3.2 糟糕的响应性
不良的锁管理:长时间占有、大容器迭代、密集计算,导致其它线程等待较长。
可以降低后台任务优先级,从而提高前台程序响应性
10.3.3 活锁
活锁不会阻塞线程,但也不能继续执行,因为线程不断重复执行相同的操作,而且总会失败。
例如处理事务消息的应用程序:失败后回滚整个事务,并重新放到队列的开头。如此反复。这也称为毒药消息,这种活锁是由过度的错误恢复代码造成的:错误的将不可修复的错误作为可修复的错误。
又例如两个人相遇,让路,反反复复。
解决活锁问题:需要在重试机制中引入随机性。