程序的运行,其本质上,是对系统资源(CPU、内存、磁盘、网络等等)的使用。如何高效的使用这些资源是编程优化演进的一个方向。线程池就是一种对提高系统资源利用的优化手段。
在多个线程并发处理任务的情况下,如果每个线程只是执行一个时间很短的任务就结束,那么线程的创建和销毁会占用相当一部分系统资源。线程池就是在池中创建多个活跃的线程,当任务到来时,就分配若干线程去执行任务,任务执行完之后,线程不会销毁,而是放回到线程池中,等待执行新的任务。这样就避免了线程频繁的创建和销毁。
参考博客:
https://www.cnblogs.com/rinack/p/9888717.html
https://www.cnblogs.com/dolphin0520/p/3932921.html
用于线程池创建的类为ThreadPoolExecutor,线程池具备何种功能/属性与构建线程池时传入的参数相关。在工厂类Executors中,有已经设计好的线程池,可以直接拿来使用。JDK推荐使用Executors工厂类来创建线程池(programmers are urged to use the more convenient {@link Executors} factory methods),但是如果要根据实际情况来自己设计线程池的话,还是要用ThreadPoolExecutor。
下面即从ThreadPoolExecutor入手来学习线程池。
ThreadPoolExecutor继承自抽象类AbstractExecutorService。
AbstractExecutorService实现自ExecutorService接口中的方法
ExecutorService接口继承Executor,Executor是An object that executes submitted {@link Runnable} tasks.(一个用来执行提交的任务的类),里面只有一个方法execute(Runnable),execute()是一个关键方法,用来执行线程任务。
mark部分JDK文档中对ThreadPoolExecutor类的说明。
-corePoolSize & maximumPoolSize:
当一个新任务(Runnable target)被execute()方法提交执行,有以下两种情况:
如果线程数小于核心池(corePoolSize )大小,将会创建一个新线程来执行任务,即便其他线程处于空闲状态。
如果线程数大于核心池大小,但小于线程池最大容量(maximumPoolSize),只有当任务队列已经满员的时候才会创建新线程,否则存入队列等待执行。
如果corePoolSize和maximumPoolSize相同,就得到一个fixed-size thread pool。
-On-demand construction:按需构建
默认情况下,只有当任务到来,才会执行线程的创建和启动。但prestartCoreThread()和prestartAllCoreThreads()允许在构建线程池之初创建好线程。
-ThreadFacotry
ThreadFactory用于创建线程,默认情况下使用defaultThreadFactory来创建线程,所创建的线程都在同一ThreadGroup中,并且具有相同的优先级,均为no-daemon线程。
-Keep-alive times
如果当前线程池线程数多于corePoolSize,则超过corePoolSize线程数的线程空闲时间超过Keep-alive times就会被停止。如果为0,线程将一直存在。
默认情况下,Keep-alive times仅适用于多于corePoolSize的线程,allowCoreThreadTimeOut(boolean)方法可让keep-alive适用于包括core线程在内的所有线程。即所有线程只要空闲一定时间就会被终结。
-Queuing 队列
BlockingQueue被用于转换(transfer)或持有(hold)任务。源码中的transfer,参考SynchronousQueue,自己的理解其实是传递,SynchronousQueue只是一个中转,到达一个任务就把任务交给一个线程,本身不存储任务。
一般情况下,当新任务到来时,如果线程数小于corePoolSize,将会创建新线程,而非存入队列。
如果corePoolSize或大于corePoolSize的线程在运行,将会存入队列而非创建新线程。
如果任务不能被存入队列(队列已满或其他原因),将会创建一个新线程。但是,如果当前线程数已经达到maximumPoolSize,并且队列已满,这个任务会被拒绝。
队列的三种策略(strategies):
Direct handoffs:
直接传递:SynchronousQueue,直接将任务传递给线程而不是持有(hold)他们,所以SynchronousQueue的大小永远为0。这种策略下,线程数会一直增长,如果当前没有线程可用,试图queue一个任务将会失败。所以Direct handoffs策略要求maximumPoolSizes为无限大(unbounded),这样才能避免拒绝一个任务。但maximumPoolSizes无限大,当任务到来时直接创建线程。当任务到达的平均速度比它们被处理的速度还要快时,就有可能出现无限的线程增长。
Unbounded queues:
无限大的队列:LinkedBlockingQueue,队列无限大,这样当任务到来时,如果核心线程数未达到corePoolSize,创建新线程,如果已经达到corePoolSize,任务会存入队列。此后线程数不会再增长,只会将任务一直存到这个无限大的队列中,也就是线程池最大线程数就是corePoolSize,maximumPoolSize无效。
这种策略适用于,传递的多个任务(tasks)之间是独立的、相互不影响的,适用于处理多个执行时间短、数量庞大的任务,当任务到达的平均速度比它们被处理的速度还要快时,就有可能出现队列无线的增长。
Bounded queues:
有限队列:ArrayBlockingQueue,当任务到来时,如果未达到corePoolSize,线程数增长;已达到corePoolSize未达到maximumPoolSize,存入任务队列,如果线程数已达到最大,且任务队列已满,拒绝新到来的任务。
有限队列配合合适大小的maximumPoolSize,使用容量大的队列(queue)和小的maximumPoolSize会使cpu消耗、系统资源(OS Resource)达到最小,但是可能会导致任务处理效率低(low throughput);使用较小队列需要较大的maximumPoolSize,这样能够充分利用CPU,但也许会导致无法接受的调度消耗(encounter unacceptable scheduling overhead),这样同样会降低任务处理效率。
-Rejected tasks 任务拒绝
当线程池已关闭,或是线程数已达maximumPoolSize、队列已满,execute()提交的任务会被拒绝。四种任务拒绝策略:
这样洋洋洒洒看下来,线程池这一块还蛮复杂,所以JAVA开发者开发了Executors类,定义好了一些适用于大多数情况的连接池,让我们可以直接拿来使用,如此在使用线程池时,就不用自己费心去设计、考虑以上那些内容了。所以JAVA的优势不仅在于语法简单,更重要的是,其内部还为我们默默实现了很多内容,这些内容不只是语法上的,比如指针,还包括开发者需求方面,就比如Executors直接创建线程池。
ThreadPoolExecutor类中共有四个构造方法,传入的参数有所差别,以下列举其中一个构造方法。
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.acc = System.getSecurityManager() == null ?
null :
AccessController.getContext();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
参数说明:
public class TestPool {
public static void main(String[] args) {
// corePoolSize:5;maximumPoolSize:10;有限队列ArrayBlockingQueue
ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 10, 200, TimeUnit.MILLISECONDS,
new ArrayBlockingQueue<>(5));
for (int i = 0; i < 15; i++) {
Task task = new Task(i);
executor.execute(task);
System.out.println("线程数目:" + executor.getPoolSize() + "队列大小:" + executor.getQueue().size());
}
executor.shutdown();
}
static class Task implements Runnable {
private int taskNum;
public Task(int taskNum) {
this.taskNum = taskNum;
}
@Override
public void run() {
System.out.println("第-" + taskNum + "-个task正在运行");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("第-" + taskNum + "-个task停止运行");
}
public int getTaskNum() {
return taskNum;
}
public void setTaskNum(int taskNum) {
this.taskNum = taskNum;
}
}
}
在用构造器构建好线程池之后,还是可以对线程池参数进行调整,如以下这些方法:
首先需要指出的一点是,用Executors构建的线程池,其参数是不可再调整的,因为Executors提供的线程池构建方法,返回的都是ExecutorService的实例,ExecutorService中并没有修改线程池的方法。
-CachedThreadPool:
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue());
}
按需构建线程的线程池,maximumPoolSize为无限大,时间单位为秒,移除空闲时间超过60秒的线程,使用的work queue为SynchronousQueue。
CachedThreadPool适用于处理生命周期短(short-lived)、异步的任务。而且能够保证线程的重用(reuse):如果线程池存在空闲的线程,将会利用这些线程而不是新建线程。
-FixedThreadPool:
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue());
}
固定大小的线程池,corePoolSize和maximumPoolSize相同,可以指定线程数nThreads,keep alive times为0,即创建的线程会一直存在,使用无线队列LinkedBlockingQueue。FixedThreadPool中最多只能存在nThreads个线程,多余的任务会在队列中存储。
-ScheduledThreadPool:
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
newScheduledThreadPool()返回的是ScheduledExecutorService,ScheduledExecutorService继承自ExecutorService,额外提供了schedule(Runnable command,long delay, TimeUnit unit)等方法,能够定时执行任务。
-SingleThreadExecutor:
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue()));
}
只有一个线程,任务队列为无限大。