第8章 线程池的使用
8.1 在任务与执行策略之间的隐性耦合
虽然Executor框架为制定和修改执行策略都提供了相当大的灵活性,但并非所有的任务都适用所有的执行策略。有些类型的任务需要明确地指明执行策略:
- 依赖性任务
- 使用线程封闭机制的任务
- 对响应时间敏感的任务
- 使用ThreadLocal的任务
只有当任务是同类型并且互相独立时,线程池才能达到最佳性能。
-8.1.1 线程饥饿死锁
在线程池中,如果任务依赖与其他任务,那么可能会产生死锁。给出个具体的例子:
同样的,当提交了有依赖性的Excecutor任务时,如果线程池不够大也会发生死锁。所以要清楚地知道可能会出现的线程“饥饿”死锁,因此需要在代码或配置文件中记录或设置线程池的大小
-8.1.2 运行时间较长的任务
限定任务等待资源时间,而不是无限制地等待,可以缓解执行时间较长任务的影响。
8.2 设置线程池的大小
8.3 配置ThreadPoolExecutor
ThreadPoolExecutor为一些Executor提供了基本的实现,是一个灵活的,稳定的线程池,允许进行各种定制。
-8.3.1 线程的创建与销毁
线程池的基本大小(Core Pool Size), 最大大小(Maximum Pool Size)以及存活时间等因素共同负责线程的创建与销毁。基本大小是没有任务执行时线程池的大小,并且只有在工作队列满了的情况下才会创建超出这个数量的线程。最大大小表示可同时活动的线程数量上限。如果某个线程的空闲时间超过了存活时间,那么将被标记为可回收的,并且当线程池的当前大小超过了基本大小时,这个线程将被终止。
newFixedThreadPool工厂方法将线程池的基本大小和最大大小设置为参数中指定的值,而且创建的线程池不会超时。newCachedThreadPool工厂方法将线程池的最大大小设置为Integer.MAX_VALUE,而将基本大小设置为0,并将超时设置为1分钟,这种方法创建出来的线程池可以被无限扩展,并且当需求降低时会自动收缩。
-8.3.2 管理任务队列
在有限的线程池中会限制可并发执行的任务数量。ThreadPoolExecutor允许提供一个BlockingQueue来保存等待执行的任务。基本的任务安排方法有3种:*队列,有界队列和同步移交。
newFixedThreadPool和newSingleThreadExecutor在默认情况下将使用一个*的LinkedBlockingDeque。如果所有工作者都处于忙碌状态,那么任务将在队列中等候。如果任务持续快速地到达,并且超过了线程池处理它们的速度,那么队列将无限增加。一种更稳妥的做法是使用有界队列,如ArrayBlockingQueue,有界的LinkedBlockingDeque,PriorityBlockingQueue。有界队列有助于避免资源耗尽的情况,但是当队列填满后,新的任务该怎么办呢?在使用有界的工作队列时,队列的大小与线程池的大小必须一起调节。对于非常大的或者*的线程池,可以通过使用SynchronousQueue来避免任务排队,以直接将任务从生产者移交给工作这线程。SynchronousQueue不是一个真正的队列,而是一种在线程之间进行移交的机制。要将一个元素放入SynchronousQueue中,必须有另一个线程正在等待接受这个元素。如果没有线程正在等待,并且线程池的当前大小小于最大值,那么ThreadPoolExecutor将创建一个新的线程,否则根据饱和策略,这个任务将被拒绝。
-8.3.3 饱和策略
当有界队列被填满后,饱和策略开始发挥作用。ThreadPoolExecutor的饱和策略可以通过调用setRejectedExecutionHandler来修改,JDK提供了几种不同的RejectedExecutionHandler实现,每种实现都包含有不同的饱和策略。“中止(Abort)”是默认的策略,该策略将抛出未检查的DiscardExecutionException。调用者可以捕获这个异常,然后根据需求编写自己的处理代码。当新提交的任务无法保存到队列中等待执行时,“抛弃(Discard)”策略会悄悄抛弃该任务。“调用者运行(Caller-Runs)”策略实现了一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者。
当工作队列被填满后,没有预定义的饱和策略来阻塞execute,通过使用信号量(Semaphore)来限制任务的到达率,就可以实现这个功能:
写到这里,才发现有件事可能自己弄反了,关于提交工作任务是哪一步的问题?从上面的理解来看,提交新工作的那一步相对应的代码是 exec .execute(new Runnable()), 所以工作队列被填满后需要阻塞execute
-8.3.4 线程工厂
每当线程需要创建一个线程时,都是通过线程工厂方法来完成的,默认创建一个新的,非守护的线程,并不包含特殊的配置信息。在许多情况下需要定制线程的工厂方法。
在MyAppThread中还可以定制其他行为,如:
如果在应用程序中需要利用安全策略来控制对某些代码库的访问权限,那么可以通过Executor中的privilegedThreadFactory工厂来定制自己的线程工厂。通过这种方式创建出来的线程,将与创建privilegedThreadFactory的线程拥有相同的访问权限,AccesssControlContext和contextClassLoader。
-8.3.5 在调用构造函数后再定制ThreadPoolExecutor
可以通过设置函数Setter来修改大多数传递给它的构造函数的参数:
8.4 扩展ThreadPoolExecutor
提供了可以在子类中改写的方法:beforeExecute,afterExecute和terminated,来个例子:
8.5 递归算法的并行化
如果循环中的迭代操作都是独立的,并且不需要等待所有的迭代操作都完成再继续执行,那么就可以使用Executor将串行循环转化为并行循环。在一些递归设计中同样可以采用循环并行的方法。下面实际来讲解一个例子:谜题框架,这些谜题都需要找出一系列的操作从初始状态转换到目标状态。
我们将谜题定义为:包含一个初始位置,一个目标位置,以及用于判断是否有效移动的规则集。规则集包含两部分:计算从指定位置开始的所有合法移动,以及每次移动的结果位置。
可以以并行方式来计算下一步移动以及目标条件,因为计算某次移动的过程很大程度上与计算其他移动的过程是相互独立的。
程序清单8-17的ValueLatch中使用CountDownLatch来实现所需的闭锁行为,并且使用锁定机制来确保解答只会被设置一次:
没个任务首先会查询solution闭锁,找到一个解答就停止。而在此之前,主线程需要等待。第一个找到解答的线程还会关闭Executor,从而阻止接受新的任务。
如果不存在解答,那么ConcurrentPuzzleSolver就不能很好地处理这种情况:如果已经遍历了所有的移动和位置都没找到解答,那么在getSolution调用中将会永远等待下去。要结束并发程序,其中一种方法是:记录活动任务的数量,当该值为零时将解答设为null,如:
上面的程序让我想到了设计模式中的装饰器模式,这样第8章就看完喽!第九章是关于图形界面的,java里有关图形的我是一律pass掉。终于进入这本书的第三部分了,希望可以早日看完。