第8章,线程池的使用

8.1.2 运行时间较长的任务#

有一项技术可以缓解执行时间较长任务造成的影响,即限定任务等待资源的时间,而不要无限制地等待。在平台类库的大多数可阻塞方法中,都同时定义了限时版本和无限时版本,例如Thread.join、BlockingQueue.put、CountDownLatch.await以及Selector.select等。如果等待超时,那么可以把任务标识为失败,然后中止任务或者将任务重新放回队列以便随后执行。这样,无论任务的最终结果是否成功,这种办法都能确保任务总能继续执行下去,并将线程释放出来以执行一些能更快完成的任务。如果在线程池中总是充满了被阻塞的任务,那么也可能表明线程池的规模过小。

~真是一个好办法,等待若干时间而不是无限期等待下去,就可以解决活跃性甚至死锁的问题。

8.2 设置线程池的大小#

对于计算密集型的任务,在拥有Ncpu个处理器的系统上,当线程池的大小为Ncpu+1时,通常能实现最优的利用率。(即使当计算密集型的线程偶尔由于页缺失故障或者其他原因而暂停时,这个“额外”的线程也能确保CPU的时钟周期不会被浪费。)对于包含I/O操作或者其他阻塞操作的任务,由于线程并不会一直执行,因此线程池的规模应该更大。要正确地设置线程池的大小,你必须估算出任务的等待时间与计算时间的比值。线程池的最优大小=CPU数 X CPU利用率 X(1+ W/C),其中W/C为任务等待时间与计算时间的比值。

~线程池的最优大小,也不知道这样设置合不合理,但是至少提供了一个选择。 这样方式来设置线程池的大小,主要难处还是在估算出任务的等待时间与计算时间的比值。

8.3 配置ThreadPoolExecutor

public ThreadPoolExecutor(
	int corePoolSize,
	int maximumPoolSize,
	long keepAliveTime,TimeUnit unit,
	BlockingQueue<Runnable> workQueue,
	ThreadFactory  threadFactory,
	RejectedExecutionHandler  handler){……}

线程池的基本大小(CorePoolSize)、最大大小(MaximumPoolSize)以及存活时间等因素共同负责线程的创建与销毁。基本大小也就是线程池的目标大小,即在没有任务执行时线程池的大小,并且只有在工作队列满了的情况下才会创建超出这个数量的线程。线程池的最大大小表示可同时活动的线程数量的上限。如果某个线程的空闲时间超过了存活时间,那么将被标记为可回收的,并且当线程池的当前大小超过了基本大小时,这个线程将被终止。
在Java6中,可以通过allowCoreThreadTimeOut来使线程池中的所有线程超时。对于一个大小有限的线程池并且在该线程池中包含一个工作队列,如果希望这个线程池在没有任务的情况下能销毁所有线程,那么可以启用这个特性并将基本大小设置为零。

~可以多看看,了解可以为线程池设置哪些属性。

8.3.2 管理队列任务#

在使用有界的工作队列时,队列的大小与线程池的大小必须一起调节。如果线程池较小而队列较大,那么有助于减少内存使用量,降低CPU的使用率,同时还可以减少上下文切换,但付出的代价是可能会限制吞吐量。

~想想还真是这样,线程池设置的大一些,那么任务队列可以设置小一些,当然在线程池设置的比较小,那么任务队列可以大一些,因为线程少了,处理同等数量的任务花费的时间多了一些,另外在设置线程池和队列大小的时候,还得考虑CPU利用率,内存占用率等。

对于非常大的或者无界的线程池,可以通过使用SynchronousQueue来避免任务排队,以及直接将任务从生产者移交给工作者线程。SynchronousQueue不是一个真正的队列,而是一种在线程之间进行移交的机制。要将一个元素放入SynchronousQueue中,必须有另一个线程正在等待接受这个元素。如果没有线程正在等待,并且线程池的当前大小小于最大值,那么ThreadPoolExecutor将创建一个新的线程,否则根据饱和策略,这个任务将被拒绝。使用直接移交将更高效,因为任务会直接移交给执行它的线程,而不是被首先放在队列中,然后由工作者线程从队列中提取该任务。只有当线程池是无界的或者可以拒绝任务时,SynchronousQueue才有实际价值。在newCachedThreadPool工厂方法中就使用了SynchronousQueue。

~终于看到了SynchronousQueue的使用价值,但是线程池搭配SynchronousQueue使用时,线程池需要为无界或者可以拒绝任务。

8.3.2.1 任务依赖#

只有当任务相互独立时,为线程池或工作队列设置界限才是合理的。如果任务之间存在依赖性,那么有界的线程池或队列就可能导致线程“饥饿”死锁问题。此时应该使用无界的线程池,例如newCachedThreadPool。
对于提交其他任务并等待其结果的任务来说,还有另一种配置方法,就是使用有界的线程池,并使用SynchronousQueue作为工作队列,以及“调用者运行(Caller-Runs)”饱和策略。

~看来解决任务之间依赖性问题的办法还是有,只是自己不知道而已。

8.3.3 饱和策略

当有界队列被填满后,饱和策略开始发挥作用。ThreadPoolExecutor的饱和策略可以通过调用setRejectedExecutionHandler来修改。(如果某个任务被提交到一个已被关闭的Executor时,也会用到饱和策略。)JDK提供了几种不同的RejectedExecutionHandler实现,每种实现都包含有不同的饱和策略:AbortPolicy、CallerRunsPolicy、DiscardPolicy和DiscardOldestPolicy。
“中止(Abort)”是默认的饱和策略,该策略将抛出未检查的RejectedExecutionException。

~有必要了解一下饱和策略。

8.3.4 线程工厂

在许多情况下都需要使用定制的线程工厂方法。例如,你希望为线程池中的线程指定一个UncaughtExceptionHandler,或者实例化一个定制的Thread类用于执行调试信息的记录。你还可能希望修改线程的优先级(这通常并不是一个好主意。)或者守护状态(同样,这也不是一个好主意。)。或许你只是希望给线程取一个更有意义的名称,用来解释线程的转储信息和错误日志。

~线程工厂就是线程池中创建线程的方法,可以通过这个方法做一些事情,比如设置未捕获异常处理类,为线程增加调试信息等等,有没有这样的使用场景是一回事,知不知道是另外一回事。

8.3.5 在调用构造函数后再定制ThreadPoolExecutor#

如果将ExecutorService暴露给不信任的代码,又不希望对其进行修改,就可以通过unconfigurableExecutorService来包装它。

~也就说不希望用户向下转型为线程池,然后再为线程池设置或修改一些属性,就是为了防止这种情况。 ~要知道这种使用方法,有这样的场景时就能够想到就行。

8.4 扩展ThreadPoolExecutor#

ThreadPoolExecutor是可扩展的,它提供了几个可以在子类化中改写的方法:beforeExecute、afterExecute和terminated,这些方法可以用于扩展ThreadPoolExecutor的行为。
在执行任务的线程中将调用beforeExecute和afterExecute等方法,在这些方法中还可以添加日志、计时、监视或统计信息收集的功能。无论任务是从run中正常返回,还是抛出一个异常而返回,afterExecute都会被调用。(如果任务在完成后带有一个Error,那么就不会调用afterExecute。)如果beforeExecute抛出一个RuntimeException,那么任务将不被执行,并且afterExecute也不会被调用。
在线程池完成关闭操作时调用terminated,也就是在所有任务都已经完成并且所有工作者线程也已经关闭后。terminated可以用来释放Executor在其生命周期里分配的各种资源,此外还可以执行发送通知、记录日志或者收集finalize统计信息等操作。

~有必要知道。