目录
Java类库中提供的线程池创建方式常见有四种,通过调用Executors类中的静态方法可以实现不同策略的线程池技术,其中newFixedThreadPool,newCachedThreadPool,newSingleThreadExecutor是通过访问ThreadPoolExecutor构造函数实现的,而newScheduleThreadPool是直接继承ThreadPoolExecutor类,通过newSchduleThreadPool的构造函数访问ThreadPoolExecutor构函数来实现。
ThreadPoolExecutor是比较灵活,稳定的线程池,如果Executors中执行策略不能满足需求,可以通过ThreadPoolExecutor对象来时实例化一个对象,指定参数来定制线程池。ThreadPoolExecutor中定义了多个构造函数,其中比较常见的形式如下:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
}
对于ThreadPoolExecute构造函数中的几个参数分以下部分进行介绍。
线程的创建与销毁
线程池的基本大小(corePoolSize),最大数量(maximumPoolSize)以及存活时间(keepAliveTime)等几个参数共同负责线程池的创建与销毁。
参数类型 | 参数名 | 描述 |
---|---|---|
int | corePoolSize | 线程池的基本大小 |
int | maximumPoolSize | 线程池的最大数量 |
long | keepAliveTime | 超过线程基本大小的线程在空闲时存活时间 |
TimeUnit | unit | keepAliveTime参数的时间单位 |
corePoolSize是线程池基本大小,创建线程池初期,线程并不会立即启动,而当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使有其他空闲的线程也会创建线程,直到线程数量达到线程池基本大小。即使在没有任何任务可执行时,线程池数量是corpPoolSize。如果调用了线程池中的prestartAllCoreThreads()方法,线程池会提前创建并启动所有的基本线程。
maximumPoolSize是线程池的最大数量,线程池中的工作队列被任务填满后会创建超过corePoolSize数量的线程,而阈值不会超过maximumPoolSize,也就是线程池最大数量表示的是可同时活动的线程数量的上限。
如果某些线程空闲时间超过了keepAliveTime存活时间,表示线程没有任务可执行,那么这些线程将会被标记为可回收的,并且当线程池的当前大小超过了基本大小corePoolSize时,多余的空闲线程将会被终止。
TimeUnit是线程保持活动的时间单位,TimeUnit是枚举类型,枚举值有:DAYS(天),HOURS(小时),MINUTES(分钟),SECONDS(秒),MILLISECONDS(毫秒,千分之一秒),MICROSECONDS(微秒,千分之一毫秒),NANOSECONDS(纳秒,千分之一微秒)。
对于java类库中的线程池,翻看方法源码可以看到相应的参数,如下:
创建方法 | corePoolSize | maximumPoolSize | keepAliveTime | unit |
---|---|---|---|---|
Executors.newFixedThreadPool (int nThreads) | nThreads | nThreads | 0 | TimeUnit.MILLISECONDS |
Executors.newSingleThreadExecutor | 1 | 1 | 0 | TimeUnit.MILLISECONDS |
Executors.newCachedThreadPool() | 0 | Integer.MAX_VALUE | 60 | TimeUnit.SECONDS |
Executors.newScheduledThreadPool (int nThreads) | nThreads | Integer.MAX_VALUE | 0 | NANOSECONDS |
Executors.newFixedThreadPool工厂方法将线程池中基本大小和最大大小都设置为参数中指定的值,线程空闲存活时间是0,线程不会长期空闲而终止。Executors.newSingleThreadExecutor是指定参数为1的是FixedThreadPool。Executors.newCachedThreadPool工厂方法将基本大小设置为零,最大大小位置为Integer.MAX_VALUE,这种方法创建的线程可以无限扩展,并且当需求降低时会自动收缩,线程存活时间keepAliveTime设置为60秒,表示的是空闲线程超过60秒将会被终止。
管理队列任务
与管理对列任务相关的参数如下:
参数类型 | 参数名 | 描述 |
---|---|---|
BlockingQueue<Runnable> | workQueue | 任务队列 |
有限的线程池会限制并发执行的任务数量,无限制地创建线程将导致系统不稳定性。通过采用固定线程池大小(非以为每个任务创建一个线程)来解决这个问题,此方案还是有问题。在高负载的情况下,应用程序仍可能会耗尽资源,只是出现问题的概率叫较小。如果请求的到达速率超过了线程池的处理速率,那么新到来的请求将会累计起来。在线程池中,这些请求会在Executor管理的Runnable队列中等待,而不会像线程那样去竞争CPU资源。将等待中的任务放到队列中比使用线程来表示的开销低很多,但如果客户提交给服务器的请求速率超过了服务器的处理速率,那么仍可能会耗尽资源。
即使请求的平均到达速率很稳定,也仍然会出现请求突增的情况,尽管队列有助于缓解任务的突增问题,但是如果任务持续的高速地到来,那么最终还是会抑制请求的到达率以避免耗尽内存,甚至在耗尽内存之前,响应性能也随着任务队列的增长而变的越来越糟糕。
ThreadPoolExecutor允许提供一个BlockingQueue来保存等待执行的任务,基本的任务排队方法有3中:无界队列、有界队列和同步移交。
线程池名称 | 队列名称 | 队列类型 |
---|---|---|
FixedThreadPool | LinkedBlockingQueue<Runnable> | 无界队列(链表结构的阻塞队列) |
SingleThreadExecutor | LinkedBlockingQueue<Runnable> | 无界队列(链表结构的阻塞队列) |
CachedThreadPool | SynchronousQueue<Runnable> | 同步移交(无存储元素的阻塞队列) |
ScheduledThreadPoolExecutor | DelayedWorkQueue | 无界优先级队列 |
其他 | ||
---- | ArrayBlockingQueue | 有界阻塞队列 |
---- | PriorityBlockingQueue | 无界优先级队列 |
1)无界队列:
newFixedThreadPool和newSingleThreadExecutor在默认情况下使用一个无界的LinkedBlockingQueue。其容量的默认值是Integer.MAX_VALUE,如果所有的工作者线程处于忙碌状态,那么任务将在队列中等候。如果任务持续快速到达,并且超过了线程池处理它们的速度,那么队列将无限制地增加。
2)有界队列:
有界队列是一种比较合理的资管管理策略,如ArrayBlockingQueue,有界的LinkedBlockingQueue,PriorityBlockingQueue。有界队列有助于避免资源耗尽的情况发生。但当队列被填满之后,新的任务该如何处理?通常可以采用直接丢弃任务或者抛出异常等方法来解决,这种在队列被填满后所采取的措施称为饱和策略。使用有界队列时,队列的大小与线程池的大小必须一起调节。如果线程池比较大,那么有助于减少内存使用量,降低CPU的使用率,同时还可以减少上下文切换,但付出的代价是可能会限制吞吐量。
3)同步移交:
对于非常大的或者无界的线程池,可以通过使用SynchronousQueue来避免任务排队,以及直接将任务从生产者移交给工作者线程。SynchronousQueue不是一个真正的队列,而是一种在线程之间进行移交的机制。要将一个元素放入SynchronousQueue中,必须有另外一个线程正在接受这个元素。如果没有线程正在等待,并且线程池的当前大小小于最小值,那么ThreadPoolExecutor将创建一个新的线程,否则根据饱和策略,这个任务将被拒绝。使用直接移交将更高效,因为任务会直接移交给执行它的线程,在newCachedThreadPool工厂方法中就是用了SynchronousQueue。
当使用了像LinkedBlockingQueue或者ArrayBlockingQueue这样的FIFO(先进先出)队列时,任务的执行顺序与它们到达的顺序相同。如果想进一步控制任务的执行顺序,可以使用PriorityBlockingQueue,这个队列将根据优先级来安排任务。
只有任务相互独立时,为线程或者工作队列设置界限才是合理的。如果任务之间存在依赖性,那么有界的线程或者队列可能会冬至线程的“饥饿”死锁问题。此时应该使用无界的线程池,如newCachedThreadPool。
线程工厂
参数类型 | 参数名 | 描述 |
---|---|---|
ThreadFactory | threadFactory | 用于创建线程的工厂 |
类库中创建线程池方法参数中一般指定了线程池大小如Executors.newFixedThreadPool(nThreads),而另外一种创建线程池方法是Executors.newFixedThreadPool(nThreads,threadFactory),每当线程池需要创建一个线程时,都会调用线程工厂方法ThreadFactory来完成。默认的线程工厂方法将创建一个新的,非守护的线程,并且不包含特殊的配置信息。通过指定的线程工厂方法,可以定制线程池的配置信息。
在ThreadFactory中只定义了一个方法newThread,每当线程池创建一个新线程时都会调用这个方法。
public interface ThreadFactory {
Thread newThread(Runnable r);
}
多种情况下都需要定制线程工厂,如下是一个简单的线程工厂方法的实现,指定线程的名称。线程池会调用newThread()方法创建线程。
public class FixedThreadPoolDemo {
public static void main(String[] args) {
ExecutorService exec = Executors.newFixedThreadPool(5,new ThreadFactory() {
private int number = 0;
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
t.setName("Thread-pool"+"-"+(++number));
return t;
}
});
Runnable task =()->{
try {
TimeUnit.SECONDS.sleep(1);
System.out.println(Thread.currentThread().getName() + " executing!!!");
} catch (InterruptedException e) {
e.printStackTrace();
}
};
for(int i =0 ; i< 5;i++) {
exec.submit(task);
}
exec.shutdown();
}
}
饱和策略
参数类型 | 参数名 | 描述 |
---|---|---|
RejectedExecutionHandler | handler | 饱和策略 |
当线程池和队列都满之后,线程池处于饱和状态,此时必须采取一种策略来处理新提交的任务。ThreadPoolExecutor的饱和策略可以通过调用setRejectExecutionHandler来修改,如果某个任务被提交到一个已被关闭的Executor时,也会用到饱和策略。JDK线程池框架提供了几种不同的RejectedExecutionHandler实现。
策略名称 | 描述 |
---|---|
AbortPolicy | 默认的饱和策略,该策略将抛出为检查的RejectedExecutionException. |
CallerRunsPolicy | 一种调节机制,策略不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者,从而降低新任务的流量。 |
DiscardPolicy | 新提交的任务会直接被抛弃 |
DiscardOldestPolicy | 抛弃下一个将被执行的任务,然后尝试提交新任务。 |
案例:自定义线程池,线程池基本大小为2,最大大小为3,线程存活时间是0秒,队列使用的是有界的LinkedBlockingQueue,最多能存储4任务。线程池饱和后将采用饱和策略来处理。
public class RejectionExecutionHandlerDemo {
public static void main(String[] args) {
//设置线程池基本大小为2,最大大小为3,而队列能存储4个任务
ThreadPoolExecutor exec = new ThreadPoolExecutor(2,3,0L,TimeUnit.SECONDS,new LinkedBlockingQueue<Runnable>(4));
//1. abortPolicy策略:任务超过7个将会抛出异常。
//exec.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
// 2. callerRunsPolicy策略:不会抛弃任务,也不会抛出异常,将任务回退到调用者,此例
//中会回退到主线程,让主线程执行
//exec.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
//3. DiscardOldestPolicy:抛弃最老的任务
// exec.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardOldestPolicy());
//4. DiscardPolicy:新提交的任务会被抛弃
exec.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardPolicy());
for(int i =0 ; i < 10;i++) {
exec.submit(new Task());
}
exec.shutdown();
}
static class Task implements Runnable{
private static int taskCount= 1;
private final int id = taskCount++;
@Override
public void run() {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
System.out.println("Thread interrupted "+e.getMessage());
}
System.out.println(Thread.currentThread().getName()+":"+"task-"+id+" Exeuction finished!!!");
}
}
}
1.AbortPolicy:“中止策略”,默认的饱和策略,会直接抛出RejectionExecutionException异常。
设置饱和策略的代码是exec.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
Exception in thread "main" java.util.concurrent.RejectedExecutionException: Task java.util.concurrent.FutureTask@55f96302 rejected from java.util.concurrent.ThreadPoolExecutor@3d4eac69[Running, pool size = 3, active threads = 3, queued tasks = 4, completed tasks = 0]
at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2063)
at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:830)
at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1379)
at java.util.concurrent.AbstractExecutorService.submit(AbstractExecutorService.java:112)
at threadSocket.RejectionExecutionHandlerDemo.main(RejectionExecutionHandlerDemo.java:21)
pool-1-thread-1:task-1 Exeuction finished!!!
pool-1-thread-2:task-2 Exeuction finished!!!
pool-1-thread-3:task-7 Exeuction finished!!!
pool-1-thread-1:task-3 Exeuction finished!!!
pool-1-thread-3:task-5 Exeuction finished!!!
pool-1-thread-2:task-4 Exeuction finished!!!
pool-1-thread-1:task-6 Exeuction finished!!!
执行结果:执行了7个任务,第8个任务提交的时候,直接抛出了异常。
2.CallerRunsPolicy:“调用者运行策略”,此策略不会直接抛弃任务,也不回抛出异常,而是将任务退回到调用者。它不会在线程池里面某个线程中执行新提交的任务,而是在一个调用execute的线程中执行任务。
设置饱和策略的代码是exec.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
pool-1-thread-3:task-7 Exeuction finished!!!
main:task-8 Exeuction finished!!!
pool-1-thread-1:task-1 Exeuction finished!!!
pool-1-thread-2:task-2 Exeuction finished!!!
pool-1-thread-1:task-4 Exeuction finished!!!
pool-1-thread-3:task-3 Exeuction finished!!!
main:task-10 Exeuction finished!!!
pool-1-thread-2:task-5 Exeuction finished!!!
pool-1-thread-3:task-9 Exeuction finished!!!
pool-1-thread-1:task-6 Exeuction finished!!!
执行结果:所有任务得到了执行,2个任务在主线程中得到执行,其余任务在线程池中执行。
3.DiscardOldestPolicy:“抛弃最旧的策略”,策略将会抛弃下一个将被执行的任务,然后尝试提交新任务。如果工作队列是一个优先队列,那么“抛弃最旧的”将导致抛弃优先级最高的任务,因此最好不要将优先级队列和DiscardOldestPolicy一起使用。
设置饱和策略的代码是exec.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardOldestPolicy());
pool-1-thread-2:task-2 Exeuction finished!!!
pool-1-thread-1:task-1 Exeuction finished!!!
pool-1-thread-3:task-7 Exeuction finished!!!
pool-1-thread-1:task-8 Exeuction finished!!!
pool-1-thread-2:task-6 Exeuction finished!!!
pool-1-thread-3:task-9 Exeuction finished!!!
pool-1-thread-1:task-10 Exeuction finished!!!
执行结果:任务3,4,5,6被抛弃,后面添加的任务得到了执行。
4.DiscardPolicy:"抛弃策略",直接抛弃任务,线程池不作处理。
设置饱和策略的代码是exec.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardPolicy());
pool-1-thread-3:task-7 Exeuction finished!!!
pool-1-thread-2:task-2 Exeuction finished!!!
pool-1-thread-1:task-1 Exeuction finished!!!
pool-1-thread-3:task-4 Exeuction finished!!!
pool-1-thread-1:task-5 Exeuction finished!!!
pool-1-thread-2:task-3 Exeuction finished!!!
pool-1-thread-3:task-6 Exeuction finished!!!
执行结果:任务8,9,10直接被抛弃,前面的任务得到执行。
线程池配置及执行流程
1.线程池配置
通过调用ThreadPoolExecutor构造函数来自定义线程池,对于上面所提到的几种参数,要仔细斟酌。合理配置线程池,可以使得线程池的执行任务效率提升,避免资源额外消耗。
线程池理想大小取决于被提交任务的类型以及所部署系统的特性,通常线程池大小可以进行配置或者动态计算Runtime.getRuntime().availableProcessors(),避免将线程池大小设置为极大或者极小。如果线程池过大,那么大量的线程会在相对较少的CPU和内存资源上发生竞争,这会导致更高的内存使用量,还可能耗尽资源。如果线程池过小,那么将会导致许多空闲处理器无法执行工作,从而降低了吞吐率。
可以根据任务是计算密集型还是IO密集型、任务执行时间长短、任务是否依赖数据库连接池这样的稀缺资源,任务优先级等几个方面, 将不同类型的任务使用不同规模的线程池来执行,从而使得每个线程池可以根据各自工作负载来调整,同一个线程池中的任务是用类型的并且是相互独立时(任务之间不相互依赖),线程池的性能才能达到最大。例如对于计算密集型的任务,在用于N个处理器的系统上,当线程池的大小为N+1时,通常能实现最优的利用率(额外的1个线程可以在有线程出现故障或者其他原因而暂停时,确保CPU的时钟周期不会被浪费)。
2.线程池的执行流程
对于ThreadPoolExecutor执行任务的方法execute()可以分为下面几个步骤:
步骤一:线程池中当前运行的线程数量少于corePoolSize,则会创建新线程来执行任务。
步骤二:如果当前运行的线程数量等于或者大于corePoolSize时,会将任务添加到队列BlockingQueue中。
步骤三:队列已经满后,无法将任务添加到队列BlockingQueue中,则会创建新线程来执行任务。
步骤四:如果创建新线程后,线程池中线程数量超过了maximumPoolSize,将会调用线程池ThreadPoolExecutor中的方法RejectedExecutionHandler.rejectedExecution()执行设置的饱和策略。
参考
《Java编程思想》《Java并发编程实战》