ThreadpoolExecutor
/** corePoolSize:核心线程数
* maximumPoolSize:最大线程数
* keepAliveTime:超时时间,超出核心线程数量以外的线程空余存活时间
* unit:存活时间单位
* workQueue:保存执行任务的队列
* threadFactory:创建新线程使用的工厂
* handler:当任务无法执行的时候的处理方式
*/
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.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
Executors
newFixedThreadPool
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue());
}
FixedThreadPool 的核心线程数和最大线程数都是指定值,也就是说当线程池中的线程数超 过核心线程数后,任务都会被放到阻塞队列中。另外 keepAliveTime 为 0,也就是超出核心 线程数量以外的线程空余存活时间
而这里选用的阻塞队列是 LinkedBlockingQueue,使用的是默认容量 Integer.MAX_VALUE, 相当于没有上限
这个线程池执行任务的流程如下:
- 线程数少于核心线程数,也就是设置的线程数时,新建线程执行任务
- 线程数等于核心线程数后,将任务加入阻塞队列
- 由于队列容量非常大,可以一直添加
- 执行完任务的线程反复去队列中取任务执行
用途:FixedThreadPool 用于负载比较大的服务器,为了资源的合理利用,需要限制当前线程数量
newCachedThreadPool
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue());
}
CachedThreadPool 创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空 闲线程,若无可回收,则新建线程; 并且没有核心线程,非核心线程数无上限,但是每个空闲 的时间只有 60 秒,超过后就会被回收。
它的执行流程如下:
- 没有核心线程,直接向 SynchronousQueue 中提交任务
- 如果有空闲线程,就去取出任务执行;如果没有空闲线程,就新建一个
- 执行完任务的线程有 60 秒生存时间,如果在这个时间内可以接到新任务,就可以继续活下去,否则就被回收
newSingleThreadExecutor
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue()));
}
创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定 顺序(FIFO, LIFO, 优先级)执行
源码分析
execute
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get();
//1.当前池中线程比核心数少,新建一个线 程执行任务
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
//2.核心池已满,但任务队列 未满,添加到队列中
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
//任务成功添加到队列以后,再次检查是否需要添加新的线程,因为已存在的线程可能被销毁了
if (! isRunning(recheck) && remove(command))
//如果线程池处于非运行状态,并且把当前的任务从任务队列中 移除成功,则拒绝该任务
reject(command);
//如果之前的线程已被销毁完,新建一个 线程
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
//3.核心池已满,队列已满,试着创建一个新线程
else if (!addWorker(command, false))
//如果创建新线程失败了,说明线程池被关闭或者线程池完全满了, 拒绝任务
reject(command);
}
ctl作用
在线程池中,ctl贯穿了线程池的整个生命周期。
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
它是一个原子类,主要作用是用来保存线程数量和线程池的状态。我们来分析一下这段代码, 其实比较有意思,他用到了位运算
一个 int 数值是 32 个 bit 位,这里采用高 3 位来保存运行状态,低 29 位来保存线程数量。
我们来分析默认情况下,也就是 ctlOf(RUNNING)运行状态,调用了 ctlOf(int rs,int wc)方法; 其中
private static int ctlOf(int rs, int wc) { return rs | wc; }
其中 RUNNING =-1 << COUNT_BITS ; -1 左移 29 位. -1 的二进制是 32 个 1(1111 1111 1111 1111 1111 1111 1111 1111)
那么-1 <<左移 29 位, 也就是 【111】 表示; rs | wc 。二进制的 111 | 000 。得到的结 果仍然是 111
那么同理可得其他的状态的 bit 位表示
private static final int COUNT_BITS = Integer.SIZE - 3;//32-3
private static final int CAPACITY = (1 << COUNT_BITS) - 1;/将 1 的二进制 向右位移 29 位,再减 1 表示最大线程容量
//运行状态保存在 int 值的高 3 位 (所有数值左移 29 位)
private static final int RUNNING = -1 << COUNT_BITS;// 接收新任务,并执行队列中的任务
private static final int SHUTDOWN = 0 << COUNT_BITS;/ 不接收新任务,但是执行队列中的任务
private static final int STOP = 1 << COUNT_BITS;//不接受新任务,不执行队列中的任务,中断正在执行中的任务
private static final int TIDYING = 2 << COUNT_BITS;//所有任务都已结束,线程数量为 0,处于该状态的线程池即将调用 terminated()方法
private static final int TERMINATED = 3 << COUNT_BITS;// terminated()方法执行完成
addWorker
如果工作线程数小于核心线程数的话,会调用 addWorker,顾名思义,其实就是要创建一个 工作线程。我们来看看源码的实现 源码比较长,看起来比较唬人,其实就做了两件事。1)用循环 CAS 操作来将线程数加 1; 2)新建一个线程并启用。
private boolean addWorker(Runnable firstTask, boolean core) {
retry://goto语句,避免死循环
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);
/**
* 如果线程处于非运行状态,并且 rs 不等于 SHUTDOWN 且 firstTask 等于空且且 workQueue 为空,直接返回 false(表示不可添加 work 状态)
* 1. 线程池已经 shutdown 后,还要添加新的任务,拒绝
* 2. (第二个判断)SHUTDOWN 状态不接受新任务,但仍然会执行已经加入任务队列的任 务,所以当进入 SHUTDOWN 状态,而传进来的任务为空,并且任务队列不为空的时候,是允许添加 新线程的,如果把这个条件取反,就表示不允许添加 worker
*/
if (rs >= SHUTDOWN &&
! (rs == SHUTDOWN &&
firstTask == null &&
! workQueue.isEmpty()))
return false;
for (;;) {//自旋
int wc = workerCountOf(c);////获得Worker工作线程数
//如果工作线程数大于默认容量大小或者大于核心线程数大小,则直接返回 false 表示不 能再添加 worker。
if (wc >= CAPACITY ||
wc >= (core ? corePoolSize : maximumPoolSize))
return false;
//通过cas来增加工作线程数, 如果 cas 失败,则直接重试
if (compareAndIncrementWorkerCount(c))
break retry;
c = ctl.get(); //再次获取ctl的值
if (runStateOf(c) != rs)//这里如果不相等,说明线程的状态发生了变化,继续重试
continue retry;
// else CAS failed due to workerCount change; retry inner loop
}
}
//上面这段代码主要是对 worker 数量做原子+1 操作,下面的逻辑才是正式构建一个 worker
boolean workerStarted = false;//工作线程是否启动的标识
boolean workerAdded = false;//工作线程是否已经添加成功的标识
Worker w = null;
try {
w = new Worker(firstTask);//构建一个Worker,这个worker是什么呢?我们 可以看到构造方法里面传入了一个 Runnable 对象
final Thread t = w.thread;//从 worker 对象中取出线程
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());
//只有当前线程池是正在运行状态,[或是 SHUTDOWN 且 firstTask 为空],才能添加到 workers 集合中
if (rs < SHUTDOWN ||
(rs == SHUTDOWN && firstTask == null)) {
//任务刚封装到 work 里面,还没 start,你封装的线程就是 alive,几个意思?肯定是要抛异常出去的
if (t.isAlive()) // precheck that t is startable
throw new IllegalThreadStateException();
workers.add(w);//将新创建的Worker添加到workers集合中
int s = workers.size();
//如果集合中的工作线程数大于最大线程数,这个最大线程数表示线程池曾经出现过的最大线程数
if (s > largestPoolSize)
largestPoolSize = s;//更新线程池出现过的最大线程数
workerAdded = true;//表示工作线程创建成功了
}
} finally {
mainLock.unlock();//释放锁
}
if (workerAdded) {//如果worker添加成功
t.start();//启动线程
workerStarted = true;
}
}
} finally {
if (! workerStarted)
addWorkerFailed(w);//如果添加失败,就需要做一件事,就是递减实际工作线程数(还记得我们最开始的时候增加了工作线程数吗)
}
return workerStarted;//返回结果
}
Worker 类说明
我们发现 addWorker 方法只是构造了一个 Worker,并且把 firstTask 封装到 worker 中,它是 做什么的呢?我们来看看
- 每个worker,都是一条线程,同时里面包含了一个firstTask,即初始化时要被首先执行的任务.
- 最终执行任务的,是 runWorker()方法
Worker 类继承了 AQS,并实现了 Runnable 接口,注意其中的 firstTask 和 thread 属性: firstTask 用它来保存传入的任务;thread 是在调用构造方法时通过 ThreadFactory 来创建的 线程,是用来处理任务的线程。
在调用构造方法时,需要传入任务,这里通过 getThreadFactory().newThread(this);来新建 一个线程,newThread 方法传入的参数是 this,因为 Worker 本身继承了 Runnable 接口, 也就是一个线程,所以一个 Worker 对象在启动的时候会调用 Worker 类中的 run 方法。 Worker 继承了 AQS,使用 AQS 来实现独占锁的功能。为什么不使用 ReentrantLock 来实现呢?可以看到 tryAcquire 方法,它是不允许重入的,而 ReentrantLock 是允许重入的: lock 方法一旦获取了独占锁,表示当前线程正在执行任务中;那么它会有以下几个作用
- 如果正在执行任务,则不应该中断线程;
- 如果该线程现在不是独占锁的状态,也就是空闲的状态,说明它没有在处理任务,这时可以对该线程进行中断;
- 线程池在执行 shutdown 方法或 tryTerminate 方法时会调用 interruptIdleWorkers 方法来 中断空闲的线程,interruptIdleWorkers 方法会使用 tryLock 方法来判断线程池中的线程 是否是空闲状态
- 之所以设置为不可重入,是因为我们不希望任务在调用像 setCorePoolSize 这样的线程池 控制方法时重新获取锁,这样会中断正在运行的线程
private final class Worker
extends AbstractQueuedSynchronizer
implements Runnable
{
private static final long serialVersionUID = 6138294804551838833L;
//注意了,这才是真正执行task的线程,从构造函数可知是由 ThreadFactury 创建的
final Thread thread;
//这就是首先需要执行的 task
Runnable firstTask;
//完成的任务数,用于线程池统计
volatile long completedTasks;
Worker(Runnable firstTask) {
setState(-1); //初始状态 -1,防止在调用 runWorker(),也就是真正执行 task前中断 thread。
this.firstTask = firstTask;
this.thread = getThreadFactory().newThread(this);
}
/** Delegates main run loop to outer runWorker */
public void run() {
runWorker(this);
}
// Lock methods
//
// The value 0 represents the unlocked state.
// The value 1 represents the locked state.
protected boolean isHeldExclusively() {
return getState() != 0;
}
protected boolean tryAcquire(int unused) {
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
protected boolean tryRelease(int unused) {
setExclusiveOwnerThread(null);
setState(0);
return true;
}
public void lock() { acquire(1); }
public boolean tryLock() { return tryAcquire(1); }
public void unlock() { release(1); }
public boolean isLocked() { return isHeldExclusively(); }
void interruptIfStarted() {
Thread t;
if (getState() >= 0 && (t = thread) != null && !t.isInterrupted()) {
try {
t.interrupt();
} catch (SecurityException ignore) {
}
}
}
}
addWorkerFailed
addWorker 方法中,如果添加 Worker 并且启动线程失败,则会做失败后的处理。 这个方法主要做两件事
- 如果 worker 已经构造好了,则从 workers 集合中移除这个 worker
- 原子递减核心线程数(因为在 addWorker 方法中先做了原子增加)
- 尝试结束线程池
private void addWorkerFailed(Worker w) {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
if (w != null)
workers.remove(w);
decrementWorkerCount();
tryTerminate();
} finally {
mainLock.unlock();
}
}
runWorker方法
前面已经了解了 ThreadPoolExecutor 的核心方法 addWorker,主要作用是增加工作线程, 而 Worker 简单理解其实就是一个线程,里面重写了 run 方法,这块是线程池中执行任务的 真正处理逻辑,也就是 runWorker 方法,这个方法主要做几件事
- 如果 task 不为空,则开始执行 task
- 如果 task 为空,则通过 getTask()再去取任务,并赋值给 task,如果取到的 Runnable 不为空,则 执行该任务
- 执行完毕后,通过 while 循环继续 getTask()取任务
- 如果 getTask()取到的任务依然是空,那么整个 runWorker()方法执行完毕
final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
Runnable task = w.firstTask;
w.firstTask = null;
/**
* unlock,表示当前 worker 线程允许中断,因为 new Worker 默认的 state=-1,此处是调用 Worker 类的 tryRelease()方法,将 state 置为 0,
* 而 interruptIfStarted()中只有 state>=0 才允许调用中断
*/
w.unlock(); // allow interrupts
boolean completedAbruptly = true;
try {
//注意这个 while 循环,在这里实现了 [线程复用] // 如果 task 为空,则通过 getTask 来获取任务
while (task != null || (task = getTask()) != null) {
w.lock();//上锁,不是为了防止并发执行任务,为了在shutdown()时不终止正在运行的 worker
// 线程池为 stop 状态时不接受新任务,不执行已经加入任务队列的任务,还中断正在执行的任务
// 所以对于 stop 状态以上是要中断线程的
// Thread.interrupted() &&runStateAtLeast(ctl.get(), STOP)确保线 程中断标志位为 true 且是 stop 状态以上,接着清除了中断标志
// !wt.isInterrupted()则再一次检查保证线程需要设置中断标志位
if ((runStateAtLeast(ctl.get(), STOP) ||
(Thread.interrupted() &&
runStateAtLeast(ctl.get(), STOP))) &&
!wt.isInterrupted())
wt.interrupt();
try {
beforeExecute(wt, task);//这里默认是没有实现的,在一些特定的场景中 我们可以自己继承 ThreadpoolExecutor 自己重写
Throwable thrown = null;
try {
task.run();//执行任务中的run方法
} catch (RuntimeException x) {
thrown = x; throw x;
} catch (Error x) {
thrown = x; throw x;
} catch (Throwable x) {
thrown = x; throw new Error(x);
} finally {
afterExecute(task, thrown);//这里默认默认而也是没有实现
}
} finally {
task = null;//置空任务(这样下次循环开始时,task 依然为 null,需要再通过 getTask()取) + 记录该 Worker 完成任务数量 + 解锁
w.completedTasks++;
w.unlock();
}
}
completedAbruptly = false;
} finally {
processWorkerExit(w, completedAbruptly);
//1.将入参 worker 从数组 workers 里删除掉;
//2.根据布尔值 allowCoreThreadTimeOut 来决定是否补充新的 Worker 进数组 workers
}
}
getTask
worker 线程会从阻塞队列中获取需要执行的任务,这个方法不是简单的 take 数据,我们来 分析下他的源码实现 你也许好奇是怎样判断线程有多久没有活动了,是不是以为线程池会启动一个监控线程,专 门监控哪个线程正在偷懒?想太多,其实只是在线程从工作队列 poll 任务时,加上了超时 限制,如果线程在 keepAliveTime 的时间内 poll 不到任务,那我就认为这条线程没事做, 可以干掉了,看看这个代码片段你就清楚了
private Runnable getTask() {
boolean timedOut = false; // Did the last poll() time out?
for (;;) {//自旋
int c = ctl.get();
int rs = runStateOf(c);
// Check if queue empty only if necessary.
/**
* 对线程池状态的判断,两种情况会 workerCount-1,并且返回 null
* 1. 线程池状态为 shutdown,且 workQueue 为空(反映了 shutdown 状态的线程池还是
* 要执行 workQueue 中剩余的任务的)
* 2. 线程池状态为 stop(shutdownNow()会导致变成 STOP)(此时不用考虑 workQueue
* 的情况)
*/
if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
decrementWorkerCount();
return null;//返回 null,则当前 worker 线程会退出
}
int wc = workerCountOf(c);
// timed变量用于判断是否需要进行超时控制。
// allowCoreThreadTimeOut默认是false,也就是核心线程不允许进行超时;
// wc > corePoolSize,表示当前线程池中的线程数量大于核心线程数量;
// 对于超过核心线程数量的这些线程,需要进行超时控制
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
/**
* 1. 线程数量超过maximumPoolSize可能是线程池在运行时被调用了setMaximumPoolSize() 被改变了大小,否则已经 addWorker()成功不会超过 maximumPoolSize
* 2. timed && timedOut 如果为 true,表示当前操作需要进行超时控制,并且上次从阻塞队列中 获取任务发生了超时.其实就是体现了空闲线程的存活时间
*/
if ((wc > maximumPoolSize || (timed && timedOut))
&& (wc > 1 || workQueue.isEmpty())) {
if (compareAndDecrementWorkerCount(c))
return null;
continue;
}
try {
/**
* 根据 timed 来判断,如果为 true,则通过阻塞队列 poll 方法进行超时控制,如果在 keepaliveTime 时间内没有获取到任务,则返回 null.
* 否则通过 take 方法阻塞式获取队列中的任务
*/
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take();
if (r != null)//如果拿到的任务不为空,则直接返回给worker进行处理
return r;
timedOut = true;
} catch (InterruptedException retry) {
timedOut = false;// 如果获取任务时当前线程发生了中断,则设置 timedOut 为
false 并返回循环重试
}
}
}
这里重要的地方是第二个 if 判断,目的是控制线程池的有效线程数量。由上文中的分析可以 知道,在执行 execute 方法时,如果当前线程池的线程数量超过了 corePoolSize 且小于 maximumPoolSize,并且 workQueue 已满时,则可以增加工作线程,但这时如果超时没有获取到任务,也就是 timedOut 为 true 的情况,说明 workQueue 已经为空了,也就说明了 当前线程池中不需要那么多线程来执行任务了,可以把多于 corePoolSize 数量的线程销毁掉,保持线程数量在 corePoolSize 即可。
什么时候会销毁?当然是 runWorker 方法执行完之后,也就是 Worker 中的 run 方法执行完,由 JVM 自动回收。
getTask 方法返回 null 时,在 runWorker 方法中会跳出 while 循环,然后会执行 processWorkerExit 方法。
processWorkerExit
runWorker 的 while 循环执行完毕以后,在 finally 中会调用 processWorkerExit,来销毁工作线 程。
到目前为止,我们已经从 execute 方法中输入了 worker 线程的创建到执行以及最后到销毁 的全部过程。那么我们继续回到 execute 方法.我们只分析完
addWorker 这段逻辑,继续来看后面的判断
execute 后续逻辑分析
如果核心线程数已满,说明这个时候不能再创建核心线程了,于是走第二个判断
第二个判断逻辑比较简单,如果线程池处于运行状态并且任务队列没有满,则将任务添加到
队列中
第三个判断,核心线程数满了,队列也满了,那么这个时候创建新的线程也就是(非核心线
程)
如果非核心线程数也达到了最大线程数大小,则直接拒绝任务
if (isRunning(c) && workQueue.offer(command)) {//2.核心池已满,但任务队列未满,添加到队列中
int recheck = ctl.get();
//任务成功添加到队列以后,再次检查是否需要添加新的线程,因为已存在的线程可能被销毁了
if (! isRunning(recheck) && remove(command))
reject(command);//如果线程池处于非运行状态,并且把当前的任务从任务队列中移除成功,则拒绝该任务
else if (workerCountOf(recheck) == 0)//如果之前的线程已被销毁完,新建一个线程
addWorker(null, false);
}
else if (!addWorker(command, false))//3.核心池已满,队列已满,试着创建一个新线程
reject(command);/如果创建新线程失败了,说明线程池被关闭或者线程池完全满了,拒绝任务
拒绝策略
1、AbortPolicy:直接抛出异常,默认策略;
2、CallerRunsPolicy:用调用者所在的线程来执行任务;
3、DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务;
4、DiscardPolicy:直接丢弃任务;
当然也可以根据应用场景实现 RejectedExecutionHandler 接口,自定义饱和策略,如记录日志或持久化存储不能处理的任务
如何合理配置线程池大小,也是很多同学反馈给我的问题,我也简单说一下。线程池大小不 是靠猜,也不是说越多越好。
在遇到这类问题时,先冷静下来分析
- 需要分析线程池执行的任务的特性: CPU 密集型还是 IO 密集型
- 每个任务执行的平均时长大概是多少,这个任务的执行时长可能还跟任务处理逻辑是否涉 及到网络传输以及底层系统资源依赖有关系
如何合理配置线程池的大小
如果是 CPU 密集型,主要是执行计算任务,响应时间很快,cpu 一直在运行,这种任务 cpu 的利用率很高,那么线程数的配置应该根据 CPU 核心数来决定,CPU 核心数=最大同时执行 线程数,加入 CPU 核心数为 4,那么服务器最多能同时执行 4 个线程。过多的线程会导致上 下文切换反而使得效率降低。那线程池的最大线程数可以配置为 cpu 核心数+1
如果是 IO 密集型,主要是进行 IO 操作,执行 IO 操作的时间较长,这是 cpu 出于空闲状态, 导致 cpu 的利用率不高,这种情况下可以增加线程池的大小。这种情况下可以结合线程的等 待时长来做判断,等待时间越高,那么线程数也相对越多。一般可以配置 cpu 核心数的 2 倍。
线程池设定最佳线程数目 = (线程池设定的线程等待时间+线程 CPU 时间)/ 线程 CPU 时间 )* CPU 数目
这个公式的线程 cpu 时间是预估的程序单个线程在 cpu 上运行的时间(通常使用 loadrunner 测试大量运行次数求出平均值)
线程池中的线程初始化
默认情况下,创建线程池之后,线程池中是没有线程的,需要提交任务之后才会创建线程。 在实际中如果需要线程池创建之后立即创建线程,可以通过以下两个方法办到: prestartCoreThread():初始化一个核心线程; prestartAllCoreThreads():初始化所有核心线程
线程池的关闭
ThreadPoolExecutor 提供了两个方法,用于线程池的关闭,分别是 shutdown()和 shutdownNow(),其中:shutdown():不会立即终止线程池,而是要等所有任务缓存队列中 的任务都执行完后才终止,但再也不会接受新的任务 shutdownNow():立即终止线程池,并 尝试打断正在执行的任务,并且清空任务缓存队列,返回尚未执行的任务
线程池容量的动态调整
ThreadPoolExecutor 提供了动态调整线程池容量大小的方法:setCorePoolSize()和 setMaximumPoolSize(),setCorePoolSize:设置核心池大小 setMaximumPoolSize:设置线 程池最大能创建的线程数目大小
任务缓存队列及排队策略
在前面我们多次提到了任务缓存队列,即 workQueue,它用来存放等待执行的任务。 workQueue 的类型为 BlockingQueue,通常可以取下面三种类型:
- ArrayBlockingQueue:基于数组的先进先出队列,此队列创建时必须指定大小,未读写分离,有界队列。
- LinkedBlockingQueue:基于链表的先进先出队列,如果创建时没有指定此队列大小,则默认为 Integer.MAX_VALUE,读写分离,无界队列。
- SynchronousQueue:这个队列比较特殊,它不会保存提交的任务,而是将直接新建一个线程来执行新来的任务。
线程池的监控
如果在项目中大规模的使用了线程池,那么必须要有一套监控体系,来指导当前线程池的状 态,当出现问题的时候可以快速定位到问题。而线程池提供了相应的扩展方法,我们通过重 写线程池的 beforeExecute、afterExecute 和 shutdown 等方式就可以实现对线程的监控,简 单给大家演示一个案例
public class Demo1 extends ThreadPoolExecutor {
ConcurrentHashMap startTimes;
// 保存任务开始执行的时间,当任务结束时,用任务结束时间减去开始时间计算任务执行时间 private ConcurrentHashMap startTimes;
public Demo1(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
this.startTimes = new ConcurrentHashMap<>();
}
@Override
public void shutdown() {
System.out.println("已经执行的任务数:" + this.getCompletedTaskCount() + ", " + "当前活动线程数:" + this.getActiveCount() + ", 当前排队线程数: " + this.getQueue().size());
System.out.println();
super.shutdown();
}
//任务开始之前记录任务开始时间
@Override
protected void beforeExecute(Thread t, Runnable r) {
startTimes.put(String.valueOf(r.hashCode()), new Date());
super.beforeExecute(t, r);
}
@Override
protected void afterExecute(Runnable r, Throwable t) {
Date startDate = (Date) startTimes.remove(String.valueOf(r.hashCode()));
Date finishDate = new Date();
long diff = finishDate.getTime() - startDate.getTime(); // 统计任务耗时、初始线程数、核心线程数、正在执行的任务数量、
// 已完成任务数量、任务总数、队列里缓存的任务数量、
// 池中存在的最大线程数、最大允许的线程数、线程空闲时间、线程池是否关闭、线程池 是否终止
System.out.print("任务耗时:" + diff + "\n");
System.out.print("初始线程数:" + this.getPoolSize() + "\n");
System.out.print("核心线程数:" + this.getCorePoolSize() + "\n");
System.out.print("正在执行的任务数量:" + this.getActiveCount() + "\n");
System.out.print("已经执行的任务数:" + this.getCompletedTaskCount() + "\n ");
System.out.print(" 任务总数:" + this.getTaskCount() + "\n ");
System.out.print(" 最大允许的线程数: " + this.getMaximumPoolSize() + "\n ");
System.out.print(" 线程空闲时间: " + this.getKeepAliveTime(TimeUnit.MILLISECONDS) + "\n ");
System.out.println();
super.afterExecute(r, t);
}
public static ExecutorService newCachedThreadPool() {
return new Demo1(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new
SynchronousQueue());
}
}
public class Test implements Runnable {
private static ExecutorService es = Demo1.newCachedThreadPool();
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws Exception {
for (int i = 0; i < 100; i++) {
es.execute(new Test());
}
es.shutdown();
}
}
线程池原理图示
Callable/Future 使用及原理分析
线程池的执行任务有两种方法,一种是 submit、一种是 execute; 这两个方法是有区别的,那么基于这个区别我们再来看看。
execute
- execute 只可以接收一个 Runnable 的参数
- execute 如果出现异常会抛出
- execute 没有返回值
submit
- submit 可以接收 Runable 和 Callable 这两种类型的参数,
- 对于 submit 方法,如果传入一个 Callable,可以得到一个 Future 的返回值
- submit 方法调用不会抛异常,除非调用 Future.get
这里,我们重点了解一下 Callable/Future,可能很多同学知道他是一个带返回值的线程,但 是具体的实现可能不清楚。
Callable/Future 案例演示
Callable/Future 和 Thread 之类的线程构建最大的区别在于,能够很方便的获取线程执行完 以后的结果。首先来看一个简单的例子
public class CallableDemo implements Callable {
@Override
public String call() throws Exception {
//Thread.sleep(3000);//阻塞案例演示
return "hello world";
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
CallableDemo callableDemo = new CallableDemo();
FutureTask futureTask = new FutureTask(callableDemo);
new Thread(futureTask).start();
System.out.println(futureTask.get());
}
}
想一想我们为什么需要使用回调呢?那是因为结果值是由另一线程计算的,当前线程是不知 道结果值什么时候计算完成,所以它传递一个回调接口给计算线程,当计算完成时,调用这 个回调接口,回传结果值。
这个在很多地方有用到,比如 Dubbo 的异步调用,比如消息中间件的异步通信等等...
利用 FutureTask、Callable、Thread 对耗时任务(如查询数据库)做预处理,在需要计算结 果之前就启动计算。
所以我们来看一下 Future/Callable 是如何实现的
Callable/Future 原理分析
刚刚实现的 demo 中,我们用到了两个 api,分别是 Callable 和 FutureTask。
Callable 是一个函数式接口,里面就只有一个 call 方法。子类可以重写这个方法,并且这个方法会有一个返回值
FutureTask实现了RunnableFuture,RunnableFuture继承Runnable和Future,也就是说FutureTask本身也是一个线程对象,那么我们看他的run方法。
public void run() {
if (state != NEW ||
!UNSAFE.compareAndSwapObject(this, runnerOffset,
null, Thread.currentThread()))
return;
try {
Callable c = callable;
if (c != null && state == NEW) {
V result;
boolean ran;
try {
result = c.call();//调用 callable 的 call 方法,并获得返回结果
ran = true;//运行成功
} catch (Throwable ex) {
result = null;
ran = false;
setException(ex);//设置异常结果,
}
if (ran)
set(result);//设置结果
}
} finally {
// runner must be non-null until state is settled to
// prevent concurrent calls to run()
runner = null;
// state must be re-read after nulling runner to prevent
// leaked interrupts
int s = state;
if (s >= INTERRUPTING)
handlePossibleCancellationInterrupt(s);
}
}
其实 run 方法作用非常简单,就是调用 callable 的 call 方法返回结果值 result,根据是否发生 异常,调用 set(result)或 setException(ex)方法表示 FutureTask 任务完结。
不过因为 FutureTask 任务都是在多线程环境中使用,所以要注意并发冲突问题。注意在 run 方法中,我们没有使用 synchronized 代码块或者 Lock 来解决并发问题,而是使用了 CAS 这 个乐观锁来实现并发安全,保证只有一个线程能运行 FutureTask 任务
get 方法
get 方法就是阻塞获取线程执行结果,这里主要做了两个事情
- 判断当前的状态,如果状态小于等于 COMPLETING,表示 FutureTask 任务还没有完结,
所以调用 awaitDone 方法,让当前线程等待。 - report 返回结果值或者抛出异常
public V get() throws InterruptedException, ExecutionException {
int s = state;
if (s <= COMPLETING)
s = awaitDone(false, 0L);
return report(s);
}
awaitDone
如果当前的结果还没有被执行完,把当前线程线程和插入到等待队列
private int awaitDone(boolean timed, long nanos)
throws InterruptedException {
final long deadline = timed ? System.nanoTime() + nanos : 0L;
WaitNode q = null;
boolean queued = false;
for (;;) {
if (Thread.interrupted()) {
removeWaiter(q);
throw new InterruptedException();
}
int s = state;
if (s > COMPLETING) {
if (q != null)
q.thread = null;
return s;
}
else if (s == COMPLETING) // cannot time out yet
Thread.yield();
else if (q == null)
q = new WaitNode();
else if (!queued)
queued = UNSAFE.compareAndSwapObject(this, waitersOffset,
q.next = waiters, q);
else if (timed) {
nanos = deadline - System.nanoTime();
if (nanos <= 0L) {
removeWaiter(q);
return state;
}
LockSupport.parkNanos(this, nanos);
}
else
LockSupport.park(this);
}
}
被阻塞的线程,会等到 run 方法执行结束之后被唤醒
report
report 方法就是根据传入的状态值 s,来决定是抛出异常,还是返回结果值。这个两种情况都 表示 FutureTask 完结了
private V report(int s) throws ExecutionException {
Object x = outcome;
if (s == NORMAL)
return (V)x;
if (s >= CANCELLED)
throw new CancellationException();
throw new ExecutionException((Throwable)x);
}
线程池对于 Future/Callable 的执行
我们现在再来看线程池里面的 submit 方法,就会很清楚了
public Future submit(Callable task) {
if (task == null) throw new NullPointerException();
RunnableFuture ftask = newTaskFor(task);
execute(ftask);
return ftask;
}