《java并发编程实战》笔记(第8章)

来源:互联网 发布:js教程pdf 编辑:程序博客网 时间:2024/06/03 23:02
* 第八章 线程池的使用  p152*   有些类型的任务需要明确指定执行策略,包括:依赖性任务;使用线程封闭机制的任务;对相应时间敏感的任务;使用ThreadLocal的任务。 在一些任务中,需要拥有或排除某种特定的执行策略。如果某些任务依赖于其他任务,那么会要求线程池足够大,从而确保它们依赖任务不会被放入等待队列中或被拒绝,而采用线程封闭机制的任务需要串行执行。 在线程池中,如果任务依赖于其他任务,那么可能产生死锁。在单线程的Executor中,如果一个任务将另一个任务提交到同一个Executor,并且等待这个被提交任务的结果,那么通常会引发死锁。第二个任务停留在工作队列中,并且等待第一个任务完成,而第一个任务又无法完成,因为它在等待第二个任务的完成。 这种现象被称为线程饥饿死锁。 每当提交了一个有依赖性的Executor任务时,要清楚地知道可能会出现线程“饥饿”死锁,因此需要在代码或配置Executor的配置文件中记录线程池的大小限制或配置限制。*      运行时间较长的任务      执行时间较长的任务不仅会造成线程池的堵塞,甚至会增加执行时间较短任务的服务时间,从而影响整体的响应性。      通过限定任务等待资源的时间可以缓解执行时间较长任务造成的影响。在平台类库的大多数可阻塞方法中,都同时定义了限时版本和无限时版本,例如 Thread.join ,BlockingQueue.put , CountDownLatch.await ,Selector.select等。*      设置线程池的大小      线程池的理想大小取决于被提交任务的类型以及所部属系统的特性。在代码中通常不会固定线程池的大小,而应该通过某种配置机制来提供,或者根据Runtime.availableProcessors来动态计算。      要想正确地设置线程池的大小,必须分析计算环境,资源预算和任务的特性。      对于计算密集型的任务,在拥有N个处理器(cpu)的系统上,当线程池的大小为N+1时,通常能实现最优的利用率。对于包含I/O操作或者其他阻塞操作的任务,你必须估算出任务的等待时间与计算时间的比值,可以通过一些分析或监控工具来获得,你还可以通过另外一种方法来调节:在某个基准负载下,分别设置不同大小的线程池来运行应用程序,并观察CPU利用率的水平。    当任务需要某种通过资源池来管理资源时,例如数据库连接,那么线程池和资源池的大小将会相互影响。如果每个任务都需要一个数据库连接,那么连接池的大小就限制了线程池的大小。同样,当线程池中的任务是数据库连接的唯一使用者时,那么线程池的大小又将限制连接池的大小。* 配置ThreadPoolExecutor      ThreadPoolExecutor为一些Executor提供了基本的实现,这些Executor是由Executors中的        static ExecutorService newCachedThreadPool() 创建一个可根据需要创建新线程的线程池,但是在以前构造的线程可用时将重用它们。      static ExecutorService  newCachedThreadPool(ThreadFactory threadFactory) 创建一个可根据需要创建新线程的线程池,但是在以前构造的线程可用时将重用它们,并在需要时使用提供的 ThreadFactory 创建新线程。    static ExecutorService newFixedThreadPool(int nThreads) 创建一个可重用固定线程数的线程池,以共享的无界队列方式来运行这些线程。    static ExecutorService  newFixedThreadPool(int nThreads, ThreadFactory threadFactory)创建一个可重用固定线程数的线程池,以共享的无界队列方式来运行这些线程,在需要时使用提供的 ThreadFactory 创建新线程。

等方法返回的。ThreadPoolExecutor是一个灵活的稳定的线程池,允许进行各种定制。

:上面工厂方法返回ExecutorService, ExecutorService为接口,ThreadPoolExecutor为其实现类。

 如果默认的执行策略不能满足需求,可以通过ThreadPoolExecutor的构造函数来实例化一个对象,并根据自己的需求来定制,并且可以参考Executors的源代码来了解默认配置下的执行策略,然后在这些执行策略的基础上进行修改。

