本文介绍几种java常用的线程池如:FixedThreadPool,ScheduledThreadPool,CachedThreadPool等线程池,并分析介绍Executor框架,做到“知其然”:会用线程池,正确使用线程池。并且“知其所以然”:了解背后的基本原理。
转载请指明原处:
http://blogs.xzchain.cn
Executor是java SE5的java.util.concurrent包下的执行器,用于管理Thread对象,它帮程序员简化了并发编程。与客户端直接执行任务不同,Executor作为客户端和任务执行之间的中介,将任务的提交和任务的执行策略解耦开来,Executor允许我们管理异步任务的执行。
Executor的实现思想基于生产者-消费者模型,提交任务的线程相当于生产者,执行任务的工作线程相当于消费者,这里所说的任务即我们实现Runnable接口的类。
Executor关键类图如下:
根据这个类图,详细分析一下其中的门路:
根据上面的类图,我们详细看下ThreadPoolExecutor提供的构造方法:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
//略 ……
}
参数介绍:
* @param corePoolSize the number of threads to keep in the pool, even
* if they are idle, unless {@code allowCoreThreadTimeOut} is set
* @param maximumPoolSize the maximum number of threads to allow in the
* pool
* @param keepAliveTime when the number of threads is greater than
* the core, this is the maximum time that excess idle threads
* will wait for new tasks before terminating.
* @param unit the time unit for the {@code keepAliveTime} argument
* @param workQueue the queue to use for holding tasks before they are
* executed. This queue will hold only the {@code Runnable}
* tasks submitted by the {@code execute} method.
* @param threadFactory the factory to use when the executor
* creates a new thread
* @param handler the handler to use when execution is blocked
* because the thread bounds and queue capacities are reached
参数说明:
很多人有疑问这些参数的含义到底是什么,网上的解释也五花八门,这里我通俗的解释一下,当提交一个任务的时候,会先检查当前执行的线程数,如果当前执行的线程数小于基本线程数(corePoolSize),则直接创建线程执行任务,且这个线程属于core线程。如果当前执行的线程数大于等于基本线程数,该任务会被放到阻塞队列(workQueue)中,在阻塞队列里的任务会复用core线程,即阻塞队列里的任务会等待core线程提取执行。而当阻塞队列是有界队列时,在阻塞队列满了的时候,会创建新的线程来执行任务,这些新的线程是非core线程,满足keepAliveTime的时候会被销毁,而已经进入队列里的任务会继续由已有的全部线程来执行。超过最大线程时(maximumPoolSize),会使用我们定义的“饱和策略”来处理。
这里面其实牵扯了另一个问题,即如何实现线程复用,简单来说就是线程在执行任务时,执行完后会去队列里面take新的任务,而take方法是阻塞的,因此线程并不会被销毁,只会不停的执行任务,没有任务时要么根据我们的逻辑销毁要么阻塞等待任务。
有时候使用ThreadPoolExecutor自己创建线程池时由于不清楚如何设置线程池大小,存活时间等问题会导致资源浪费,而ThreadPoolExecutor是一个灵活的、稳定的线程池,允许各种定制。Executors提供了一系列的静态工程方法帮我们创建各种线程池。
newFixedThreadPool创建可重用且线程数量固定的线程池,当线程池所有线程都在执行任务时,新的任务会在阻塞队列中等待,当某个线程因为异常而结束,线程池会创建新的线程进行补充。
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue());
}
newFixedThreadPool使用基于FIFO(先进先出)策略的LinkedBlockingQueue作为阻塞队列。
newSingleThreadPool使用单线程的Executor,其中corePoolSize和maximumPoolSize都为1,固定线程池大小为1。
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue()));
}
newScheduledThreadPool创建可以延迟执行或者定时执行任务的线程池,使用延时工作队列DelayedWorkQueue。
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue());
}
newCachedThreadPool是一个可缓存的线程池,线程池中的线程如果60s内未被使用将被移除。使用同步队列SynchronousQueue。
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue());
}
关于同步队列SynchronousQueue
回想一下,newSingleThreadPool,newFixedThreadPool默认使用无界队列LinkedBlockingQueue,当任务执行速度远远低于新任务到来的速度时,该队列将无限增加,如果我们使用有界队列例如ArrayBlockingQueue,当队列填满后则需要完善的饱和策略,这里需要根据需求选取折中之道。
这里就引出了SynchronousQueue,对于非当大的或者无界的线程池,使用SynchronousQueue可以避免排队,它可以讲任务直接从生产中交给工作者线程,SynchronousQueue本质上不是一个队列,而是一种同步移交机制,要将一个任务放入SynchronousQueue时必须要有一个线程在等待接受,如果没有线程在等待且当前线程数大于最大值,将启用饱和策略拒绝任务。
总结:newSingleThreadPool使用单线程的Executor,newFixedThreadPool设置线程池的基本大小和最大大小为指定的值,而且创建的线程池不会超时。newCachedThreadPool将线程池的最大大小设置为Integer.MAX_VALUE(即2的31次方减1),基本大小为0,超时时间为1分钟,该线程池可以无限拓展,且需求降低时会自动收缩。newScheduledThreadPool用于执行定时任务的线程池。其他形式的线程池大家可参考其他资料。
线程池的大小取决于提交的任务的类型和系统的性能。
为了正确设置线程池大小需要考虑系统的CPU数,内存容量,任务类型(是计算密集型还是io密集型或是二者皆可),任务是否需要稀缺资源(如jdbc连接)
对于计算密集型任务,在拥有N个处理器的系统上,最佳线程池的大小为N+1。这个额外的线程保证了突发情况下CPU时钟周期不会浪费。对于包含IO操作或者其他阻塞操作的任务,由于线程不会一直执行,线程池需要更大。
线程池的大小设置最优公式如下:
参数定义:
N = number of CPUs (cpu数量)
U = target CPU utilization,0<= U <= 1 (cpu利用率)
W/C = ratio of wait time to compute time (任务等待时间和计算时间的比值)
那么最优线程池大小计算公式为:
S = N*U*(1+W/C)
其中cpu数可以通过Runtime.getRuntime().availableProcessors()获得
CPU周期只是影响线程池大小的一个主要参数,其他的因素也很重要,需要综合实践。
参考《thinking in java》、《java并发编程实战》