【图解Java多线程设计模式】总结

Single Threaded Execution模式

FPp6Hg.png

语境

多个线程共享实例时。

问题

如果各个线程都随意地改变实例状态,实例会失去安全性。

解决方案

首先,严格地规定实例的不稳定状态的范围(临界区)。接着,施加保护,确保临界区只能被一个线程执行。这样就可以确保实例的安全性。

实现

Java可以使用synchronized来实现临界区。

Immutable模式

FPpfCn.md.png

语境

虽然多个线程共享了实例,但是实例的状态不会发生变化。

问题

如果使用Single Threaded Execution模式,吞吐量会下降。

解决方案

如果实例被创建后,状态不会发生变化,建议不要使用Single Threaded Execution模式。
为了防止不小心编写出改变实例状态的代码,修改代码,让线程无法改变表示实例状态的字段。另外,如果代码中有改变实例状态的方法(setter),删除它们。获取实例状态的方法(getter)则没有影响,可以存在于代码中。
使用Immutable模式可以提高吞吐量。但是,在整个项目周期内持续地保持类的不可变性(immutability)是非常困难的。

实现

Java可以使用private来隐藏字段。另外,还可以使用final来确保字段无法改变。

Guarded Suspension模式

FPpj81.png

语境

多个线程共享实例时。

问题

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

解决方案

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

实现

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

Balking模式

FPpxv6.png

语境

多个线程共享实例时。

问题

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

解决方案

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

实现

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

Producer-Consumer模式

FyNsit.png

语境

想从某个线程(Producer角色)向其他线程(Consumer角色)传递数据时。

问题

如果Producer角色和Consumer角色的处理速度不一致,那么处理速度快的角色会被处理速度慢的角色拖后腿,从而导致吞吐量的下降。 另外,如果在Producer角色写数据的同时,Consumer角色去读取数据,又会失去安全性。

解决方案

在Producer角色和Consumer角色之间准备一个中转站——Channel角色。接着,让Channel角色持有多个数据。这样,就可以缓解Producer角色与Consumer角色之间的处理速度差异。另外,如果在Channel角色中进行线程互斥,就不会失去数据的安全性。这样就可以既不降低吞吐量,又可以在多个线程之间安全地传递数据。

Read-Write Lock模式

FW0WxH.png

语境

当多个线程共享了实例,且存在读取实例状态的线程(Reader角色)和改变实例状态的线程(Writer角色)时。

问题

如果不进行线程的互斥处理将会失去安全性。但是,如果使用Single Threaded Execution模式,吞吐量又会下降。

解决方案

首先将“控制Reader角色的锁”与“控制Writer角色的锁”分开,引入一个提供这两种锁的ReadWriteLock角色。ReadWriteLock角色会进行Writer角色之间的互斥处理,以及Reader角色与Writer角色之间的互斥处理。Reader角色之间即使发生冲突也不会有影响,因此无需进行互斥处理。这样,就可以既不失去安全性,又提高吞吐量。

实现

Java可以使用finally语句块来防止忘记释放锁。

Thread-Per-Message模式

FbP7hq.png

语境

当线程(Client角色)要调用实例(Host角色)的方法时。

问题

在方法的处理结束前,程序的控制权无法从Host角色中返回。如果方法的处理需要花费很长时间,响应性会下降。

解决方案

在Host角色中启动一个新线程。接着,将方法需要执行的实际处理交给这个新启动的线程负责。这样,Client角色的线程就可以继续向前处理。这样修改后,可以在不改变Client角色的前提下提高响应性。

实现

Java可以使用匿名内部类来轻松地启动新线程。

Worker Thread模式

kulCFS.png

语境

当线程(Client角色)要调用实例(Host角色)的方法时。

问题

如果方法的处理需要花费很长时间,响应性会下降。如果为了提高响应性而启动了一个新的线程并让它负责方法的处理,那么吞吐量会随线程的启动时间相应下降。另外,当要发出许多请求时,许多线程会启动,容量会因此下降。

解决方案

首先,启动执行处理的线程(工人线程)。接着,将代表请求的实例传递给工人线程。这样,就无需每次都启动新线程了。

Future模式

k1iDQP.png

