@@@ 在安全性和活跃性之间通常存在着某种权衡。我们使用加锁机制来确保线程安全,但如果
过度地使用加锁,则可能导致锁顺序死锁。
@@@ 我们使用线程池和信号量来限制对资源的使用,但这些被限制的行为可能会导致资源死锁。
@@@ Java 程序无法中死锁中恢复过来,因此在设计时一定要排除那些可能导致死锁出现的条件。
》》死锁
@@@ 在数据库系统设计中考虑了监测死锁以及从死锁中恢复。
---------- 当数据库系统检测到一组事务发生了死锁时(通过在表示等待关系的有向图中搜索循环),
将选择一个牺牲者并放弃这个事务。作为牺牲者的事务会释放它所持有的资源,从而使其他
事务继续进行。应用程序可以重新执行被强行中止的事物,而这个事务现在可以成功完成,
因为所有跟它竞争资源的事务都已经完成了。
@@@ JVM 在解决死锁问题方面并没有数据库服务那样强大。恢复应用程序的唯一方式就是中止
并重启它,并希望不要再发生同样的事情。
@@@ 与许多其他的并发危险一样,死锁造成的影响很少会立即显现出来。如果一个类可能发生死锁,
那么并不意味着每次都会发生死锁,而只是表示有可能。当死锁出现时,往往是在最糟糕的时候------
在高负载情况下。
### 锁顺序死锁
@@@ 如果所有的线程以固定的顺序来获得锁,那么在程序中就不会出现锁顺序死锁问题。
@@@ 要想验证锁顺序的一致性,需要对程序中的加锁行为进行全局分析。
### 动态的锁顺序死锁
@@@ 动态的锁顺序死锁可以采用程序清单的方法来检查--------查看是否存在嵌套的锁获取操作。
解决动态的锁顺序死锁问题的方法:定义锁的顺序,并在整个应用程序中都按照这个顺序来获取锁。
@@@ 如果在 Account 中包含一个唯一的 、 不可变的 ,并且具备可比性的键值,例如账号,
要制定锁的顺序就更加容易了:通过键值对对象进行排序,因而不需要使用 “ 加时赛 ” 锁。
@@@ 即使应用程序通过了压力测试也不可能找出所有的潜在的死锁。
### 在协作对象之间发生的死锁
@@@ 如果在持有锁时调用某个外部方法,那么将出现活跃性问题。在这个外部方法中可能会
获取其他锁(这可能会产生死锁),或者阻塞时间过长,导致其他线程无法及时获得当前被持有
的锁。
### 开放调用
@@@ 如果在调用某个方法时不需要持有锁,那么这种调用被称为开放调用。
@@@ 通过开放调用来避免死锁的方法,类似于采用封装机制来提供线程安全的方法:虽然在
没有封装的情况下也能确保构建线程安全的程序,但对一个使用了封装的程序进行线程安全分析,
要比分析没有使用封装的程序容易得多。
@@@ 通过尽可能地使用开放调用,将更易于找出那些需要获得多个锁的代码路径,因此也就
更容易确保采用一致的顺序来获得锁。
@@@ 收缩同步代码块的保护范围可以提高可伸缩性。
@@@ 在程序中应尽量使用开放调用。与那些在持有锁时调用外部方法的程序相比,更易于对
依赖于开放调用的程序进行死锁分析。
@@@ 在许多情况下,使某个操作失去原子性是可以接受的。
然而,在某些情况下,失去原子性会引发错误。
### 资源死锁
@@@ 有界线程池 / 资源池与相互依赖的任务不能一起使用。
》》死锁的避免与诊断
@@@ 如果一个程序必须获取多个锁,那么在设计时必须考虑锁的顺序: 尽量减少潜在的加锁
交互数量,将获取锁时需要遵循的协议写入正式文档并始终遵循这些协议。
@@@ 在使用细粒度锁的程序中,可以通过使用一种两阶段策略来检查代码中的死锁:首先,找出
在什么地方将获取多个锁(使这个集合尽量小),然后对所有这些实例进行全局分析,从而确保它们
它们在整个程序中获取锁的顺序都保持一致。
尽可能地使用开放调用,这能极大地简化分析过程。如果所有的调用都是开放调用,那么
要发现获取多个锁的实例是非常简单的,可以通过代码审查,或者借助自动化的源代码分析工具。
### 支持定时的锁
@@@ 还有一项技术可以检测死锁和从死锁中恢复过来,即显式使用 Lock 类中的定时 tryLock
功能来代替内置锁机制。
------- 当使用内置锁时,只要没有获得锁,就会永远等待下去
------- 显式锁可以指定一个超时时限(Timeout),在等待超过该时间后 tryLock 会返回一个失败
信息
如果超时时限比获取锁的时间要长很多,那么就可以在发生某个意外情况后重新获得控制权。
@@@ 当定时锁失败时,你并不需要知道失败的原因。
@@@ 即使在整个系统中没有始终使用定时锁,使用定时锁来获取多个锁也能有效地应对死锁问题。
如果在获取锁时超时,那么可以释放这个锁,然后后退并在一段时间后再尝试,从而消除了死锁发生
的条件,使程序恢复过来。
### 通过线程转储信息来分析死锁
@@@ 虽然防止死锁的主要责任在于你自己,但 JVM 仍然通过线程转储(Tread Dump)来帮助
识别死锁的发生。
@@@ 线程转储包括各个运行中的线程的栈追踪信息。
@@@ 线程转储包括加锁信息,例如每个线程持有了哪些锁,在哪些栈帧中获得这些锁,以及被
阻塞的线程正在等待获取哪一个锁。
@@@ 在生成线程转储之前,JVM 将在等待关系图中通过搜索循环来找出死锁。如果发现了一个
死锁,则获取相应的死锁信息,例如在死锁中涉及哪些锁和线程,以及这个锁的获取操作位于程序
的哪些位置。
@@@ 要在 UNIX 平台上触发线程转储操作,可以通过向 JVM 的进程发送 SIGQUIT 信号。或者在
UNIX 平台中按下 Ctrl-\ 键,在 Windows 平台中按下 Ctrl-Break 键。
在许多 IDE (集成开发环境)中都可以请求线程转储。
@@@ 内置锁与获得它们所在的线程栈帧是相关联的,而显式的 Lock 只与获得它的线程相关联。
@@@ 当诊断死锁时,JVM 可以帮我们做许多工作------哪些锁导致了这个问题,涉及哪些线程,
它们持有哪些其他的锁,以后是否间接地给其他线程带来了不利影响。
》》其他活跃性危险
@@@ 尽管死锁是最常见的活跃性危险,但在并发程序中还存在一些其他的活跃性危险,包括:
饥饿 、 丢失信号和活锁等。
### 饥饿
@@@ 当线程无法访问它所需要的资源而不能继续执行时,就发生了“ 饥饿 (Starvation)”。
引发饥饿的最常见资源就是 CPU 时钟周期。
@@@ 如果在 Java 应用程序中对线程的优先级使用不当,或者在持有锁时执行一些无法结束的
结构(例如无限循环,或者无限制地等待某个资源),那么也可能导致饥饿,因为其他需要这个锁
的线程将无法得到它。
@@@ 在 Thread API 中定义了 10 个优先级, JVM 根据需要将它们映射到操作系统的调度优先级。
这种映射是与特定平台相关的,因此在某个操作系统中两个不同的 Java 优先级可能被映射到同一个
优先级,而在另一个操作系统中则可能被映射到另一个不同的优先级。在某些操作系统中,如果优先
级的数量小于 10 个, 那么多个 Java 优先级会被映射到同一个优先级。
@@@ 通常,我们尽量不要改变线程的优先级。只要改变了线程的优先级,程序的行为就将与平台
相关,并且会导致发生饥饿问题的风险。
@@@ 要避免使用线程优先级,因为这会增加平台依赖性,并可能导致活跃性问题。在大多数并发
应用程序中,都可以使用默认的线程优先级。
### 糟糕的响应性
@@@ 如果在 GUI 应用程序中使用了后台线程,那么糟糕的响应性问题是很常见的。
----------- 如果由其他线程完成的工作都是后台任务,那么应该降低它们的优先级,从而提高前台程序
的响应性。
@@@ 不良的锁管理也可能导致糟糕的响应性。
### 活锁
@@@ 活锁(LiveLock)不会阻塞线程,但也不能继续执行,因为线程将不断重复执行相同的操作,
而且总会失败。
@@@ 活锁通常发生在处理事务消息的应用程序中。
--------- 如果不能成功地处理某个消息,那么消息处理机制将回滚整个事务,并将它重新放到队列的
开头。
---------- 如果消息处理器在处理某种特定类型的消息时存在错误 并导致它失败,那么每当这个消息从
队列中取出并传递到存在错误的处理器时,都会发生事务回滚。由于这条消息又被放回到队列
开头,因此处理器将被反复调用,并返回相同的结果。
补充:
活锁通常是由过度的错误恢复代码造成的,因为它错误地将不可修复的错误作为可修复的错误。
@@@ 当多个相互协作的线程都对彼此进行响应从而修改各自的状态,并使得任何一个线程都无法
执行时,就发生了活锁。
@@@ 要解决活锁问题,需要在重试机制中引入随机性。
-------- 在并发应用程序中,通常等待随机长度的时间和回退可以有效地避免活锁的发生。
》》小结
@@@ 活跃性故障是一个非常严重的问题,因为当出现活跃性故障时,除了中止应用程序之外没有
其他任何机制可以帮助从这种故障中恢复过来。
@@@ 最常见的活跃性故障就是锁顺序死锁。
--------- 在设计时应该避免产生锁顺序死锁:确保线程在获取多个锁时采用一致的顺序。
@@@ 最好的解决方法是在程序中始终使用开放调用。这将大大减少需要同时持有多个锁的地方,
也更容易发现这些地方。