《Java并发编程实战》-9

第10章 避免活跃性危险


10.1 死锁

哲学家问题

10.1.1 锁顺序死锁

如果所有线程以固定的顺序来获得锁,那么在线程中就不会出现锁顺序死锁问题。

10.1.2 动态的顺序死锁

通过锁顺序来避免死锁

10.1.3 在协助对象之间发生的死锁

如果在持有锁时调用某个外部方法,那么将出现活跃性问题。在这个外部方法中可能会获取其他锁(这可能会产生死锁),或者阻塞时间过长,导致其他线程无法及时获得当前被持有的锁。

10.1.4 开放调用

如果在调用某个方法时不需要持有锁,那么这种调用被称为开放调用(Open Call)。

在程序中应尽量使用开发调用。与那些在持有锁时调用外部方法的程序相比,更易于对依赖于开放调用的程序进行死锁分析。

10.1.5 资源死锁

当多个线程早相同的资源集合上等待时,也会发生死锁。

10.2 死锁的避免与诊断

10.2.1 支持定时的锁

显式使用Lock类中定时tryLock功能代替内置锁机制可以检测死锁和从死锁中恢复过来。

10.2.2 通过线程转储信息来分析死锁

虽然防止死锁的主要责任在于你自己,但JVM仍然通过线程转储(Thread Dump)来帮助识别死锁的发生。线程转储包括各个运行中的线程的栈追踪信息,这类似发生异常时的栈追踪信息。

10.3 其他活跃性危险

活跃性危险:

  • 死锁
  • 饥饿
  • 丢失信号
  • 活锁

10.3.1 饥饿

当线程由于无法访问它所需要的资源而无法继续执行时,就发生了“饥饿”,引发饥饿的最常见资源就是CPU时钟周期。

要避免使用线程优先级,因为这会增加平台依赖性,并可能导致活跃性问题。在大多数并发应用程序中,都可以使用默认的线程优先级。

10.3.2 糟糕的响应性

10.3.3 活锁

活锁是另一种形式的活跃性问题,该问题尽管不会阻塞线程,但也不能继续执行,因为线程将不断重复地执行相同的操作,并且总会失败。活锁通常发生在处理事务消息的应用程序中:如果蹦年成功处理某个消息,那么消息处理机制将回滚整个事务,并且将它重新发到队列的开头。

总结

活跃性故障时一个非常严重的问题,因为当出现活跃性故障时,除了终止应用程序之外没有其他任何机制可以邦帮助从这些故障中恢复过来。最常见的活跃性故障就是所顺序死锁。在设计时应该避免产生锁顺序死锁:确保线程在获取多个锁时采用一致的顺序。最好的解决方法是在程序中始终使用开放调用。这种大大减少需要同时持有多个锁的地方,也更容易发现这些地方。

《Java并发编程实战》-8

图形用户界面应用程序

9.1 为什么GUI是单线程的

9.1.1 串行时间处理

9.1。2 Swing中的单线程封闭机制

Swing的单线程规则是:Swing中的组件以及模型只能在这个事件分发线程中进行创建、修改以及查询。

9.2 短时间的GUI任务

模型对象与视图对象的控制流

st=>start: EDT op1=>operation: 鼠标点击 op2=>operation: 动作事件 op3=>operation: 动作监听者 op4=>operation: 更新表格模型 op5=>operation: 表格修改事件 op6=>operation: 表格监听者 op7=>operation: 更新表格视图 st->op1->op2->op3->op4->op5->op6->op7

9.3 长时间的GUI任务

9.3.1 取消

9.3.2 进度标识和完成标识

9.3.3 SwingWorker

9.4 共享数据模型

9.4.1 线程安全的数据模型

9.4.2 分解数据模型

如果体格数据模型必须被多个线程共享,而且由于阻塞,一致性或复杂度等原因而无法实现一个线程安全的模型时,可以考虑使用分解模型设计。

9.5 其他形式的单线程子系统

小结

所有GUI框架基本上都实现为单线程的子系统,其中所有与表现相关的代码都作为任务在事件线程中运行。由于只有一个事件线程,因此运行时间较长的任务会降低GUI程序的影响性,所以应该放在后台线程中运行。在一些辅助类(例如SwingWorkrt以及在本章中构建的BackgroundTask)中提供了对取消、进度指示以及完成指示的支持,因此对于执行时间较长的任务来说,无论在任务中包含了GUI组件还是非GUI组件,在开发时可以得到简化。

《Java并发编程实战》-7

线程池的使用

8.1 在任务与执行策略之间的隐形耦合

并非所有的任务有些任务需要明确地指明执行策略,包括:

  • 依赖性任务
  • 使用线程封闭机制的任务。
  • 对应时间敏感的任务。
  • 使用ThreadLocal的任务

在一些任务中,需要拥有或排除某种特定的执行策略。如果某些任务依赖于其他的任务,那么会要求线程池足够大,从而确保它们依赖任务不会被放入等待队列中或者被拒绝,而采用线程封闭机制的任务需要串行执行。通过将这些需求写入文档,将来的代码维护人员就不会由于使用了某种不合适的执行策略而破坏安全性或活跃性。

8.1.1 线程饥饿死锁

每当提交了一个有依赖性的Executor任务时,要清楚地知道可能会出现线程“饥饿”死锁,因此需要在代码或配置Executor的配置文件中记录线程池的大小限制或配置限制。

8.1.2 运行时间较长的任务

如果任务阻塞的时间过长,那么即使不出现死锁,线程池的响应性也会变得糟糕。

定任务等待资源得时间,而不是无限制地等待可以缓解执行时间较长得任务造成的影响。

8.2 设置线程池得大小

一般情况下正确地设置线程池的大小(假设有n个CPU):

  • 计算密集型: 2n+1
  • I/O密集型:2n

8.3 配置ThreadPoolExecutor

线程的创建与销毁

管理队列任务

8.3.3 饱和策略

8.3.4 线程工厂

8.4 扩展ThreadPoolExecutor

实例:给线程池添加统计信息

8.5 递归算法的并行化

实例:谜题框架

对于并发执行的任务,EXecutor框架是一种强大且灵活的框架。它提供了大量可调节的选项,例如创建线程和关闭线程的策略,处理队列任务的策略,处理过多任务的策略,并且提供了几个钩子方法来扩展它的行为。然而,与大多数功能强大的框架一样,其中有些设置参数并不能很好地工作,某些类型的任务需要特定的执行策略,而一些参数组合则可能产生奇怪的结果。