public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
用给定的初始参数创建新的 ThreadPoolExecutor。
参数:
corePoolSize - 池中所保存的线程数,包括空闲线程。
maximumPoolSize - 池中允许的最大线程数。
keepAliveTime - 当线程数大于核心时,此为终止前多余的空闲线程等待新任务的最长时间。
unit - keepAliveTime 参数的时间单位。
workQueue - 执行前用于保持任务的队列。此队列仅保持由 execute 方法提交的 Runnable 任务。
threadFactory - 执行程序创建新线程时使用的工厂。
handler - 由于超出线程范围和队列容量而使执行被阻塞时所使用的处理程序。
抛出:
IllegalArgumentException - 如果 corePoolSize 或 keepAliveTime 小于 0,或者 maximumPoolSize 小于等于 0,或者 corePoolSize 大于 maximumPoolSize。
NullPointerException - 如果 workQueue、threadFactory 或 handler 为 null。

*      线程的创建和销毁 线程池的基本大小,最大大小以及存活时间等因素共同负责线程的创建与销毁。 ThreadPoolExecutor允许提供一个BlockingQueue来保存等待执行的任务。基本的任务排队方法有三种:无界队列,有界队列和同步移交(synchronous Handoff)。队列的选择与其他的配置参数有关,例如线程池的大小等。  newFixedThreadPool和newSingleThreadExecutor在默认情况下将使用一个无界的LinkedBlockingQueue.  一种更稳妥的资源管理策略是使用有界队列,例如ArrayBlockingQueue,有界的LinkedBlockingQueue ,PriorityBlockingQueue.在使用有界的工作队列时,队列的大小与线程池的大小必须一起调节。如果线程池较小而队列较大,那么有助于减少内存使用量,降低CPU的使用率,同时还可以减少上下文切换,但付出的代价可能会限制吞吐量。  对于非常大的或者无界的线程池,可以通过使用synchronousQueue来避免任务排队,以及直接将任务从生产者移交给工作者线程。SynchronousQueue不是一个真正的队列,而是一种在线程直接进行移交的机制。只有当线程池是无界的或者可以拒绝任务时,SynchronousQueue才有实际价值。在newCachedThreadPool工厂方法中就使用了SynchronousQueue。 只有当任务相互独立时,为线程池或工作队列设置界限才是合理的。如果任务之间存在依赖性,那么有界的线程池或队列就可能导致线程“饥饿”死锁问题。此时,应该使用无界的线程池,例如 newCachedThreadPool .* 饱和策略 当有界队列被填满后,饱和策略开始发挥作用。ThreadPoolExecutor的饱和策略可以通过调用setRejectedExecutionHandle来修改。JDK提供了几种不同的RejectedExecutionHandle实现,每种实现都包含有不同的饱和策略:AbortPolicy,CallerRunsPolicy,DiscardPolicy和DiscardOldestPolicy。 “中止(Abort)”策略是默认的饱和策略,该策略抛出未检查的RejectedExecutionException,调用者可以捕获这个异常,然后根据需求编写自己的处理代码。 “调用者运行(Caller-Runs)”策略实现了一种调节机制,该策略不会抛弃任务,也不会抛出异常,而是将某些任务回退给调用者,从而降低新任务的流量,它不会在线程池中执行新提交的任务,而是**直接在execute方法的调用线程中运行被拒绝的任务**;如果执行程序已关闭,则会丢弃该任务。

ThreadPoolExecutor.CallerRunsPolicy()

* 线程工厂 每当线程池需要创建一个线程时,都是通过线程工厂方法来完成,默认的线程工厂方法将创建一个新的,非守护的线程,并且不包含特殊的配置信息。然而在许多情况下都需要使用定制的线程工厂方法。

public interface ThreadFactory {
Thread newThread(Runnable r);
}

原创粉丝点击