第13章-总结多线程编程的模式语言

13.1 多线程编程的模式语言

13.1.1 模式与模式语言

  1. 所谓模式(pattern),是指 “针对某个语境下反复出现的问题的解决方案”。一个模式必定有一个易于大家理解的名字。
  2. 而所谓语境,是指问题所处的状况和背景。语境也称为上下文(context)。
  3. 在一个问题中,存在着被称为约束力(force)的条件,即 “解决问题的条件、无法跨越的障碍”。许多时候,多股力量会相互作用,此消彼长。
  4. 所谓模式语言(pattern language),一言以蔽之,即模式的集合。不过,它并非是将所有模式简单地集合在一起。将相互关联、相互补充的各种模式集中在一起,然后通俗易懂地描述它们之间的关系——这才是模式语言。
  5. 如果将模式看作针对一个问题的一种解决方案,那么模式语言就是针对某个领域中的问题集的解决方案集合。不管是在编程和软件设计领域,还是在其他任何领域,只要解决方案能很好地描述出技巧、心得、要领、提示等,就是模式语言。
  6. 通过阅读模式语言可以理解该领域的问题集与解决方案集,从中选择出可以解决自己遇到的问题的模式,然后运用它们。

模式关系图

13.2 Single Threaded Execution 模式——能通过这座桥的只有一个人

  1. 语境:多个线程共享实例
  2. 问题:如果各个线程都随意地改变实例状态,实例会失去安全性。
  3. 解决方案
    • 首先,严格地规定实例的不稳定状态的范围(临界区)。
    • 接着,施加保护,确保临界区只能被一个线程执行
    • 这样就可以确保实例的安全性。
  4. 实现:Java 可以使用 synchronized 来实现临界区。
  5. 相关模式
    • 当实例的状态不会发生变化时,可以使用 Immutable 模式来提高吞吐量。
    • 在分离使用实例状态的线程和改变实例状态的线程时,可以使用Read-Write Lock模式来提高吞吐量。

模式图

13.3 Immutable 模式——想破坏也破坏不了

  1. 语境:虽然多个线程共享了实例,但是实例的状态不会发生变化
  2. 问题:如果使用Single Threaded Execution模式,吞吐量会下降。
  3. 解决方案
    • 如果实例被创建后,状态不会发生变化,建议不要使用 Single Threaded Execution 模式。
    • 为了防止不小心编写出改变实例状态的代码,请修改代码,让线程无法改变表示实例状态的字段。另外,如果代码中有改变实例状态的方法(setter),请删除它们。获取实例状态的方法(getter)则没有影响,可以存在于代码中。
  4. 使用 Immutable 模式可以提高吞吐量。但是,在整个项目周期内持续地保持类的不可变性(immutability)是非常困难的。请在项目文档中写明该类是immutable类。
  5. 实现:Java 可以使用 private 来隐藏字段。另外,还可以使用 final 来确保字段无法改变。
  6. 相关模式
    • 要在多个线程之间执行互斥处理时,可以使用Single Threaded Execution模式。
    • 当改变实例状态的线程比读取实例状态的线程少时,可以使用Read-Write Lock模式。

模式图

13.4 Guarded Suspension 模式——等我准备好哦

  1. 语境:多个线程共享实例时

  2. 问题:如果各个线程都随意地访问实例,实例会失去安全性。

  3. 解决方案

    • 如果实例的状态不正确,就让线程等待实例恢复至正确的状态。
    • 首先,用 “守护条件” 表示实例的 “正确状态”。接着,在执行可能会导致实例失去安全性的处理之前,检查是否满足守护条件
    • 如果不满足守护条件,则让线程等待,直至满足守护条件为止
    • 使用 Guarded Suspension 模式时,可以通过守护条件来控制方法的执行。但是,如果永远无法满足守护条件,那么线程会永远等待,所以可能会失去生存性
  4. 实现

    • 在 Java 中,我们可以使用 while 语句来检查守护条件,调用 wait 方法来让线程等待。
    • 接着,调用 notify/notifyAll 方法来发送守护条件发生变化的通知。而检查和改变守护条件则可以使用Single Threaded Execution模式来实现。
  5. 相关模式

    • 如果希望在不满足守护条件时,线程不等待,而是直接返回,可以使用Balking模式。
    • Guarded Suspension 模式的检查和改变守护条件的部分可以使用Single Threaded Execution模式。

