读书笔记 - Java 多线程编程实战指南
第四章 Guarded Suspension(保护性暂挂)模式
模式简介
- 多线程编程中,为了提高并发率,往往将一个任务分解为不同的部分,将其交由不同的线程来执行。这些线程间相互协作时,仍然可能会出现一个线程去等待另外一个线程完成一定的操作,其自身才能继续运行的情形。
- Guarded Suspension 模式可以帮组完美解决上述的等待问题。该模式的核心思想是如果某个线程执行特定的操作前需要满足一定的条件,则在该条件未满足时将该线程暂停运行(即暂挂线程,使其处于等待(WAITING)状态,直到该条件满足时才继续该线程的运行)。
- wait/notify 可以用来实现 Guarded Suspension 模式。但是,Guarded Suspension 模式还要解决 wait/notify 所解决的问题之外的问题。
-
模式架构
Guarded Suspension 模式的核心时一个受保护方法(Guarded Method)。该方法执行其所要真正执行的操作时需要满足特定的条件(Predicate,以下简称为保护条件)。当该条件不满足时,执行受保护方法的线程会被挂起进入等待(WAITING)状态,直到该条件满足时该线程才会继续运行。此时,受保护方法才会真正执行其所要执行的操作。
-
模式案例
某系统有个告警功能模块。该模块的主要功能是将其接收到的警告信息发送给告警服务器。该模块中的类 AlarmAgent 负责与告警服务器进行对接。AlarmAgent 的 sendAlarm 方法负责通过网络连接(Socket 连接)将告警信息发送到告警服务器。AlarmAgent 创建了一个专门的线程用于其与告警服务器建立网络连接。因此,sendAlarm 方法被调用的时候,连接线程可能还没有完成网络连接的建立。此时,sendAlarm 方法应该等待连接线程建立好网络连接。另外,即便连接线程建立好了网络连接,中途也可能由于某些原因出现与告警服务器断连的情况。此时,sendAlarm 方法需要等待心跳(Heartbeat)任务重新建立好才能上报告警信息。
本案例 Demo 详见 multithreading 工程的 com.GuardedSuspension 包
-
Guarded Suspension 模式的评价和现实考量
- Guarded Suspension 模式使应用程序避免了样板式代码。Guarded Suspension 模式的 Blocker 参与者所实现的线程挂起与唤醒功能固然可以由应用代码直接使用 wait/notify 活着 java.util.concurrent.locks.Condition 来实现,但是这里设计几个比较容易犯错的重要技术细节。
- 关注点分离(Separation of Concern)。Guarded Suspension 模式中的各个参与者各自仅关注本模式所要解决的问题中的一个方面,各个参与者的职责时高内聚(Cohesive)的。应用开发人员只需要根据应用的需要实现 GuardedObject、ConcretePredicate 和 ConcreteGuardedAction 这几个必须由应用是喜爱呢的参与者即可,而其他的参与者的实现都是可以复用的。
- 可能增加 JVM 垃圾回收的负担。每次保护方法被调用的时候都会有个新的 GuardedAction 实例被创建。
- 可能增加上下文切换(Context Switch)。如果频繁出现保护方法被调用时保护条件不成立,那么保护方法的执行线程就会频繁地被暂挂和唤醒,而导致频繁地上下文切换。
- Blocker 实现类中封装了几个易错的重要技术细节:
内存可见性和锁泄漏(Lock Leak)
- 保护条件中涉及的变量牵涉读线程和写线程进行共享访问
- 使用锁时需要注意临界区的代码无论是否正常执行,进入临界区前获得的锁实例都应该被释放
线程过早被唤醒
await 方法时放在一个 while 循环中,而不是 if 语句中。Condition 实例的 await 方法使得当前线程暂挂后,其他线程调用了 Condition 实例的 signal 或者 signalAll 方法固然可以唤醒被暂挂的线程,但是这并不能保证此时保护条件就是成立的。因此这个时候 callWithGuard 方法应该继续检测保护条件,直到保护条件成立
嵌套监视器锁死
conditionVarBlocker 实例的 callWithGuard 方法和 signalAfter/signal/broadcastAfter/broadcast 方法本身涉及了由 java.util.concurrent.locks.Lock 实例实现的同步块。如果这些方法又在另外一个同步块代码中被调用,那么这就形成了嵌套同步。而嵌套同步可能产生锁死的问题。
如果我们能够避免不必要的嵌套同步,那么嵌套监视器锁死的问题也就可以避免。ConditionVarBlocker 的构造器支持传入一个 java.util.concurrent.locks.Lock 实例正是出于避免不必要的嵌套同步的考虑。
Java 标准库实例
- JDK 1.5 开始提供的阻塞队列类 java.util.concurrent.LinkedBlockingQueue 就用了 GuardedSuspension 模式。该类的 take 方法用于从队列中取出一个元素。如果 take 方法被调用时,队列时空的,则当前线程就会被阻塞;直到队列不为空时,该方法才返回一个出队列的元素。
- 只不过 LinkedBlockingQueue 在实现 Guarded Suspension 模式时,直接使用了 java.concurrent.locks.Condition。