为什么引入Executor线程池框架
① 重用存在的线程,减少对象创建、消亡的开销,提高性能;
② 线程的创建和运行分开,达到解耦目的;
③ 可有效控制最大并发线程数,提高系统资源的使用率;
Executor原理
Executor生命周期
- RUNNING:可以接收新任务,并且处理阻塞队列中的任务
- SHUTDOWN:关闭状态,不能接收新任务,可以继续处理阻塞队列中的任务。在线程池处于 RUNNING 状态时,调用 shutdown()方法会使线程池进入到该状态。
- STOP:不能接收新任务,也不能处理阻塞队列中的任务,会中断正在处理的任务。在线程池处于 RUNNING 或 SHUTDOWN 状态时,调用 shutdownNow() 方法会使线程池进入到该状态。
- TIDYING:所有任务都执行完成,线程池中workerCount (有效线程数) 为0。线程池进入该状态后会调用 terminated() 方法进入TERMINATED 状态。
- TERMINATED:调用terminated()方法进入该状态。
Executor的使用
说明:
- Executor 执行器接口,该接口定义执行Runnable任务的方式。
- ExecutorService 该接口定义提供对Executor的服务。
- ScheduledExecutorService 定时调度接口。
- AbstractExecutorService 执行框架抽象类。
- ThreadPoolExecutor JDK中线程池的具体实现。
- Executors 线程池工厂类。
ThreadPoolExecutor 线程池类
线程池是一个复杂的任务调度工具,它涉及到任务、线程池等的生命周期问题。要配置一个线程池是比较复杂的,尤其是对于线程池的原理不是很清楚的情况下,很有可能配置的线程池不是较优的,JDK中的线程池均由ThreadPoolExecutor类实现。
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}
参数说明:
corePoolSize:核心线程数。
maximumPoolSize:最大线程数。
keepAliveTime:线程存活时间。当线程数大于core数,那么超过该时间的线程将会被终结。
unit:keepAliveTime的单位。java.util.concurrent.TimeUnit类存在静态静态属性: NANOSECONDS、MICROSECONDS、MILLISECONDS、SECONDS
workQueue:Runnable的阻塞队列。若线程池已经被占满,则该队列用于存放无法再放入线程池中的Runnable。
ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit,BlockingQueue<Runnable> workQueue, RejectedExecutionHandler handler)
该方法在下面的扩展部分有更深入的讲解。其中handler表示线程池对拒绝任务的处理策略。
ThreadPoolExecutor的使用需要注意以下概念:
-
若线程池中的线程数量小于corePoolSize,即使线程池中的线程都处于空闲状态,也要创建新的线程来处理被添加的任务。
-
若线程池中的线程数量等于 corePoolSize且缓冲队列 workQueue未满,则任务被放入缓冲队列。
-
若线程池中线程的数量大于corePoolSize且缓冲队列workQueue满,且线程池中的数量小于maximumPoolSize,则建新的线程来处理被添加的任务。
-
若线程池中线程的数量大于corePoolSize且缓冲队列workQueue满,且线程池中的数量等于maximumPoolSize,那么通过 handler所指定的策略来处理此任务。
- 当线程池中的线程数量大于corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被终止。
扩展1:如果 corePoolSize = 0且阻塞队列是无界的。线程池将如何工作?
如果按照上文的逻辑,应该没有线程会被运行,然后线程无限的增加到队列里面。
下面代码会执行?与上面的原理冲突了??
public class threadTest {
private final static ThreadPoolExecutor executor = new ThreadPoolExecutor(0,1,0, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>());
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger();
while (true) {
executor.execute(() -> {
System.out.println(atomicInteger.getAndAdd(1));
});
}
}
}
我们还是从源码来看看到底线程池的逻辑是什么?
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get();
//如果工作线程数小于核心线程数,
if (workerCountOf(c) < corePoolSize) {
//执行addWork,提交为核心线程,提交成功return。提交失败重新获取ctl
if (addWorker(command, true))
return;
c = ctl.get();
}
//如果工作线程数大于核心线程数,则检查线程池状态是否是正在运行,且将新线程向阻塞队列提交。
if (isRunning(c) && workQueue.offer(command)) {
//recheck 需要再次检查,主要目的是判断加入到阻塞队里中的线程是否可以被执行
int recheck = ctl.get();
//如果线程池状态不为running,将任务从阻塞队列里面移除,启用拒绝策略
if (! isRunning(recheck) && remove(command))
reject(command);
// 如果线程池的工作线程为零,则调用addWoker提交任务
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
//添加非核心线程失败,拒绝
else if (!addWorker(command, false))
reject(command);
}
addWorker
private boolean addWorker(Runnable firstTask, boolean core) {
retry:
for (;;) {
int c = ctl.get();
//获取线程池状态
int rs = runStateOf(c);
// Check if queue empty only if necessary.
// 判断是否可以添加任务。
if (rs >= SHUTDOWN &&
! (rs == SHUTDOWN &&
firstTask == null &&
! workQueue.isEmpty()))
return false;
for (;;) {
//获取工作线程数量
int wc = workerCountOf(c);
//是否大于线程池上限,是否大于核心线程数,或者最大线程数
if (wc >= CAPACITY ||
wc >= (core ? corePoolSize : maximumPoolSize))
return false;
//CAS 增加工作线程数
if (compareAndIncrementWorkerCount(c))
break retry;
c = ctl.get(); // Re-read ctl
//如果线程池状态改变,回到开始重新来
if (runStateOf(c) != rs)
continue retry;
// else CAS failed due to workerCount change; retry inner loop
}
}
boolean workerStarted = false;
boolean workerAdded = false;
Worker w = null;
//上面的逻辑是考虑是否能够添加线程,如果可以就cas的增加工作线程数量
//下面正式启动线程
try {
//新建worker
w = new Worker(firstTask);
//获取当前线程
final Thread t = w.thread;
if (t != null) {
//获取可重入锁
final ReentrantLock mainLock = this.mainLock;
//锁住
mainLock.lock();
try {
// Recheck while holding lock.
// Back out on ThreadFactory failure or if
// shut down before lock acquired.
int rs = runStateOf(ctl.get());
// rs < SHUTDOWN ==> 线程处于RUNNING状态
// 或者线程处于SHUTDOWN状态,且firstTask == null(可能是workQueue中仍有未执行完成的任务,创建没有初始任务的worker线程执行)
if (rs < SHUTDOWN ||
(rs == SHUTDOWN && firstTask == null)) {
// 当前线程已经启动,抛出异常
if (t.isAlive()) // precheck that t is startable
throw new IllegalThreadStateException();
//workers 是一个 HashSet 必须在 lock的情况下操作。
workers.add(w);
int s = workers.size();
//设置 largeestPoolSize 标记workAdded
if (s > largestPoolSize)
largestPoolSize = s;
workerAdded = true;
}
} finally {
mainLock.unlock();
}
//如果添加成功,启动线程
if (workerAdded) {
t.start();
workerStarted = true;
}
}
} finally {
//启动线程失败,回滚。
if (! workerStarted)
addWorkerFailed(w);
}
return workerStarted;
}
分析源码得出结论:
线程池的原理应该是:
(1)如果当前运行的线程,少于corePoolSize,则创建一个新的线程来执行任务。
(2)如果运行的线程等于或多于 corePoolSize,将任务加入 BlockingQueue。
(3)如果加入 BlockingQueue 成功,需要二次检查线程池的状态如果线程池没有处于 Running,则从 BlockingQueue 移除任务,启动拒绝策略。
如果线程池处于 Running状态,则检查工作线程(worker)为0,则创建新的线程来处理任务。如果启动线程数大于maximumPoolSize,任务将被拒绝策略拒绝。
(4)如果加入 BlockingQueue失败,则创建新的线程来处理任务。
(5)如果启动线程数大于maximumPoolSize,任务将被拒绝策略拒绝。
扩展2:使用无界队列的线程池会导致内存飙升吗?
我们以最常用的fixed线程池举例,他的线程池数量是固定的,因为他用的是近乎于无界的LinkedBlockingQueue,几乎可以无限制的放入任务到队列里。
所以只要线程池里的线程数量达到了corePoolSize指定的数量之后,接下来就维持这个固定数量的线程了。
然后,所有任务都会入队到workQueue里去,线程从workQueue获取任务来处理。
这个队列几乎永远不会满,当然这是几乎,因为LinkedBlockingQueue默认的最大任务数量是Integer.MAX_VALUE,非常大,近乎于可以理解为无限吧。
只要队列不满,就跟maximumPoolSize、keepAliveTime这些没关系了,因为不会创建超过corePoolSize数量的线程的。
那么此时万一每个线程获取到一个任务之后,他处理的时间特别特别的长,长到了令人发指的地步。比如处理一个任务要几个小时,此时会如何?
当然会出现workQueue里不断的积压越来越多得任务,不停的增加。
这个过程中会导致机器的内存使用不停的飙升,最后也许极端情况下就导致JVM OOM了,系统就挂掉了。