模式图

13.5 Balking 模式——不需要就算了

  1. 语境:多个线程共享实例时。

  2. 问题:如果各个线程都随意地访问实例,实例会失去安全性。但是,如果要等待安全的时机,响应性又会下降

  3. 解决方案

    • 当实例状态不正确时就中断处理。
    • 首先,用 “守护条件” 表示实例的 “正确状态”。
    • 接着,在执行可能会导致实例失去安全性的处理之前,检查是否满足守护条件。只有满足守护条件时才让程序继续执行。
    • 如果不满足守护条件就中断执行,立即返回
  4. 实现

    • Java 可以使用 if 语句来检查守护条件。
    • 这里可以使用 return 语句从方法中返回或是通过 throw 语句抛出异常来进行中断。
    • 而检查和改变守护条件则可以使用 Single Threaded Execution 模式来实现。
  5. 相关模式

    • 当要让线程等待至满足守护条件时,可以使用Guarded Suspension模式。
    • Balking 模式的检查和改变守护条件的部分可以使用Single Threaded Execution模式。

模式图

13.6 Producer-Consumer 模式——我来做,你来用

  1. 语境:想从某个线程(Producer角色)向其他线程(Consumer角色)传递数据时。
  2. 问题:
    • 如果 Producer 角色和 Consumer 角色的处理速度不一致,那么处理速度快的角色会被处理速度慢的角色拖后腿,从而导致吞吐量下降。
    • 另外,如果在Producer角色写数据的同时,Consumer角色去读取数据,又会失去安全性
  3. 解决方案
    • 在 Producer 角色和 Consumer 角色之间准备一个中转站 Channel 角色
    • 接着,让 Channel 角色持有多个数据。这样,就可以缓解 Producer 角色与 Consumer 角色之间的处理速度差异。
    • 另外,如果在 Channel 角色中进行线程互斥,就不会失去数据的安全性。这样就可以既不降低吞吐量,又可以在多个线程之间安全地传递数据。
  4. 相关模式
    • Channel角色安全传递数据的部分可以使用 Guarded Suspension 模式。
    • 在Future模式中传递返回值的时候可以使用 Producer-Consumer 模式。
    • Worker Thread模式中传递请求的时候可以使用 Producer-Consumer 模式。

模式图

13.7 Read-Write Lock 模式——大家一起读没问题,但读的时候不要写哦

  1. 语境:当多个线程共享了实例,且存在读取实例状态的线程(Reader角色)和改变实例状态的线程(Writer角色)时。
  2. 问题:如果不进行线程的互斥处理将会失去安全性。但是,如果使用 Single Threaded Execution模式,吞吐量又会下降。
  3. 解决方案
    • 首先将 “控制Reader角色的锁” 与 “控制Writer角色的锁” 分开,引入一个提供这两种锁的ReadWriteLock角色。
    • ReadWriteLock 角色会进行 Writer 角色之间的互斥处理,以及 Reader 角色与 Writer 角色之间的互斥处理。
    • Reader 角色之间即使发生冲突也不会有影响,因此无需进行互斥处理。这样,就可以既不失去安全性,又提高吞吐量。
  4. 实现:Java 可以使用 finally 语句块来防止忘记释放锁。
  5. 相关模式
    • Read-Write Lock模式中的ReadWriteLock角色实现互斥处理的部分可以使用Guarded Suspension模式。
    • Writer 角色完全不存在时,可以使用 Immutable 模式。(因为Reader不改变状态)

