第 10 章 避免活跃性危险

本文深入探讨并发编程中常见的活跃性问题,如死锁、饥饿、活锁及其实现细节。强调了在设计时避免锁顺序死锁的重要性,并介绍了使用开放调用和定时锁来预防死锁的策略。同时,文章提供了如何通过线程转储信息来诊断和分析死锁的实用方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

              @@@  在安全性活跃性之间通常存在着某种权衡。我们使用加锁机制来确保线程安全,但如果

              过度地使用加锁,则可能导致锁顺序死锁

              @@@   我们使用线程池和信号量来限制对资源的使用,但这些被限制的行为可能会导致资源死锁

              @@@   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)不会阻塞线程,但也不能继续执行,因为线程将不断重复执行相同的操作,

                而且总会失败。

                @@@   活锁通常发生在处理事务消息的应用程序中

                ---------    如果不能成功地处理某个消息,那么消息处理机制将回滚整个事务,并将它重新放到队列的

                            开头。

                ----------    如果消息处理器在处理某种特定类型的消息时存在错误 并导致它失败,那么每当这个消息从

                            队列中取出并传递到存在错误的处理器时,都会发生事务回滚。由于这条消息又被放回到队列

                            开头,因此处理器将被反复调用,并返回相同的结果。

                补充:

                          活锁通常是由过度的错误恢复代码造成的,因为它错误地将不可修复的错误作为可修复的错误

               @@@    当多个相互协作的线程都对彼此进行响应从而修改各自的状态,并使得任何一个线程都无法

               执行时,就发生了活锁

               @@@   要解决活锁问题,需要在重试机制中引入随机性

                --------   在并发应用程序中,通常等待随机长度的时间和回退可以有效地避免活锁的发生。

》》小结

               @@@   活跃性故障是一个非常严重的问题,因为当出现活跃性故障时,除了中止应用程序之外没有

               其他任何机制可以帮助从这种故障中恢复过来。

               @@@   最常见的活跃性故障就是锁顺序死锁

               ---------  在设计时应该避免产生锁顺序死锁:确保线程在获取多个锁时采用一致的顺序。

               @@@   最好的解决方法是在程序中始终使用开放调用。这将大大减少需要同时持有多个锁的地方,

                也更容易发现这些地方。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小达人Fighting

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值