一、使用线程池的好处
- 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
- 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
- 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
线程池提供了一种限制和管理资源(包括执行一个任务)。 每个线程池还维护一些基本统计信息,例如已完成任务的数量。
二 Executor 框架
2.1 简介
Executor
框架是 Java5 之后引进的,在 Java 5 之后,通过 Executor
来启动线程比使用 Thread
的 start
方法更好。包括了线程池的管理,还提供了线程工厂、队列以及拒绝策略等,Executor
框架让并发编程变得更加简单。
2.2 Executor 框架结构(主要由三大部分组成)
1) 任务(Runnable
/Callable
)
执行任务需要实现的 Runnable
接口 或 Callable
接口。Runnable
接口或 Callable
接口 实现类都可以被 ThreadPoolExecutor
或 ScheduledThreadPoolExecutor
执行。
2) 任务的执行(Executor
)
如下图所示,包括任务执行机制的核心接口 Executor
,以及继承自 Executor
接口的 ExecutorService
接口。ThreadPoolExecutor
和 ScheduledThreadPoolExecutor
这两个关键类实现了 ExecutorService 接口。
这里提了很多底层的类关系,但是,实际上我们需要更多关注的是 ThreadPoolExecutor
这个类,这个类在我们实际使用线程池的过程中,使用频率还是非常高的。
注意: 通过查看
ScheduledThreadPoolExecutor
源代码我们发现ScheduledThreadPoolExecutor
实际上是继承了ThreadPoolExecutor
并实现了 ScheduledExecutorService ,而ScheduledExecutorService
又实现了ExecutorService
,正如我们下面给出的类关系图显示的一样。
3) 异步计算的结果(Future
)
Future
接口以及 Future
接口的实现类 FutureTask
类都可以代表异步计算的结果。
当我们把 Runnable
接口 或 Callable
接口 的实现类提交给 ThreadPoolExecutor
或 ScheduledThreadPoolExecutor
执行。(调用 submit()
方法时会返回一个 FutureTask
对象)
2.3 Executor 框架的使用示意图
- 主线程首先要创建实现
Runnable
或者Callable
接口的任务对象。 - 把创建完成的实现
Runnable
/Callable
接口的 对象直接交给ExecutorService
执行:ExecutorService.execute(Runnable command)
)或者也可以把Runnable
对象或Callable
对象提交给ExecutorService
执行(ExecutorService.submit(Runnable task)
或ExecutorService.submit(Callable <T> task)
)。 - 如果执行
ExecutorService.submit(…)
,ExecutorService
将返回一个实现Future
接口的对象(我们刚刚也提到过了执行execute()
方法和submit()
方法的区别,submit()
会返回一个FutureTask 对象)。由于 FutureTask
实现了Runnable
,我们也可以创建FutureTask
,然后直接交给ExecutorService
执行。 - 最后,主线程可以执行
FutureTask.get()
方法来等待任务执行完成。主线程也可以执行FutureTask.cancel(boolean mayInterruptIfRunning)
来取消此任务的执行。
三 (重要)ThreadPoolExecutor 类简单介绍
先看几个重要的属性
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
private static final int COUNT_BITS = Integer.SIZE - 3;
// 二进制表示 00011111 11111111 11111111 11111111 = 536870911
private static final int CAPACITY = (1 << COUNT_BITS) - 1;
// 二进制表示 11100000 00000000 00000000 00000000
// pas:这个地方用到了原反补码的知识点,不知道为何的请自行查阅
private static final int RUNNING = -1 << COUNT_BITS;
// 二进制表示 00000000 00000000 00000000 00000000
private static final int SHUTDOWN = 0 << COUNT_BITS;
// 二进制表示 00100000 00000000 00000000 00000000
private static final int STOP = 1 << COUNT_BITS;
// 二进制表示 01000000 00000000 00000000 00000000
private static final int TIDYING = 2 << COUNT_BITS;
// 二进制表示 01100000 00000000 00000000 00000000
private static final int TERMINATED = 3 << COUNT_BITS;
复制代码
ctl
是对线程池的运行状态和线程池中有效线程的数量进行控制的一个字段, 它包含两部分的信息: 线程池的运行状态 (runState) 和线程池内有效线程的数量 (workerCount),这里可以看到,使用了Integer类型来保存,高3位保存runState,低29位保存workerCount。COUNT_BITS 就是29,CAPACITY(capacity)就是1左移29位减1(29个1),这个常量表示workerCount的上限值,大约是5亿。
下面再介绍下线程池的运行状态. 线程池一共有五种状态, 分别是:
-
RUNNING :能接受新提交的任务,并且也能处理阻塞队列中的任务;
-
SHUTDOWN:关闭状态,不再接受新提交的任务,但却可以继续处理阻塞队列中已保存的任务。在线程池处于 RUNNING 状态时,调用 shutdown()方法会使线程池进入到该状态。(finalize() 方法在执行过程中也会调用shutdown()方法进入该状态);
-
STOP:不能接受新任务,也不处理队列中的任务,会中断正在处理任务的线程。在线程池处于 RUNNING 或 SHUTDOWN 状态时,调用 shutdownNow() 方法会使线程池进入到该状态;
-
TIDYING:如果所有的任务都已终止了,workerCount (有效线程数) 为0,线程池进入该状态后会调用 terminated() 方法进入TERMINATED 状态。
-
TERMINATED
在terminated() 方法执行完后进入该状态,默认terminated()方法中什么也没有做。
进入TERMINATED的条件如下:
- 线程池不是RUNNING状态;
- 线程池状态不是TIDYING状态或TERMINATED状态;
- 如果线程池状态是SHUTDOWN并且workerQueue为空;
- workerCount为0;
- 设置TIDYING状态成功。
下图为线程池的状态转换过程:
// Packing and unpacking ctl
// 获取线程池运行状态
private static int runStateOf(int c) { return c & ~CAPACITY; }
// 获取工作线程程数
private static int workerCountOf(int c) { return c & CAPACITY; }
// 获取运行状态和工作线程数的值
private static int ctlOf(int rs, int wc) { return rs | wc; }
复制代码
ThreadPoolExecutor的构造方法
ThreadPoolExecutor
类中提供的四个构造方法。我们来看最长的那个,其余三个都是在这个构造方法的基础上产生(其他几个构造方法说白点都是给定某些默认参数的构造方法比如默认制定拒绝策略是什么)。
提交线程的执行顺序为 corePoolSize –> workQueue –> maximumPoolSize。
/**
* Executors下有几个默认的实现,都是基于此构造方法来创建的
* 用给定的初始参数创建一个新的ThreadPoolExecutor。
*/
public ThreadPoolExecutor(int corePoolSize,//线程池的核心线程数量
int maximumPoolSize,//线程池的最大线程数
long keepAliveTime,//当线程数大于核心线程数时,多余的空闲线程存活的最长时间
TimeUnit unit,//时间单位
BlockingQueue<Runnable> 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.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
复制代码
ThreadPoolExecutor
3 个最重要的参数:
-
corePoolSize
: 核心线程数线程数定义了最小可以同时运行的线程数量。 -
maximumPoolSize
: 当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。 -
workQueue
: 当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。 -
keepAliveTime
:当线程池中的线程数量大于corePoolSize
的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了keepAliveTime
才会被回收销毁; -
TimeUnit
:keepAliveTime
参数的时间单位。 -
threadFactory
:executor 创建新线程的时候会用到。 -
handler
:如果maximumPoolSize所有的线程都没有空闲,那么触发饱和策略。线程池提供了4种策略:
- AbortPolicy:直接抛出异常,这是默认策略;
- CallerRunsPolicy:用调用者所在的线程来执行任务;
- DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务;
- DiscardPolicy:直接丢弃任务;
说完线程池的构造和简单原理性的东西,下面来看给线程池提交任务
public void execute(Runnable command) { if (command == null) throw new NullPointerException(); // clt记录着runState和workerCount int c = ctl.get(); // workerCountOf方法取出低29位的值,表示当前活动的线程数; // 如果当前活动线程数小于corePoolSize,则新建一个线程放入线程池中; // 并把任务添加到该线程中。 if (workerCountOf(c) < corePoolSize) { // 第二个参数表示限制添加线程的数量是根据corePoolSize来判断还是maximumPoolSize来判断; // 如果为true,根据corePoolSize来判断; // 如果为false,则根据maximumPoolSize来判断 if (addWorker(command, true)) return; // 加入队列失败重新获取ctl,因为线程池在时刻变化 c = ctl.get(); } // 如果当前线程池是运行状态并且任务添加到队列成功 if (isRunning(c) && workQueue.offer(command)) { // 加入队列失败重新获取ctl,因为线程池在时刻变化 int recheck = ctl.get(); // 不是运行状态不在接受新的任务,需要移除新任务 if (! isRunning(recheck) && remove(command)) // 执行过后通过handler使用拒绝策略对该任务进行处理,整个方法返回 reject(command); // 获取线程池中的有效线程数,如果数量是0,则执行addWorker方法 // 1. 第一个参数为null,表示在线程池中创建一个线程,但不去启动; // 2. 第二个参数为false,将线程池的有限线程数量的上限设置为maximumPoolSize, // 添加线程时根据maximumPoolSize来判断; // 如果判断workerCount大于0,则直接返回,在workQueue中新增的command会在将来的某个时刻被执行。 else if (workerCountOf(recheck) == 0) addWorker(null, false); } // 如果执行到这里,有两种情况: // 1. 线程池是RUNNING状态,但workerCount >= corePoolSize并且workQueue已满。 // 2. 线程池已经不是RUNNING状态;加入队列失败了 // 这时,再次调用addWorker方法,但第二个参数传入为false, // 将线程池的有限线程数量的上限设置为maximumPoolSize;如果失败则拒绝该任务 else if (!addWorker(command, false)) reject(command); } 复制代码
addWorker(null, false);可能有人会奇怪,为什么添加线程的时候为什么为空,因为任务已经加入的工作队列当中,所以worker在执行的时候,会直接从workQueue中获取任务。(我猜想可能是因为考虑到线程池有大量的简单任务时增加效率吧,等待有缘人补充完整)
下图简单的介绍了线程池接受一个任务时候的执行流程。
阅读上面的源码,说一个两个我面试时候遇到的面试题,
1、如果线程池运行完成一个任务之后,又来了一个新的任务,此时是新建一个线程还是使用原来的执行。
2、线程池回收的时候是按照创建顺序吗?
答案我放到文章最后,自己结合源码思考一下。
addWorker方法
在阅读addWorker可能有点绕,需要阅读并理解线程池的状态的表达
// 返回false代表添加工作线程失败
private boolean addWorker(Runnable firstTask, boolean core) {
retry:
for (;;) {
// clt记录着runState和workerCount
int c = ctl.get();
// 获取运行状态
int rs = runStateOf(c);
// 如果rs >= SHUTDOWN,具体看上面线程池的状态。则表示此时不再接收新任务;
if (rs >= SHUTDOWN &&
// 线程池关闭状态,不接受新任务
! (rs == SHUTDOWN &&
// 这种情况下不会接受新提交的任务,所以在firstTask不为空的时候会返回false;
firstTask == null &&
// 如果firstTask为空,并且workQueue也为空,则返回false,
! workQueue.isEmpty()))
// 因为队列中已经没有任务了,不需要再添加线程了
return false;
for (;;) {
// 获取工作线程数
int wc = workerCountOf(c);
// 工作的线程>=容量 或
if (wc >= CAPACITY ||
// 工作线程大于核心线程或最大线程个数
wc >= (core ? corePoolSize : maximumPoolSize))
return false;
// 尝试添加工作线程如果成功,跳出循环
if (compareAndIncrementWorkerCount(c))
break retry;
// 添加失败,重新获取ctl的值
c = ctl.get();
// 如果线程池的状态已经改变,说明状态已被改变,返回第一个for循环继续执行
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;
try {
// 创建一个工作线程
w = new Worker(firstTask);
// 每一个Worker对象都会创建一个线程
final Thread t = w.thread;
if (t != null) {
// 加锁,锁的状态可以查看我另一篇文章点我主页查看
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
int rs = runStateOf(ctl.get());
// rs < SHUTDOWN表示是RUNNING状态
if (rs < SHUTDOWN ||
// 线程池为关闭状态和没有新任务要提交
(rs == SHUTDOWN && firstTask == null)) {
if (t.isAlive()) // precheck that t is startable
throw new IllegalThreadStateException();
// 加入,workers是一个HashSet
workers.add(w);
int s = workers.size();
// largestPoolSize记录着线程池中出现过的最大线程数量
if (s > largestPoolSize)
largestPoolSize = s;
// 新线程添加成功的状态
workerAdded = true;
}
} finally {
mainLock.unlock();
}
// 添加成功启动线程
if (workerAdded) {
t.start();
workerStarted = true;
}
}
} finally {
// 启动失败则执行加入工作线程失败,需要移除线程
if (! workerStarted)
addWorkerFailed(w);
}
return workerStarted;
}
复制代码
小结
经过上述的execute()方法和addWorker()之后我们需要的任务已经在执行,或者在等待队列中了
面试题答案(记得自己思考一下哦)
第一题、从execute方法我们可以中我们可以看到workerCountOf(ctl.get()) 方法来判断线程池工作中的线程是否小于核心线程池的个数,工作线程数,如果小于就会新建一个线程并执行,线程池不管其他线程有没有在执行。
如果大于等于来核心线程池的数量,并且所有的执行线程已经执行完毕,创建一个空的线程,但需要和其他线程去抢队列的任务。
第二题,还是从execute方法中,可以看到,添加一个线程并没有标识线程是核心还是临时线程,所以线程池回收是按照线程是否空闲来回收的。
近期评论