模式图

13.8 Thread-Per-Message 模式——这项工作就交给你了

  1. 语境:当线程(Client角色)要调用实例(Host角色)的方法时。
  2. 问题:在方法的处理结束前,程序的控制权无法从 Host 角色中返回。如果方法的处理需要花费很长时间,响应性会下降。
  3. 解决方案
    • 在 Host 角色中启动一个新线程
    • 接着,将方法需要执行的实际处理交给这个新启动的线程负责。
    • 这样,Client 角色的线程就可以继续向前处理。这样修改后,可以在不改变 Client 角色的前提下提高响应性。
  4. 实现:Java 可以使用匿名内部类来轻松地启动新线程。
  5. 相关模式
    • 当想要节省线程启动所花费的时间时,可以使用Worker Thread模式。
    • 当想要将处理结果返回给Client角色时,可以使用Future模式。

模式图

13.9 Worker Thread模式——工作没来就一直等,工作来了就干活

  1. 语境:当线程(Client角色)要调用实例(Host角色)的方法时。
  2. 问题:
    • 如果方法的处理需要花费很长时间,响应性会下降。
    • 如果为了提高响应性而启动了一个新的线程并让它负责方法的处理,那么吞吐量会随线程的启动时间相应下降。
    • 另外,当要发出许多请求时,许多线程会启动,容量会因此下降。
  3. 解决方案
    • 首先,启动执行处理的线程(工人线程)。
    • 接着,将代表请求的实例传递给工人线程。这样,就无需每次都启动新线程了。
  4. 相关模式
    • 在将工人线程的处理结果返回给调用方时可以使用 Future 模式。
    • 在将代表请求的实例传递给工人线程时可以使用 Producer-Consumer 模式。

模式图

13.10 Future模式——先给您提货单

  1. 语境:当一个线程(Client角色)向其他线程委托了处理,而 Client 角色也想要获取处理结果时。
  2. 问题:如果在委托处理时等待执行结果,响应性会下降。
  3. 解决方案
    • 首先,编写一个与处理结果具有相同接口(API)的 Future 角色。
    • 接着,在处理开始时返回 Future 角色,稍后再将处理结果设置到 Future 角色中。
    • 这样,Client 角色就可以通过 Future 角色在自己觉得合适的时机获取(等待)处理结果。
  4. 相关模式
    • 在 Client 角色等待处理结果的部分可以使用 Guarded Suspension 模式。
    • 当想在 Thread-Per-Message 模式中获取处理结果时可以使用 Future 模式。
    • 当想在 Worker Thread 模式中获取处理结果时可以使用 Future 模式。

模式图

13.11 Two-Phase Termination 模式——先收拾房间再睡觉

  1. 语境:当想要终止正在运行的线程时。
  2. 问题:如果因为外部的原因紧急终止了线程,就会失去安全性
  3. 解决方案
    • 首先,让即将被终止的线程自己去判断开始终止处理的时间点。
    • 为此,我们需要准备一个方法,来表示让该线程终止的 “终止请求”。
    • 该方法执行的处理仅仅是设置 “终止请求已经到来” 这个闭锁
    • 线程会在可以安全地开始终止处理之前检查该闭锁。如果检查结果是终止请求已经到来,线程就会开始执行终止处理。
  4. 实现
    • Java 不仅仅要设置终止请求的标志,还要使用 interrupt 方法来中断 wait方法、sleep方法和 join 方法
    • 由于线程在 wait方法、sleep方法和 join 方法中抛出 InterruptedException 异常时会清除中断状态,所以在使用 isInterrupted 方法检查终止请求是否到来时需要格外注意。
    • 当想要实现即使在运行时发生异常也能进行终止处理时,可以使用 finally 语句块。
  5. 相关模式
    • 当想在执行终止处理时禁止其他处理,可以使用 Balking 模式。
    • 当要确保一定会执行终止处理时,可以使用 Before/After 模式。