语境

当一个线程(Client角色)向其他线程委托了处理,而Client角色也想要获取处理结果时。

问题

如果在委托处理时等待执行结果,响应性会下降。

解决方案

首先,编写一个与处理结果具有相同接口(API)的Future角色。接着,在处理开始时返回Future角色,稍后再将处理结果设置到Future角色中。这样,Client角色就可以通过Future角色在自己觉得合适的时机获取(等待)处理结果。

Two-Phase Termination模式

k82GnO.png

语境

当想要终止正在运行的线程时。

问题

如果因为外部的原因紧急终止了线程,就会失去安全性。

解决方案

首先,让即将被终止的线程自己去判断开始终止处理的时间点。为此,需要准备一个方法,来表示让该线程终止的“终止请求”。该方法执行的处理仅仅是设置“终止请求已经到来”这个闭锁。线程会在可以安全地开始终止处理之前检查该闭锁。如果检查结果是终止请求已经到来,线程就会开始执行终止处理。

实现

Java不仅仅要设置终止请求的标志,还要使用interrupt方法来中断wait方法、sleep方法和join方法。由于线程在wait方法、sleep方法和join方法中抛出InterruptedException异常时会清除中断状态,所以在使用isInterrupted方法检查终止请求是否到来时需要格外注意。
当想要实现即使在运行时发生异常也能进行终止处理时,可以使用finally语句块。

Thread-Specific Storage模式

kBSzYq.png

语境

当想让原本为单线程环境设计的对象(TSObject角色)运行于多线程环境时。

问题

复用TSObject角色是非常困难的。即使是修改TSObject角色,让其可以运行于多线程环境,稍不注意还是会失去安全性和生存性。而且,可能根本就无法修改TSObject角色。另外,由于不想修改使用TSObject角色的对象(Client角色)的代码,所以也不想改变TSObject角色的接口。

解决方案

创建每个线程所特有的存储空间,让存储空间与线程一一对应并进行管理。
首先,编写一个与TSObject角色具有相同接口的TSObjectProxy角色。另外,为了能够管理“Client角色->TSObject角色”之间的对应表,还需要编写一个TSObjectCollection角色。
TSObjectProxy角色使用TSObjectCollection角色来获取与当前线程对应的TSObject角色,并将处理委托给该TSObject角色。Client角色不再直接使用TSObject角色,取而代之的是TSObjectProxy角色。
这样修改后,一个TSObject角色一定只会被一个线程调用,因此无需在TSObject角色中进行互斥处理。关于多线程的部分被全部隐藏在了TSObjectCollection角色内部。另外,也无需改变TSObject角色的接口。
不过,在使用Thread-Specific Storage模式后,上下文会被隐式地引入到程序中,这会导致难以彻底地理解整体代码。

实现

Java可以使用java.lang.ThreadLocal类来扮演TSObjectCollection角色。

Active Object模式

ksR5tJ.png

语境

假设现在有处理请求的线程(Client角色)和包含了处理内容的对象(Servant角色),而且Servant角色只能运行于单线程环境。

问题

虽然多个Client角色都想要调用Servant角色,但是Servant角色并不是线程安全的。希望,即使Servant角色的处理需要很长时间,它对Client角色的响应性也不会下降。
处理的请求顺序和执行顺序并不一定相同。
处理的结果需要返回给Client角色。

解决方案

需要构建一个可以接收异步消息,而且与Client运行于不同线程的主动对象。
首先,引入一个Scheduler角色的线程。调用Servant角色的只能是这个Scheduler角色。这是一种只有一个工人线程的Worker Thread模式。这样修改后,就可以既不用修改Servant角色去对应多线程,又可以让其可以被多个Client处理。
接下来需要将来自Client角色的请求实现为对Proxy角色的方法调用。Proxy角色将一个请求转换为一个对象,使用Producer-Consumer模式将其传递给Scheduler角色。这样修改后,即使Servant角色的处理需要花费很长时间,Client角色的响应性也不会下降。
选出下一个要执行的请求并执行——这是Scheduler角色的工作。这样修改后,Scheduler角色就可以决定请求的执行顺序了。
最后,使用Future模式将执行结果返回给Client角色。