文章目录
8.1 模式简介
- 这是一个来自工作车间的故事。在这里,工人们负责组装塑料模型。
- 客户会将很多装有塑料模型的箱子带到工作车间来,然后摆放在桌子上。
- 工人必须将客户送过来的塑料模型一个一个组装起来。他们会先取回放在桌子上的装有塑料模型的箱子,然后在阅读了箱子中的说明书后开始组装。当一箱模型组装完成后,工人们会继续去取下一个箱子。当所有模型全部组装完成后,工人们会等待新的模型被送过来。
- Worker的意思是工作的人、劳动者。在Worker Thread模式中,工人线程(worker thread)会逐个取回工作并进行处理。当所有工作全部完成后,工人线程会等待新的工作到来。
- Worker Thread 模式也被称为 Background Thread(背景线程)模式。另外,如果从 “保存多个工人线程的场所” 这一点来看,我们也可以称这种模式为 Thread Pool(线程池)模式。
8.3 Worker Thread模式中的角色
8.3.1 Client(委托者)
- Client角色创建表示工作请求的Request角色并将其传递给Channel角色。
8.3.2 Channel(通信线路)
- Channel角色接收来自于Client角色的Request角色,并将其传递给Worker角色。在示例程序中,由Channel类扮演此角色。
8.3.3 Worker(工人)
- Worker角色从Channel角色中获取Request角色,并进行工作。当一项工作完成后,它会继续去获取另外的Request角色。
8.3.4 Request(请求)
- Request角色是表示工作的角色。Request角色中保存了进行工作所必需的信息。
Worker Thread模式的类图如图8-4所示,Timethreads图如图8-5所示。
8.3.5 类图和Timethreads图
8.4 拓展思路的要点
8.4.1 提高吞吐量
- 如果可以将自己的工作交给其他人,那么自己就可以做下一项工作。线程也是一样的。如果将工作交给其他线程,自己就可以做下一项工作。这是Thread-Per-Message模式的主题。
- 由于启动新线程需要花费时间,所以Worker Thread模式的主题之一就是通过轮流地和反复地使用线程来提高吞吐量。
- Worker Thread模式是否实际地提高了吞吐量取决于线程的启动时间。
8.4.2 容量控制
- Worker Thread模式还有另外一个主题,那就是可以同时提供的服务的数量,即容量的控制。
8.4.2.1 Worker角色的数量
- Worker角色的数量越多,可以并发进行的处理也越多。但是,即使Worker角色的数量超过了同时被请求的工作的数量,也不会对提高程序处理效率有什么帮助。因为多余的Worker角色不但不会工作,还会占用内存。增加容量就会增加消耗的资源,所以必须根据程序实际运行的环境来相应地调整Worker角色的数量。
- Worker角色的数量不一定必须在程序启动时确定,也可以像下面这样动态地改变Worker角色的数量:
- 最开始只有几个Worker角色
- 当工作增加时就增加Worker角色
- 但是,如果增加得太多会导致内存耗尽,因此到达极限值后就不再增加Worker角色
- 反之,当工作减少(即等待工作的Worker角色增加)时,就要逐渐减少Worker角色
8.4.2.2 Request角色的数量
- Channel角色中保存着Request角色。只要Worker角色不断地进行工作,在Channel角色中保存的Request角色就不会增加很多。不过,当接收到的工作的数量超出了Worker角色的处理能力后,Channel角色中就会积累很多Request角色。这时,Client角色必须等待一段时间才能将Request角色发送给Channel角色。
- 如果Channel角色可以保存很多Request角色,那么就可以填补(缓冲)Client角色与Worker角色之间的处理速度差异。但是,保存Request角色会消耗大量的内存。因此,这里我们需要权衡容量与资源。
8.4.3 调用与执行的分离
-
Client角色负责发送工作请求。它会将工作内容封装为Request角色,然后传递给Channel角色。在普通的方法调用中,这部分相当于 “设置参数并调用方法” 。其中,“设置参数” 与 “创建Request角色” 相对应,而 “传递给Channel角色” 大致与 “调用方法” 相对应。
-
Worker角色负责进行工作。它使用从Channel角色接收到的Request角色来执行实际的处理。在普通的方法调用中,这部分相当于 “执行方法”。
-
在进行普通的方法调用时,“调用方法” 和 “执行方法”是连续进行的。因为调用方法后,方法会立即执行。在普通的方法调用中,调用与执行是无法分开的。
-
但是,在Worker Thread模式和Thread-Per-Message模式中,方法的调用和方法的执行是特意被分开的。方法的调用被称为 invocation(动词为invoke),方法的执行则被称为 execution(动词为execute)。因此,可以说Worker Thread 模式和Thread-Per-Message模式将方法的调用(invocation)和执行(execution)分离开来了。调用与执行的分离同时也是Command模式的主题之一。
-
调用和执行分离究竟有什么意义
- 提高响应速度:如果调用和执行不可分离,那么当执行需要花费很长时间时,就会拖调用处理的后腿。但是如果将调用和执行分离,那么即使执行需要花费很长时间也没有什么关系,因为执行完调用处理的一方可以先继续执行其他处理,这样就可以提高响应速度。
- 控制执行顺序(调度):如果调用和执行不可分离,那么在调用后就必须开始执行。但是如果将调用和执行分离,执行就可以不再受调用顺序的制约。我们可以通过设置Request角色的优先级,并控制Channel角色将Request角色传递给Worker角色的顺序来实现上述处理。这种处理称为请求调度(scheduling)
- 可以取消和反复执行:将调用和执行分离后,还可以实现 “即使调用了也可以取消执行” 这种功能。由于调用的结果是Request角色对象,所以既可以将Request角色保存,又可以反复地执行。
- 通往分布式之路:将调用和执行分离后,可以将负责调用的计算机与负责执行的计算机分离开来,然后通过网络将扮演Request角色的对象从一台计算机传递至另外一台计算机。
8.4.4 Runnable接口的意义
- java.lang.Runnable 接口有时会被用作Worker Thread模式中的Request角色。也就是说,该模式会创建一个实现了Runnable接口的类的实例对象(Runnable对象)来表示工作内容,然后将它传递给Channel角色,让其完成这项工作。
- 但是,Runnable接口的使用方法并非仅仅如此:Runnable对象可以作为方法参数传递,可以被放入到队列中,可以跨越网络传递,也可以被保存至文件中。然后,这样的Runnable对象不论被传递到哪台计算机中的哪个线程中,都可以运行。
8.4.5 多态的Request角色
- ClientThread传递给Channel的只是Request的实例。但是,WorkerThread并不知道Request类的详细信息。WorkerThread只是单纯地接收Request的实例,然后调用它的execute方法而已。
- 也就是说,即使我们编写了一个Request类的子类并将它的实例传递给了Channel,WorkerThread也可以正常地调用execute方法。用面向对象的术语来说,就是这里使用了多态性(polymorphism)。
- Request角色中包含了完成工作所必需的全部信息。因此,即使我们实现了多态的Request角色并增加了工作的种类,也无需修改Channel角色和Worker角色。这是因为即使工作种类增加了,Worker角色依然只是调用execute方法而已。
8.4.6 独自一人的Worker角色
- 当工人线程只有一个时,由于工人线程进行处理的范围变成了单线程,所以会有互斥处理可以省略的可能性。