模式图

13.12 Thread-Specific Storage 模式——一个线程一个储物柜

  1. 语境:当想让原本为单线程环境设计的对象(TSObject角色)运行于多线程环境时。
  2. 问题
    • 复用 TSObject 角色是非常困难的。即使是修改 TSObject 角色,让其可以运行于多线程环境,稍不注意还是会失去安全性和生存性。
    • 而且,可能根本就无法修改 TSObject 角色。
    • 另外,由于我们不想修改使用 TSObject 角色的对象(Client角色)的代码,所以我们也不想改变TSObject角色的接口(API)。
  3. 解决方案
    • 创建每个线程所特有的存储空间,让存储空间与线程一一对应并进行管理。
    • 首先,编写一个与 TSObject 角色具有相同接口(API)的 TSObjectProxy 角色。
    • 另外,为了能够管理 “Client角色→TSObject角色” 之间的对应表,我们还需要编写一个TSObjectCollection角色。
    • TSObjectProxy 角色使用 TSObjectCollection 角色来获取与当前线程对应的 TSObject 角色,并将处理委托给该 TSObject 角色
    • Client 角色不再直接使用 TSObject 角色,取而代之的是 TSObjectProxy 角色
    • 这样修改后,一个 TSObject 角色一定只会被一个线程调用,因此无需在TSObject角色中进行互斥处理。
    • 关于多线程的部分被全部隐藏了在 TSObjectCollection 角色内部。另外,也无需改变 TSObject 角色的接口(API)。
    • 不过,在使用 Thread-Specific Storage 模式后,上下文会被隐式地引入到程序中,这会导致难以彻底地理解整体代码。
  4. 实现:Java 可以使用 java.lang.ThreadLocal 类来扮演 TSObjectCollection 角色。
  5. 相关模式
    • 当想要对多个线程进行互斥处理时可以使用Single Threaded Execution模式。

模式图

13.13 Active Object 模式——接收异步消息的主动对象

  1. 语境:假设现在有处理请求的线程(Client角色)和包含了处理内容的对象(Servant角色),而且Servant角色只能运行于单线程环境。
  2. 问题
    • 虽然多个 Client 角色都想要调用 Servant 角色,但是 Servant 角色并不是线程安全的。
    • 我们希望,即使Servant角色的处理需要很长时间,它对Client角色的响应性也不会下降。
    • 处理的请求顺序和执行顺序并不一定相同。
    • 处理的结果需要返回给 Client 角色。
  3. 解决方案
    • 我们需要构建一个可以接收异步消息,而且与 Client 运行于不同线程的主动对象。
    • 首先,我们引入一个 Scheduler 角色的线程。调用 Servant 角色的只能是这个 Scheduler 角色
    • 这是一种只有一个工人线程的 Worker Thread 模式。这样修改后,就可以既不用修改Servant角色去对应多线程,又可以让其可以被多个Client处理。
    • 接下来需要将来自 Client 角色的请求实现为对 Proxy 角色的方法调用。
    • Proxy 角色将一个请求转换为一个对象,使用 Producer-Consumer 模式将其传递给 Scheduler 角色。这样修改后,即使 Servant 角色的处理需要花费很长时间,Client 角色的响应性也不会下降。
    • 选出下一个要执行的请求并执行——这是Scheduler角色的工作。这样修改后,Scheduler角色就可以决定请求的执行顺序了。
    • 最后,使用 Future 模式将执行结果返回给Client角色。
  4. 相关模式
    • 在实现 Scheduler 角色的部分可以使用 Worker Thread 模式。
    • 在将请求从 Proxy 角色传递给 Scheduler 角色的部分可以使用 Producer-Consumer 模式。
    • 在将执行结果返回给 Client 角色的部分可以使用 Future 模式。

模式图

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值