ThreadPoolExecutor是ExecutorService的一个实例,是一个线程池管理器,能够通过调用线程池里面的线程来执行异步任务。
ThreadPoolExecutor主要用来解决两个痛点。一,当执行大量异步任务时,线程池能够减少线程创建和切换的开销,提高性能。二,当有过多的异步任务执行时,程序性能可能因为资源瓶颈降低,甚至直接挂掉。而ThreadPoolExecutor提供了一种管理线程资源的方法,它能够控制线程创建的数量,甚至是拒绝新任务。另外,ThreadPoolExecutor也提供一些数据统计功能,例如已完成任务数量、当前线程数目等。
为了适应各种应用场景,ThreadPoolExecutor提供了一系列可配置参数,例如corePoolSize(核心线程数目)、maximumPoolSize(最大线程数目)、keepAliveTime(线程空闲时间)、workQueue(任务存放队列)等。
下面让我们来理解一下这几个参数的作用。
首先是corePoolSize和maximumPoolSize。一个ThreadPoolExecutor会根据corePoolSize和maximumPoolSize设置的边界条件,自动的调整线程池里面线程的数量。调整规则如下:当一个新任务通过execute方法提交了,如果此时线程池里的线程数量少于corePoolSize,那么线程池会直接创建一个新线程来执行该任务,即使线程池里面存在空闲线程。继续提交任务,如果某时刻线程池里面的线程数量大于corePoolSize但小于maximumPoolSize,那么当且仅当工作队列(workQueue)满了的时候才会创建新的线程。如果线程数量达到maximumPoolSize且全部线程都是运行状态,继续提交任务,则会根据RejectedExecutionHandler来处理。
为了理解线程池线程数量的变化规则,我们看一下下面的这个演示程序。
package com.code.threadPool;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class Test {
public static void main(String[] args) {
int corePoolSize = 2;
int maximumPoolSize = 5;
int keepAliveTime = 10;
BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(4, true);
ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime,
TimeUnit.SECONDS, workQueue);
test(poolExecutor, 2);
//执行完所有任务后关闭线程池
poolExecutor.shutdown();
}
static void test(ThreadPoolExecutor poolExecutor, int taskCount) {
for (int i = 0; i < taskCount; i++) {
poolExecutor.submit(new TestRunnable());
System.out.println("总任务数目:" + poolExecutor.getTaskCount() + ",已完成任务数目:"
+ poolExecutor.getCompletedTaskCount() + ",线程池线程数目:" + poolExecutor.getPoolSize()
+ ",线程池处于运行状态的线程数目:" + poolExecutor.getActiveCount());
}
}
}
class TestRunnable implements Runnable {
@Override
public void run() {
/*try {
TimeUnit.SECONDS.sleep(1l);
} catch (InterruptedException e) {
e.printStackTrace();
}*/
System.out.println(Thread.currentThread().getName()+"执行完毕");
}
}
我们初始化了一个corePoolSize=2,maximumPoolSize=5,workQueue大小为4的线程池,线程空闲时间为10秒。然后我们调整test方法的参数taskCount,看一下程序运行结果。
首先,测试taskCount=2的结果如下。对比上面的线程创建规则,我们可以看到,当我们的任务很快执行,空闲线程依然存活。此时,我们提交第二个任务,虽然线程池里面存在空闲线程,但由于线程数目<corePoolSize(=2),此时仍然会创建新的线程。
总任务数目:1,已完成任务数目:0,线程池线程数目:1,线程池处于运行状态的线程数目:1
pool-1-thread-1执行完毕
总任务数目:2,已完成任务数目:1,线程池线程数目:2,线程池处于运行状态的线程数目:1
pool-1-thread-2执行完毕
然后我们打开TestRunnable里面注释的那段代码,增加任务的执行时间,测试taskCount=5,结果如下。我们发现,虽然任务的总数目大于线程池线程数目,但由于workQueue未满(理论值为3,原因可以自己想想),线程池并不会创建新的线程。
总任务数目:1,已完成任务数目:0,线程池线程数目:1,线程池处于运行状态的线程数目:1
总任务数目:2,已完成任务数目:0,线程池线程数目:2,线程池处于运行状态的线程数目:2
总任务数目:3,已完成任务数目:0,线程池线程数目:2,线程池处于运行状态的线程数目:2
总任务数目:4,已完成任务数目:0,线程池线程数目:2,线程池处于运行状态的线程数目:2
总任务数目:5,已完成任务数目:0,线程池线程数目:2,线程池处于运行状态的线程数目:2
pool-1-thread-2执行完毕
pool-1-thread-1执行完毕
pool-1-thread-1执行完毕
pool-1-thread-2执行完毕
pool-1-thread-1执行完毕
接下来测试taskCount=7,结果如下。分析一下,我们的corePoolSize=2,可以执行两个任务,workQueue的大小为4,可以放入4个任务,这样一共是6个任务。那么,当第7个任务到来的时候,workQueue就没地方放了,因此只能创建新的线程来执行,线程池线程数目就变成3了。其实,workQueue满说明任务很多,线程池线程不够,因此需要创建新的线程了。
总任务数目:1,已完成任务数目:0,线程池线程数目:1,线程池处于运行状态的线程数目:1
总任务数目:2,已完成任务数目:0,线程池线程数目:2,线程池处于运行状态的线程数目:2
总任务数目:3,已完成任务数目:0,线程池线程数目:2,线程池处于运行状态的线程数目:2
总任务数目:4,已完成任务数目:0,线程池线程数目:2,线程池处于运行状态的线程数目:2
总任务数目:5,已完成任务数目:0,线程池线程数目:2,线程池处于运行状态的线程数目:2
总任务数目:6,已完成任务数目:0,线程池线程数目:2,线程池处于运行状态的线程数目:2
总任务数目:7,已完成任务数目:0,线程池线程数目:3,线程池处于运行状态的线程数目:3
pool-1-thread-1执行完毕
pool-1-thread-3执行完毕
pool-1-thread-2执行完毕
pool-1-thread-3执行完毕
pool-1-thread-1执行完毕
pool-1-thread-2执行完毕
pool-1-thread-3执行完毕
最后,我们测试taskCount=10的情况,结果如下。我们的maximumPoolSize为5,最多处理5个任务,workQueue的大小为4,最多可以放入4个任务,这样一共是9个任务。想一下,第10个任务来了怎么处理?此时,既没有空闲线程,workQueue也满了,线程数达到最大,也不能创建新线程。这个时候,线程池会根据RejectedExecutionHandler指定的策略来处理,默认策略为AbortPolicy,它会抛出RejectedExecutionException异常。
总任务数目:1,已完成任务数目:0,线程池线程数目:1,线程池处于运行状态的线程数目:1
总任务数目:2,已完成任务数目:0,线程池线程数目:2,线程池处于运行状态的线程数目:2
总任务数目:3,已完成任务数目:0,线程池线程数目:2,线程池处于运行状态的线程数目:2
总任务数目:4,已完成任务数目:0,线程池线程数目:2,线程池处于运行状态的线程数目:2
总任务数目:5,已完成任务数目:0,线程池线程数目:2,线程池处于运行状态的线程数目:2
总任务数目:6,已完成任务数目:0,线程池线程数目:2,线程池处于运行状态的线程数目:2
总任务数目:7,已完成任务数目:0,线程池线程数目:3,线程池处于运行状态的线程数目:3
总任务数目:8,已完成任务数目:0,线程池线程数目:4,线程池处于运行状态的线程数目:4
总任务数目:9,已完成任务数目:0,线程池线程数目:5,线程池处于运行状态的线程数目:5
Exception in thread "main" java.util.concurrent.RejectedExecutionException: Task java.util.concurrent.FutureTask@3d4eac69 rejected from java.util.concurrent.ThreadPoolExecutor@42a57993[Running, pool size = 5, active threads = 5, queued tasks = 4, completed tasks = 0]
at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2047)
at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:823)
at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1369)
at java.util.concurrent.AbstractExecutorService.submit(AbstractExecutorService.java:112)
at com.code.threadPool.Test.test(Test.java:27)
at com.code.threadPool.Test.main(Test.java:19)
pool-1-thread-4执行完毕
pool-1-thread-1执行完毕
pool-1-thread-5执行完毕
pool-1-thread-2执行完毕
pool-1-thread-3执行完毕
pool-1-thread-1执行完毕
pool-1-thread-2执行完毕
pool-1-thread-4执行完毕
pool-1-thread-5执行完毕
如果线程池已经关闭,或者线程池已经饱和(线程数目达到maximumPoolSize,workQueue已满),此时如果继续添加任务,此时会触发RejectedExecutionHandler.rejectedExecution(Runnable, ThreadPoolExecutor)。RejectedExecutionHandler可以理解成线程池拒绝服务的一个回调策略,它有下面四种策略:1.AbortPolicy,线程池的默认策略,直接抛出RejectedExecutionException异常。2.CallerRunsPolicy,调用execute的线程自己去执行提交的任务。该策略其实是一个反馈控制,降低了向线程池提交任务的速度。3.DiscardPolicy,直接丢弃任务。4.DiscardOldestPolicy,丢弃处于workQueue队列头位置的任务,然后尝试重新执行execute方法。
Java里面任何一个对象都有创建方法,线程池里面的线程也不例外。ThreadPoolExecutor里面的线程池是通过一个线程工厂ThreadFactory创建的,如果不制定,则会默认使用Executors.defaultThreadFactory,其实现逻辑见下面代码。通过该线程工厂创建的线程,都处于相同的ThreadGroup,优先级都为Thread.NORM_PRIORITY,且都是非守护线程。如果需要自己定义线程的名字、优先级、线程组、守护状态,我们可以创建自己的线程工厂。
static class DefaultThreadFactory implements ThreadFactory {
private static final AtomicInteger poolNumber = new AtomicInteger(1);
private final ThreadGroup group;
private final AtomicInteger threadNumber = new AtomicInteger(1);
private final String namePrefix;
DefaultThreadFactory() {
SecurityManager s = System.getSecurityManager();
group = (s != null) ? s.getThreadGroup() :
Thread.currentThread().getThreadGroup();
namePrefix = "pool-" +
poolNumber.getAndIncrement() +
"-thread-";
}
public Thread newThread(Runnable r) {
Thread t = new Thread(group, r,
namePrefix + threadNumber.getAndIncrement(),
0);
if (t.isDaemon())
t.setDaemon(false);
if (t.getPriority() != Thread.NORM_PRIORITY)
t.setPriority(Thread.NORM_PRIORITY);
return t;
}
}
下面我们来看一下keepAliveTime这个参数,该属性跟TimeUnit一起,指定了超过corePoolSize的线程的最大存活时间。如果超过该时间,线程就会被销毁。注意,默认情况下,该参数只对超过corePoolSize的线程生效。
最后,我们来看一下workQueue这个参数,类型为BlockingQueue。workQueue主要是用来存放提交的任务,主要分为下面三种。一、SynchronousQueue。该模式下,被提交的任务会直接交给线程池处理,队列不存放任何任务。为了防止线程池拒绝提交的任务,通常需要设置一个无界的maximumPoolSizes,以此保证提交的任务能够获得线程去执行。二、无界队列。当所有的corePoolSize的线程都忙,新提交的任务会直接进入队列等待。因此,该模式下线程的数量不会超过corePoolSize,此时的maximumPoolSizes实际上并无意义。三、有界队列。通过设置maximumPoolSizes,可以设置最大线程数目,防止资源耗尽。该模式下,maximumPoolSizes和workQueue的大小会相互影响:使用较大容量的队列,较小的maximumPoolSizes,可以降低CPU消耗率、系统资源使用率、线程切换开销,但有可能导致低吞吐量。另一方面,使用较小的队列,通常需要较大的线程数目maximumPoolSizes,这会导致CPU持续繁忙。
ThreadPoolExecutor构造器参数的多样性也使得它的使用较为复杂,为了简化线程池的使用,jdk给我们提供了一个更为简单的线程池的工厂类Executors。