1、为什么用ThreadPoolExecutor创建线程池
虽然jdk中Executor框架提供了如newFixedThreadPool()、newSingleThreadExecutor()、newCachedThreadPool()等创建线程池的方法,但都有其局限性,不够灵活;另外由于前面几种方法内部也是通过ThreadPoolExecutor方式实现,使用ThreadPoolExecutor有助于大家明确线程池的运行规则,创建符合自己的业务场景需要的线程池,避免资源耗尽的风险。
如果大家开发中使用《Alibaba Java Coding Guidelines/阿里编码规约》插件或者查看过《阿里巴巴java开发手册》中指出了线程资源必须通过线程池提供,不允许在应用中自行显示的创建线程,这样一方面是线程的创建更加规范,可以合理控制开辟线程的数量;另一方面线程的细节管理交给线程池处理,优化了资源的开销。而线程池不允许使用Executors去创建,而要通过ThreadPoolExecutor方式创建
同时也建议大家在开发中安装《Alibaba Java Coding Guidelines/阿里编码规约》插件,能扫描我们在开发中代码的不足。
ThreadPoolExecutor是一个通过使用可能几个池线程之一来执行每个提交任务的ExecutorService,这些线程池通常通过Executors工厂方法进行配置。
ThreadPoolExecutor中的线程池处理了两个不同的问题:
- 由于减少了每个任务调用的开销,在执行大量的异步任务时它们通常提供改进的性能;
- 它们提供了边界和管理资源的一种手段,包括多线程,在执行任务集合时的消耗。
- 每个ThreadPoolExecutor还维护一些基本的统计数据,例如完成任务的数量。
2、ThreadPoolExecutor源码分析
-
知识点:
- 高几位,低几位;左边是高,右边是低;低几位,从右边开始数第几位,高几位:从左侧开始数
- <<:左移动几位,移动几位相当于乘以2的几次方,更多可以查看&、|、~、^、<<、>>、<<<、>>>说明
-
ThreadPoolExecutor类的层级关系
2.1、成员变量源码
/**
* AtomicInteger类型的ctl代表了ThreadPoolExecutor中的控制状态,
* 它是一个复核类型的成员变量,是一个原子整数,借助高低位包装了两个概念:
* 高3位保存:线程池的运行状态 (runState)
* 低29位保存:线程池内有效线程的数量 (workerCount),使用了Integer类型来保存,大约是5亿。
*/
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
/**
* workerCount:线程池中当前活动的线程数量,它占据ctl的低29位,
* 这样,每当活跃线程数增加或减少时,ctl直接做相应数目的增减即可,十分方便。
* 而ThreadPoolExecutor中COUNT_BITS就代表了workerCount所占位数
*
* Integer的二进制位数-3后的剩余位数才是线程的个数
* java int 32位-3等于29
*/
private static final int COUNT_BITS = Integer.SIZE - 3;
/**
* 线程最大个数:
*
* 线程最大个数上下限阈值,下限很明显就是0,ThreadPoolExecutor中CAPACITY就代表了
* workerCount的上限,它是ThreadPoolExecutor中理论上的最大活跃线程数
*
* 运算过程为1左移29位,也就是
* 00000000 00000000 00000000 00000001 --> 001 0000 00000000 00000000 00000000,
* 再减去1的话,就是 000 11111 11111111 11111111 11111111
* 前三位代表线程池运行状态runState,所以这里workerCount的理论最大值就应该是29个1,即536870911;
*
* 个人习惯:1左移29位减1=(1*2的29次方)-1约等于5亿
*/
private static final int CAPACITY = (1 << COUNT_BITS) - 1;
/**
* RUNNING:接受新任务,并处理队列任务
*
* -1在Java底层是由32个1表示的,左移29位的话,
* 即111 00000 00000000 00000000 00000000,
* 低29位全部为0,
* 高3位全部为1的话,
* 表示RUNNING状态,即-536870912;
*/
private static final int RUNNING = -1 << COUNT_BITS;
/**
* SHUTDOWN:不接受新任务,但会处理队列任务
*
* 0在Java底层是由32个0表示的,无论左移多少位,还是32个0,
* 即000 00000 00000000 00000000 00000000,
* 低29位全部为0,
* 高3位全部为0的话,
* 表示SHUTDOWN状态,即0
*/
private static final int SHUTDOWN = 0 << COUNT_BITS;
/**
* 不接受新任务,不会处理队列任务,而且会中断正在处理过程中的任务
*
* 1在Java底层是由前面的31个0和1个1组成的,左移29位的话,
* 即001 00000 00000000 00000000 00000000,
* 低29位全部为0,
* 高3位为001的话,
* 表示STOP状态,即536870912;
*/
private static final int STOP = 1 << COUNT_BITS;
/**
* 所有的任务已结束,workerCount为0,线程过渡到TIDYING状态,
* 将会执行terminated()钩子方法
*
* 2在Java底层是由前面的30个0和1个10组成的,左移29位的话,
* 即010 00000 00000000 00000000 00000000,
* 低29位全部为0,
* 高3位为010的话,
* 表示TIDYING状态,即1073741824;
*/
private static final int TIDYING = 2 << COUNT_BITS;
/**
* terminated()方法已经完成
*
* 2在Java底层是由前面的30个0和1个11组成的,左移29位的话,
* 即011 00000 00000000 00000000 00000000,
* 低29位全部为0,
* 高3位为011的话,
* 表示TERMINATED状态,即1610612736;
*/
private static final int TERMINATED = 3 << COUNT_BITS;
/**
* ~是按位取反的意思,
* CAPACITY表示的是高位的3个0,和低位的29个1,
* 而~CAPACITY则表示高位的3个1,2低位的9个0,
* 然后再与入参c执行按位与操作,即高3位保持原样,低29位全部设置为0,
* 也就获取了线程池的运行状态runState。
*/
private static int runStateOf(int c) { return c & ~CAPACITY; }
/**
* 传入的c代表的是ctl的值,即高3位为线程池运行状态runState,
* 低29位为线程池中当前活动的线程数量workerCount,
* 将其与CAPACITY进行与操作&,运算规则:0&0=0; 0&1=0; 1&0=0; 1&1=1;
* 也就是与000 11111 11111111 11111111 11111111进行与操作,
* c的前三位通过与000进行与操作,无论c前三位为何值,最终都会变成000,也就是舍弃前三位的值,
* 而c的低29位与29个1进行与操作,c的低29位还是会保持原值,
* 这样就从AtomicInteger ctl中解析出了workerCount的值。
*/
private static int workerCountOf(int c) { return c & CAPACITY; }
/**
* 原子变量ctl的初始化方法
* 传入的rs表示线程池运行状态runState,其是高3位有值,低29位全部为0的int,
* 而wc则代表线程池中有效线程的数量workerCount,其为高3位全部为0,而低29位有值的int,
* 将runState和workerCount做或操作|处理。运算规则:0|0=0; 0|1=1; 1|0=1; 1|1=1;
* 即用runState的高3位,workerCount的低29位填充的数字,
* 而默认传入的runState、workerCount分别为RUNNING和0
* 即:111 00000 00000000 00000000 00000000
*/
private static int ctlOf(int rs, int wc) { return rs | wc; }
生命周期
线程池的完整生命周期具有如下五个阶段:
- RUNNING:这是线程池的初始状态。此状态下线程池会接受新任务并且处理队列中等待的任务。
- SHUTDOWN:RUNNING状态下调用shutdown方法后进入此状态。此状态下线程池不接受新任务,但会处理队列中等待的任务。
- STOP:RUNNING/SHUTDOWN状态下调用shutdownNow方法后进入此状态。此状态下线程池不接受新任务,也不处理既有等待任务,并且会中断既有运行中的线程。
- TIDYING:SHUTDOWN/STOP状态会流转到此状态。此时所有任务都已运行完毕,工作线程数为0,任务队列都为空。从字面角度理解,此时线程池已经清干净了。
- TERMINATED:TIDYING状态下,线程池执行完terminated钩子方法后进入此状态,此时线程池已完全终止。
由上面我们可以得知,运行状态的值按照
RUNNING-->SHUTDOWN-->STOP-->TIDYING-->TERMINATED
顺序值是递增的,这些值之间的数值顺序很重要。随着时间的推移,运行状态单调增加,但是不需要经过每个状态。
可能存在的线程池状态的转换如下:
- RUNNING -> SHUTDOWN:调用shutdown()方法后,或者线程池实现了finalize方法,在里面调用了shutdown方法,即隐式调用;
- (RUNNING or SHUTDOWN) -> STOP:调用shutdownNow()方法后;
- SHUTDOWN -> TIDYING:线程池和队列均为空时;
- STOP -> TIDYING:线程池为空时;
- TIDYING -> TERMINATED:terminated()钩子方法完成时。
/**
* CAS方式递增ctl的workerCount字段。
*/
private boolean compareAndIncrementWorkerCount(int expect) {
return ctl.compareAndSet(expect, expect + 1);
}
/**
* CAS方式递减ctl的workerCount字段。
*/
private boolean compareAndDecrementWorkerCount(int expect) {
return ctl.compareAndSet(expect, expect - 1);
}
/**
* 递减ctl的workerCount字段。
* 这仅在线程突然终止时调用(请参阅processWorkerExit)。其他减量在getTask中执行。
*/
private void decrementWorkerCount() {
do {} while (! compareAndDecrementWorkerCount(ctl.get()));
}
/**
* 用于持有任务并将其转换成工作线程worker的队列;
*/
private final BlockingQueue<Runnable> workQueue;
/**
* 包含线程池中所有工作线程worker的集合,仅仅当拥有mainLock锁时才能访问它;
*
* 线程池中用于保存工作线程的是一个HashSet,还有一些统计的字段比如largestPoolSize
* 用于统计线程池中出现过的最大线程数,completedTaskCount用于统计完成的任务数。
* 这些东西的更新与读取都会被mainLock保护。这里很容易有个问题,
* 为什么不用并发容器来保存工作线程?
* Doug Lea在源码的doc里的描述大意是:用锁可以串行化interruptIdleWorkers方法,
* 避免关闭线程池时大量线程并发中断其他线程。另外在shutdown/shutdownNow时由于需要
* 遍历工作线程集合来检查权限,在检查完权限后会中断工作线程。加上锁也可以保证在检查
* 权限与中断线程过程中,工作线程集合元素不变。
*/
private final ReentrantLock mainLock = new ReentrantLock();
/**
* 用来保存当前线程池中的所有线程;
* 可通过该集合对线程池中的线程进行中断, 遍历等;
* 创建新的线程时, 要添加到该集合,
* 移除线程, 也要从该集合中移除对应的线程;
* 对该集合操作都需要mainLock锁.
*/
private final HashSet<Worker> workers = new HashSet<Worker>();
/**
* 当线程池中的状态表示的值小于TERMINATED的值时,
* 当前调用了awaitTermination方法的线程就会wait对应的时间;
* 等到过了指定的wait时间, 或者线程池状态等于或大于TERMINATED,
* wait的线程被唤醒, 就继续执行;
*/
private final Condition termination = mainLock.newCondition();
/**
* 用于统计线程池中出现过的最大线程数
*/
private int largestPoolSize;
/**
* 已完成任务的计数器,只有在worker线程的终止,仅仅当拥有mainLock锁时才能访问它;
*/
private long completedTaskCount;
/**
* 新线程的工厂类;
*/
private volatile ThreadFactory threadFactory;
/**
* 执行过程中shutdown时调用的handler;
*/
private volatile RejectedExecutionHandler handler;
/**
* 空闲线程等待工作的超时时间(纳秒),即空闲线程存活时间;
*/
private volatile long keepAliveTime;
/**
* 默认值为false,如果为false,core线程在空闲时依然存活;
* 如果为true,则core线程等待工作,直到时间超时至keepAliveTime;
*/
private volatile boolean allowCoreThreadTimeOut;
/**
* 核心线程池大小,保持存活的工作线程的最小数目,
* 当小于corePoolSize时,会直接启动新的一个线程来处理任务,
* 而不管线程池中是否有空闲线程;
*/
private volatile int corePoolSize;
/**
* 线程池最大大小,也就是线程池中线程的最大数量。
*/
private volatile int maximumPoolSize;
2.2、构造方法源码
/**
* corePoolSize:指定了线程池中的核心线程数量,它的数量决定了添加的任务是开辟新的线程去执行,
* 还是放到workQueue任务队列中去;
* maximumPoolSize:指定了线程池中的最大线程数量,这个参数会根据你使用的workQueue
* 任务队列的类型,决定线程池会开辟的最大线程数量;
* keepAliveTime:当线程池中空闲线程数量超过corePoolSize时,
* 多余的线程会在多长时间内被销毁;
* unit:keepAliveTime的单位
* workQueue:任务队列,被添加到线程池中,但尚未被执行的任务;
* 它一般分为直接提交队列、有界任务队列、无界任务队列、优先任务队列几种;
* threadFactory:线程工厂,用于创建线程,一般用默认即可;
* handler:拒绝策略;当任务太多来不及处理时,如何拒绝任务;
*/
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.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;
}
2.3、内部类Worker类
线程池中的每一个线程被封装成一个Worker对象,ThreadPool维护的其实就是一组 Worker对象,请参见JDK源码。 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这样的线程池控制方法时重新获取锁。如果使用ReentrantLock,它是可重入的,这样如果在任务中调用了如setCorePoolSize这类线程池控制的方法,会中断正在运行的线程。
Worker继承自AQS,用于判断线程是否空闲以及是否可以被中断。 此外,在构造方法中执行了setState(-1);,把state变量设置为-1,为什么这么做呢? 是因为AQS中默认的state是0,如果刚创建了一个Worker对象,还没有执行任务时,这时就不应该被中断,看一下tryAquire方法:
- tryAcquire方法是根据state是否是0来判断的,所以,setState(-1);将state设置为-1是
为了禁止在执行任务前对线程进行中断。
正因为如此,在runWorker方法中会先调用Worker对象的unlock方法将state设置为0。
private final class Worker
extends AbstractQueuedSynchronizer
implements Runnable
{
/**
* 这个类永远不会被序列化,
* 但是我们提供了一个serialVersionUID来抑制javac警告。
*/
private static final long serialVersionUID = 6138294804551838833L;
/**
* 保存传入的任务
*/
final Thread thread;
/**
* 通过ThreadFactory来创建的线程,是用来处理任务的线程。
*/
Runnable firstTask;
/** Per-thread task counter */
volatile long completedTasks;
/**
* 这里通过getThreadFactory().newThread(this);
* 来新建一个线程,newThread方法传入的参数是this,
* 因为Worker本身继承了Runnable接口,也就是一个线程,
* 所以一个Worker对象在启动的时候会调用Worker类中的run方法。
*/
Worker(Runnable firstTask) {
// 因为AQS中默认的state是0,如果刚创建了一个Worker对象,
// 还没有执行任务时,这时就不应该被中断
// tryAcquire方法是根据state是否是0来判断的,
// 所以,setState(-1);是为了禁止在执行任务前对线程进行中断。
setState(-1);
this.firstTask = firstTask;
this.thread = getThreadFactory().newThread(this);
}
/**
* 运行线程,调用runWorker
*/
public void run() {
runWorker(this);
}
// Lock methods
// 0 :解锁状态.
// 1 :锁定状态
protected boolean isHeldExclusively() {
return getState() != 0;
}
protected boolean tryAcquire(int unused) {
// cas修改state,不可重入
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) {
}
}
}
}
Worker类继承AQS实现了一个简单不可重入的互斥锁,在执行用户提交任务的开始时需要获取锁,任务结束时需要释放锁。锁在这里最主要的目的是为了保证被别的线程中断时处于空闲状态,即没有在执行任务。当然如果shutdownNow方法被调用时,所有的线程都会被中断不管是否处于空闲状态。
很自然会想到为什么不能复用ReentrantLock组合在里面呢?
实际上这里不能用ReentrantLock,因为不能允许工作线程能够多次获取锁。否则提交的任务通过调用ThreadPoolExecutor#setCorePoolSize -> interruptIdleWorkers 会把任务本身对应的工作线程给中断掉,因为工作线程可以通过tryLock方法重入了锁。
2.4、核心方法
2.4.1、execute方法
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
/*
* 1、如果少于corePoolSize数量的线程正在运行,尝试利用给定的Runnable实例
* command开启一个新的线程作为它的第一个任务。addWorker()方法的调用会对线
* 程池运行状态runState、worker线程数量workerCount进行原子性检测,
* 返回值为启动新线程结果。
*
* 2、如果一个任务可以成功地进入队列,然后我们还需要再次检查
* (即双份检查)自从进入这个方法后,我们是否应该添加一个线程
* (因为自从上一次检查以来可能存在死亡情况),所以我们重新检查状态,
* 如果有必要的话,即线程池已停止,回滚之前的入队操作,或者在没有线程时启动一个新线程。
*
* 3、如果我们不能入列一个任务,那么我们尝试添加一个新线程。如果添加失败,
* 我们知道线程池可能已被关闭或者数量饱和,所以我们会拒绝这个任务。
*/
// 获取ctl的值c
int c = ctl.get();
// 如果c中有效线程数目小于corePoolSize大小,尝试添加新的worker线程处理任务command
// workerCountOf()方法:从c中获取有效线程数
// 线程数的判断利用corePoolSize作为边界约束条件
if (workerCountOf(c) < corePoolSize) {
// 添加新的worker线程处理任务,ture表示成功,false表示失败
// addWorker(command, false):如果为true,根据corePoolSize来判断;
// 如果为false,则根据maximumPoolSize来判断
if (addWorker(command, true))
return;
// 添加work线程失败则再次获取ctl的值
c = ctl.get();
}
// isRunning(c):根据c判断当前线程池的状态是否为RUNNING状态,
// 即既可以接受新任务,又会处理队列任务的状态。
// offer()方法:尝试将commond添加到队列workQueue中
// BlockingQueue的offer()方法表示如果可能的话,
// 将参数对象加到BlockingQueue里,
// 即如果BlockingQueue可以容纳,则返回true,否则返回false
if (isRunning(c) && workQueue.offer(command)) {
// 如果当前线程池处于RUNNING状态,且workQueue能够容纳command,并添加成功的话,
// 再次获取ctl的值recheck,
int recheck = ctl.get();
/*
* 为什么还需要再次做线程池状态检查呢?
* 因为在我们刚刚把任务成功添加到任务队列workQueue中的那一刻,
* 或者前一刻,任务还是RUNNING状态,但是保不齐其他调用者或者
* 线程此时会修改线程池状态,那么此时我们就需要将任务再进行必要的移除,
* 这是考虑复杂情况的一种安全机制的保障!
*/
// 如果当前线程池的状态不是RUNNING,并且从队列workQueue移除command成功的话,
// 调用reject()
if (! isRunning(recheck) && remove(command))
// 拒绝任务command
reject(command);
// 否则如果当前工作线程woker数目为0,尝试添加新的worker线程,但是不携带任务
else if (workerCountOf(recheck) == 0)
/*
* addWorker方法这里传入的参数表示:
* 1. 第一个参数为null,表示在线程池中创建一个线程,但不去启动;
* 2. 第二个参数为false,将线程池的有限线程数量的上限设置为
* maximumPoolSize,添加线程时根据maximumPoolSize来判断;
* 如果判断workerCount大于0,则直接返回,在workQueue中新增的
* command会在将来的某个时刻被执行。
*/
addWorker(null, false);
}
/*
* 如果执行到这里,有两种情况:
* 1. 线程池已经不是RUNNING状态;
* 2. 线程池是RUNNING状态,但workerCount >= corePoolSize并且workQueue已满。
* 这时,再次调用addWorker方法,但第二个参数传入为false,
* 将线程池的有限线程数量的上限设置为maximumPoolSize;
* 如果失败则拒绝该任务
*/
else if (!addWorker(command, false))
reject(command);
}
execute执行流程
结论:
- 如果workerCount < corePoolSize,则创建并启动一个线程来执行新提交的任务;
- 如果workerCount >= corePoolSize,且线程池内的阻塞队列未满,则将任务添加到该阻塞队列中;
- 如果 workerCount >= corePoolSize && workerCount < maximumPoolSize,且线程池内的阻塞队列已满,则创建并启动一个线程来执行新 提交的任务;
- 如果workerCount >= maximumPoolSize,并且线程池内的阻塞队列已满,则根据拒绝策略来处理该任务, 默认的处理方式是直接抛异常。
2.4.2、addWorker方法
addWorker方法的主要工作是在线程池中创建一个新的线程并执行
- firstTask:用于指定新增的线程执行的第一个任务,
- core:
- true表示在新增线程时会判断当前活动线程数是否小于corePoolSize
- false表示新增线程前需要判断当前活动线程数是否小于 maximumPoolSize
retry就是一个标记,标记对一个循环方法的操作(continue和break)处理点,功能类似于C/C++中goto,所以retry一般都是伴随着for循环出现,retry:标记的下一行就是for循环,在for循环里面调用continue(或者break)再紧接着retry标记时,就表示从这个地方开始执行continue(或者break)操作
通俗讲就是continue abc;相当于break,testOtherFlag方法与testBreakR方法等价关系,相当于少写了一些判断,代码更整洁,但是不是很通俗易懂,需要知识积累
标记示例
public static void main(String[] args) {
abc:
for(int i = 0; i < 3; i++) {
for(int j = 0; j < 5; j++) {
System.out.print(j + ", ");
if(j == 3) {
// 相当于 break
continue abc;
}
}
}
}
public static void testOtherFlag() {
abc:
for(int i = 0; i < 3; i++) {
for(int j = 0; j < 5; j++) {
System.out.print(j + ", ");
if(j == 3) {
break abc;
}
}
}
System.out.println(" >>> OK");
}
public static void testBreakR() {
boolean flag = false;
for(int i = 0; i < 3; i++) {
for(int j = 0; j < 5; j++) {
System.out.print(j + ", ");
if(j == 3) {
flag = true;
break;
}
}
if(flag) {
break;
}
}
System.out.println(" >>> OK");
}
源码开始:
private boolean addWorker(Runnable firstTask, boolean core) {
// 标记位
retry:
// 自旋
for (;;) {
int c = ctl.get();
// 获取运行状态
int rs = runStateOf(c);
/*
* 如果rs >= SHUTDOWN(非运行状态),则表示此时不再接收新任务;
* 接着判断以下3个条件,只要有1个不满足,则返回false:
* 1. rs == SHUTDOWN,这时表示关闭状态,不再接受新提交的任务,
* 但可以继续处理阻塞队列中已保存的任务
* 2. firsTask为空
* 3. 阻塞队列不为空
*
* 首先考虑rs == SHUTDOWN的情况
* 这种情况下不会接受新提交的任务,所以在firstTask不为空的时候会返回 false;
* 然后,如果firstTask为空,并且workQueue也为空,则返回false,
* 因为队列中已经没有任务了,不需要再添加线程了
*/
if (rs >= SHUTDOWN &&
! (rs == SHUTDOWN &&
firstTask == null &&
! workQueue.isEmpty()))
return false;
//自旋
for (;;) {
// 获得 Worker 工作线程数
int wc = workerCountOf(c);
// 如果工作线程数大于默认容量大小或者大于核心线程数大小,
// 则直接返回 false 表示不能再添加 worker。
if (wc >= CAPACITY ||
wc >= (core ? corePoolSize : maximumPoolSize))
return false;
// 通过 cas 来增加工作线程数
// 成功则跳出整个循环,执行下面流程
// 失败,则直接继续流程
if (compareAndIncrementWorkerCount(c))
break retry;
// 再次获取 ctl 的值
c = ctl.get();
// 如果当前的运行状态不等于rs,说明状态已被改变,返回第一个 for循环继续执行
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 {
// 根据firstTask来创建Worker对象 w = new Worker(firstTask);
// 每一个Worker对象都会根据ThreadFactory创建一个线程
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状态;
* 如果rs是RUNNING状态或者rs是SHUTDOWN状态
* 并且firstTask为null,向线程池中添加线程。
* 因为在SHUTDOWN时不会在添加新的任务,但还是会执行 workQueue中的任务
*/
if (rs < SHUTDOWN ||
(rs == SHUTDOWN && firstTask == null)) {
// 任务刚封装到 work 里面,还没 start,封装的线程是alive直接抛异常
if (t.isAlive()) // precheck that t is startable
throw new IllegalThreadStateException();
//将新创建的Worker添加到workers集合中,workers是一个HashSet
workers.add(w);
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;
}
2.4.3、runWorker方法
在Worker类中的run方法调用了runWorker方法来执行任务,也就是addWorker中线程启动执行的方法t.start();
final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
// 获取第一个任务
Runnable task = w.firstTask;
w.firstTask = null;
/*
* 获取互斥锁。
* 在持有互斥锁时,调用线程池shutdown方法不会中断该线程。
* 但是shutdownNow方法无视互斥锁,会中断所有线程。
*/
w.unlock();
// 是否因为异常退出循环
boolean completedAbruptly = true;
try {
// 如果task为空,则通过getTask来获取任务, getTask()方法从阻塞队列中取任务;
while (task != null || (task = getTask()) != null) {
w.lock();
/*
* 这里if做的事情就是判断是否需要中断当前线程。
* 如果线程池至少处于STOP阶段,当前线程未中断,则中断当前线程;
* 否则清除线程中断位。
*
* if条件中的Thread.interrupted() &&runStateAtLeast(ctl.get(), STOP)
* 做的事情说穿了就是清除中断位并确认目前线程池状态没有达到STOP阶段。
*/
if ((runStateAtLeast(ctl.get(), STOP) ||
(Thread.interrupted() &&
runStateAtLeast(ctl.get(), STOP))) &&
!wt.isInterrupted())
wt.interrupt();
try {
// 调用由子类实现的前置处理钩子。
beforeExecute(wt, task);
Throwable thrown = null;
try {
// 真正的执行任务
task.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, 计数器+1, 释放互斥锁。
task = null;
w.completedTasks++;
w.unlock();
}
}
// 表示执行任务过程中是否出现过异常
completedAbruptly = false;
} finally {
/*
* 处理工作线程退出。
* 上面主循环中的前置处理、任务调用、后置处理都是可能会抛出异常的。
*/
processWorkerExit(w, completedAbruptly);
}
}
2.4.4、getTask方法用来从阻塞队列中取任务
/**
* 工作线程从任务队列中拿取任务的核心方法。
* 根据配置决定采用阻塞或是时限获取。
* 在以下几种情况会返回null从而接下来线程会退出(runWorker方法循环结束):
* 1. 当前工作线程数超过了maximumPoolSize(由于maximumPoolSize可以动态调整,这是可能的)。
* 2. 线程池状态为STOP (因为STOP状态不处理任务队列中的任务了)。
* 3. 线程池状态为SHUTDOWN,任务队列为空 (因为SHUTDOWN状态仍然需要处理等待中任务)。
* 4. 根据线程池参数状态以及线程是否空闲超过keepAliveTime决定是否退出当前工作线程。
*/
private Runnable getTask() {
// 上次从任务队列poll任务是否超时。
boolean timedOut = false;
// 自旋
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);
/**
* 如果线程池状态rs >= SHUTDOWN,也就是非RUNNING状态,再进行以下判断:
* 1. rs >= STOP,线程池是否正在stop;
* 2. 阻塞队列是否为空。
* 如果以上条件满足,则将workerCount减1并返回null。
* 因为如果当前线程池状态的值是SHUTDOWN或以上时,不允许再向阻塞队列中添加任务。
*/
if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
decrementWorkerCount();
return null;
}
int wc = workerCountOf(c);
/*
* timed变量用于判断是否需要进行超时控制。
* allowCoreThreadTimeOut默认是false,也就是核心线程不允许进行超时;
* 当前工作线程受keepAliveTime影响。
* wc > corePoolSize,表示当前线程池中的线程数量大于核心线程数量;
* 对于超过核心线程数量的这些线程,需要进行超时控制
*/
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
/*
* wc > maximumPoolSize的情况是因为可能在此方法执行阶段,同时执行了
* setMaximumPoolSize方法;
* timed && timedOut 如果为true,表示当前操作需要进行超时控制,
* 并且上次从阻塞队列中获取任务发生了超时
*
* 接下来判断,如果有效线程数量大于1,或者阻塞队列是空的,
* 那么尝试将 workerCount减1;
* 如果减1失败,则返回重试。
* 如果wc == 1时,也就说明当前线程是线程池中唯一的一个线程了。
*
* 换句话说就是,如果队列不为空,则当前线程不能是最后一个工作线程,
* 否则退出了就没线程处理任务了。
*/
if ((wc > maximumPoolSize || (timed && timedOut))
&& (wc > 1 || workQueue.isEmpty())) {
// 设置ctl的workCount减1, CAS失败则需要重试(因为上面if中的条件可能不满足了)。
if (compareAndDecrementWorkerCount(c))
return null;
continue;
}
try {
/**
* 根据timed来判断,如果为true,则通过阻塞队列的poll方法进行超时控制,
* 如果在keepAliveTime时间内没有获取到任务,则返回null;
* 否则通过take方法,如果这时队列为空,则take方法会阻塞直到队列不为空。
*/
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take();
if (r != null)
return r;
// workQueue.take是不会返回null的,因此说明poll超时了,timedOut设置为true
timedOut = true;
} catch (InterruptedException retry) {
// 如果获取任务时当前线程发生了中断,则设置timedOut为false并返 回循环重试
timedOut = false;
}
}
}
此判断
if ((wc > maximumPoolSize || (timed && timedOut)) && (wc > 1 || workQueue.isEmpty())) {
目的是控制线程池的有效线程数量。由上文中的分析可以知道,在执行execute方法时,如果当前线程池的线程数量超过了corePoolSize且小于 maximumPoolSize,并且workQueue已满时,则可以增加工作线程;
但这时如果超时没有获取到任务,也就是timedOut为true的情况,说明workQueue已经为空了,也就说明了当前线程池中不需要那么多线程来执行任务了,可以把多于corePoolSize数量的线程销毁掉,保持线程数量在corePoolSize即可。
什么时候会销毁?
- 当然是runWorker方法执行完之后,也就是Worker中的run方法执行完,由JVM自动回收。
getTask方法返回null时,在runWorker方法中会跳出while循环,然后会执行processWorkerExit方法。
2.4.5、processWorkerExit
private void processWorkerExit(Worker w, boolean completedAbruptly) {
/*
* 如果completedAbruptly值为true,则说明线程执行时出现了异常,
* 需要将 workerCount减1;
* 如果线程执行时没有出现异常,说明在getTask()方法中已经已经对
* workerCount进行了减1操作,这里就不必再减了。
*/
if (completedAbruptly)
decrementWorkerCount();
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
// 累加completedTaskCount,统计完成的任务数
completedTaskCount += w.completedTasks;
// 从workers中移除,也就表示着从线程池中移除了一个工作线程
workers.remove(w);
} finally {
mainLock.unlock();
}
// 根据线程池状态进行判断是否结束线程池
tryTerminate();
int c = ctl.get();
/**
* 当线程池是RUNNING或SHUTDOWN状态时,如果worker是异常结束,那么会直接 addWorker;
* 如果allowCoreThreadTimeOut=true,并且等待队列有任务,至少保留一个 worker;
* 如果allowCoreThreadTimeOut=false,workerCount不少于corePoolSize。
*/
if (runStateLessThan(c, STOP)) {
if (!completedAbruptly) {
/*
* 确定在RUNNING或SHUTDOWN状态下最少需要的工作线程数。
*
* 默认情况下,核心线程不受限制时影响,
* 在这种情况下核心线程数量应当是稳定的。
* 否则允许线程池中无线程。
*/
int min = allowCoreThreadTimeOut ? 0 : corePoolSize;
// 如果任务队列非空,至少需要1个工作线程。
if (min == 0 && ! workQueue.isEmpty())
min = 1;
// 无需补偿工作线程。
if (workerCountOf(c) >= min)
return;
}
// 异常退出或者需要补偿一个线程的情况下,加一个空任务工作线程。
addWorker(null, false);
}
}
线程池流程图
2.4.6、tryTerminate
线程池的生命周期最终态为TERMINATED,然而TERMINATED状态的演进未必是调用shutdown/shutdownNow能做到,因为TERMINATED状态下,线程池中已经没有工作线程,也没有任务队列。tryTerminate方法里面包含了一个逻辑上的责任链,将线程池状态的演进动作在线程中传播下去。
它是实现线程池状态从SHUTDOWN或者STOP流转到TIDYING->TERMINATED的桥梁方法。
final void tryTerminate() {
for (;;) {
int c = ctl.get();
/*
* 能够进行状态流转的情况是:
* 1. STOP状态
* 2. SHUTDOWN并且任务队列已空。
*/
if (isRunning(c) ||
runStateAtLeast(c, TIDYING) ||
(runStateOf(c) == SHUTDOWN && ! workQueue.isEmpty()))
return;
/*
* 这时只需要所有工作线程退出即可终止线程池。
* 如果仍然有工作线程,则中断一个空闲的线程。
*
* 一旦空闲线程被终止,则会进入processWorkerExit方法,
* 在processWorkerExit方法中即将退出的工作线程会调用tryTerminate,
* 从而将终止线程池的动作通过这样的机制在线程间传播下去。
*/
if (workerCountOf(c) != 0) {
interruptIdleWorkers(ONLY_ONE);
return;
}
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
// 这时workerCount已经为0,任务队列也已为空,状态流转到TIDYING。
if (ctl.compareAndSet(c, ctlOf(TIDYING, 0))) {
try {
// 调用terminated()钩子方法。
terminated();
} finally {
// 将线程池状态拨到TERMINATED。
ctl.set(ctlOf(TERMINATED, 0));
// 唤醒所有在线程池终止条件上等待的线程。
termination.signalAll();
}
return;
}
} finally {
mainLock.unlock();
}
// 线程池状态流转CAS失败的话重试循环。
}
}
2.4.7、interruptIdleWorkers
Worker类是线程池对工作线程的封装抽象。它主要做的事情就是不断从任务队列中取任务执行,遇到异常退出。如果在工作线程等待任务(阻塞在阻塞队列)时中断该工作线程,则工作线程会重试一次getTask的循环来获取任务,获取不到就会退出runWorker方法的大循环,从而进入processWorkerExit方法收尾。ThreadPoolExecutor中线程中断的主要方法便是interruptIdleWorkers。可以通过参数控制是否最多中断1个线程。
/**
* 参数中的onlyOne表示至多只中断一个工作线程。
* 在tryTerminate方法读取到目前线程池离可以进入终止状态只剩下workCount降为0时,
* 会调用interruptIdeleWorkers(true)。因为有可能此时其他所有线程都阻塞在任务队列上,
* 只要中断任意一个线程,通过processWorkerExit -> tryTerminate ->interruptIdleWorkers,
* 可以使线程中断+退出传播下去。
*/
private void interruptIdleWorkers(boolean onlyOne) {
final ReentrantLock mainLock = this.mainLock;
/*
* 这里加全局锁的一个很重要的目的是使这个方法串行化执行。
* 否则在线程池关闭阶段,退出的线程会通过tryTerminate调用到此方法,
* 并发尝试中断还未中断的线程,引发一场中断风暴。
*/
mainLock.lock();
try {
for (Worker w : workers) {
Thread t = w.thread;
// 工作线程在处理任务阶段是被互斥锁保护的,从而这里不会中断到。
if (!t.isInterrupted() && w.tryLock()) {
try {
t.interrupt();
} catch (SecurityException ignore) {
} finally {
w.unlock();
}
}
// 最多中断一个。
if (onlyOne)
break;
}
} finally {
mainLock.unlock();
}
}
2.4.8、shutdown
shutdown方法关闭线程池是有序优雅的,线程池进入SHUTDOWN状态后不会接受新任务,但是任务队列中已有的任务会继续处理。
shutdown方法会中断所有未处理任务的空闲线程。
public void shutdown() {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
checkShutdownAccess();
// 状态切换到SHUTDOWN。
advanceRunState(SHUTDOWN);
// 中断所有空闲线程,或者说在任务队列上阻塞的线程。
interruptIdleWorkers();
onShutdown();
} finally {
mainLock.unlock();
}
// 尝试终止线程池(状态流转至TERMINATED)。
tryTerminate();
}
private void interruptIdleWorkers() {
interruptIdleWorkers(false);
}
2.4.9、shutdownNow
shutdownNow方法关闭线程池相比shutdown就暴力了一点,会中断所有线程,哪怕线程正在执行任务。
线程池进入STOP状态后,不接受新的任务,也不会处理任务队列中已有的任务。
但需要注意的是,即便shutdownNow即便会中断正在执行任务的线程,不代表你的任务一定会挂:如果提交的任务里面的代码没有对线程中断敏感的逻辑的话,线程中断也不会发生什么。
public List<Runnable> shutdownNow() {
List<Runnable> tasks;
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
checkShutdownAccess();
// 状态切换到STOP
advanceRunState(STOP);
// 与SHUTDOWN不同的是,直接中断所有线程。
interruptWorkers();
// 将任务队列中的任务收集到tasks。
tasks = drainQueue();
} finally {
mainLock.unlock();
}
// 尝试终止线程池(状态流转至TERMINATED)。
tryTerminate();
return tasks;
}
/**
* 此方法只会被shutdownNow方法调用,用于中断所有工作线程。
*/
private void interruptWorkers() {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
for (Worker w : workers)
w.interruptIfStarted();
} finally {
mainLock.unlock();
}
}
void interruptIfStarted() {
Thread t;
/*
* 这里中断已经执行过初次unlock的工作线程(参考runWorker方法逻辑),
* 因为如果还没有走到初次unlcok那一步的工作线程,一定会读到线程池状态至少为STOP从而退出。
*/
if (getState() >= 0 && (t = thread) != null && !t.isInterrupted()) {
try {
t.interrupt();
} catch (SecurityException ignore) {
}
}
}
/**
* 将任务队列中的任务dump出来。
* 这个方法执行完以后,任务队列其实可能还会有残留的任务。
* 比方说:我们的任务队列用LinkedBlockingQueue,事件顺序如下:
* 时刻1: 线程池状态为RUNNGING,线程A执行ThreadPoolExecutor#execute方法的
* if (isRunning(c) && workQueue.offer(command))
* isRunning(c)返回true,此时还未执行offer操作。
* 时刻2: 线程B执行shutdownNow,切换线程池状态到STOP,接下来执行完drainQueue方法。
* 时刻3: 线程A开始执行offer操作,往任务队列中添加了任务。
*
* 对于这种情况,确实drainQueue没有按照doc描述返回所有未执行的任务,
* 但实际上在ThreadPoolExecutor#execute方法中,向任务队列中添加完任务后有个再次检查线程池状态的步骤。
* 此时线程A一定能够读取到线程池状态已经不是RUNNING了,在将任务从队列中移除后会使用饱和策略处理任务。
*/
private List<Runnable> drainQueue() {
BlockingQueue<Runnable> q = workQueue;
ArrayList<Runnable> taskList = new ArrayList<Runnable>();
q.drainTo(taskList);
if (!q.isEmpty()) {
for (Runnable r : q.toArray(new Runnable[0])) {
if (q.remove(r))
taskList.add(r);
}
}
return taskList;
}
3、拒绝策略(线程池饱和策略)
3.1、AbortPolicy
为Java线程池默认的阻塞策略,不执行此任务,而且直接抛出一个运行时异常,切记ThreadPoolExecutor.execute需要try catch,否则程序会直接退出。
3.2、DiscardPolicy
直接抛弃,任务不执行,空方法
3.3、DiscardOldestPolicy
从队列里面抛弃head的一个任务,并再次execute 此task。
3.4、CallerRunsPolicy
在调用execute的线程里面执行此command,会阻塞入口
3.5、自定义拒绝策略(最常用)
实现RejectedExecutionHandler,并自己定义策略模式
public static class DemoPolicy implements RejectedExecutionHandler {
/**
* 创建构造方法
*/
public DemoPolicy() { }
/**
* 拒绝策略逻辑自定义
*/
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
// 自定义逻辑
}
}
4、线程池示例
4.1、原理
-
线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。不过,就算队列里面有任务,线程池也不会马上执行它们。
-
当调用 execute() 方法添加一个任务时,线程池会做如下判断:
1. 如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务;
2. 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列。
3. 如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么还是要创建线程运行这个任务;
4. 如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,那么会进行异常捕获逻辑中。 -
当一个线程完成任务时,它会从队列中取下一个任务来执行。
-
当一个线程无事可做,超过一定的时间(keepAliveTime)时,线程池会判断,如果当前运行的线程数大于 corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到 corePoolSize 的大小。
这样的过程说明,并不是先加入任务就一定会先执行。假设队列大小为 10,corePoolSize 为 3,maximumPoolSize 为 6,那么当加入 20 个任务时,执行的顺序就是这样的:首先执行任务 1、2、3
,然后任务 4~13 被放入队列
。这时候队列满了,任务 14、15、16 会创建非核心线程马上执行
,而任务 17~20 则会抛出异常(进入拒绝策略)。最终顺序是:1、2、3、14、15、16、4、5、6、7、8、9、10、11、12、13。
所有 BlockingQueue 都可用于传输和保持提交的任务。可以使用此队列与池大小进行交互:
-
如果运行的线程少于 corePoolSize,则 Executor 始终首选添加新的线程,而不进行排队。
-
如果运行的线程等于或多于 corePoolSize,则 Executor 始终首选将请求加入队列,而不添加新的线程。
-
如果无法将请求加入队列,则创建新的线程,除非创建此线程超出 maximumPoolSize,在这种情况下,任务将被拒绝。
队列
4.1.1、SynchronousQueue直接提交
工作队列的默认选项是 SynchronousQueue,它将任务直接提交给线程而不保持它们(一般配合无界的maximumPoolSize使用)。只有当另一个线程正在等待接收它时才会成功,如果没有可用的线程则直接进入拒绝策略,因此每次都会返回并创建新线程。
如果maximumPoolSize是10,那么同时处理的任务最多只有10个线程无等待队列,吞吐量通常要高于LinkedBlockingQueue,
静态工厂方法Executors.newCachedThreadPool
使用了这个队列。
public class DemoExecutor {
public static void main(String[] args) {
/**
* 这样设置以后,如果任务处理函数出现长时间挂起的情况,会依次发生下列现象:
*
* 线程池线程数量达到核心线程数,SynchronousQueue一个不存储元素的阻塞队列
*
* 判断线程是否有空闲,有则处理任务,无则直接进入拒绝策略
*
* 线程池中的线程数量达到10个,调用DemoPolicy处理新提交的任务
*/
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
// 核心线程数
3,
// 总线程数
10,
// 超过核心线程数量的线程,多久秒之后会退出
2,
// 退出时间的单位
TimeUnit.SECONDS,
// 一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,
// 否则插入操作一直处于阻塞状态(相当于最多保持有maximumPoolSize个线程正在处理任务)
new SynchronousQueue<Runnable>(),
// 拒绝策略
new DemoPolicy()
);
/**
* 自定义发起15个线程,那么会有3个线程进入自定义拒绝策略,
* 因为核心线程数是3,总线程数是10,等待队列是0,那么最大可执行线程位10个,
* 如果线程池每个线程都忙于处理任务则,有个5进入拒绝策略,
* 如果有线程释放,如i=14时等待1s进入线程池,每个线程处理任务的时间约500毫秒,那么会被执行,不会进入拒绝策略
*/
for (int i = 0; i < 15; i++) {
int finalI = i;
// 测试是否进入拒绝策略
if(finalI ==14){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
threadPoolExecutor.execute(new Runnable() {
@Override
public void run() {
try {
System.out.println("线程====="+ finalI);
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
}
/**
* 自定义拒绝策略
*/
public static class DemoPolicy implements RejectedExecutionHandler {
/**
* 创建构造方法
*/
public DemoPolicy() { }
/**
* 拒绝策略逻辑自定义
*/
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
// 自定义逻辑
System.out.println("自定义拒绝策略");
}
}
}
4.1.2、LinkedBlockingQueue无界队列
- 不设置队列边界值:
将导致在所有 corePoolSize 线程都忙于,新任务在队列中等待。这样,创建的线程就不会超过 corePoolSize。(因此,maximumPoolSize 的值也就无效了)
当每个任务完全独立于其他任务,即任务执行互不影响时,适合于使用无界队列;这种排队可用于处理瞬态突发请求,建议指定边界值使用,否则可能导致内存溢出
。 - 设置边界值,达到核心线程,进入队列,队列满的时候创建非核心线程,处理的任务超过maximumPoolSize+capacity(队列大小)时进入拒绝策略
此队列按FIFO(先进先出)排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()
使用了这个队列
public class DemoExecutor {
public static void main(String[] args) {
/**
* 这样设置以后,如果任务处理函数出现长时间挂起的情况,会依次发生下列现象:
*
* 线程池线程数量达到核心线程数,LinkedBlockingQueue无界队列,先进先出原则
*
* 判断线程是否有空闲,有则处理任务,无则进入等待队列,超过队列边界值则直接进入拒绝策略
*
* 线程池中的线程数量达到12个,调用DemoPolicy处理新提交的任务
*/
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
// 核心线程数
3,
// 总线程数
10,
// 超过核心线程数量的线程,多久秒之后会退出
2,
// 退出时间的单位
TimeUnit.SECONDS,
// 无界队列,只会使用核心线程,不会创建非核心线程,最大处理线程数corePoolSize
// 有界队列,达到核心线程,进入队列,队列满的时候创建非核心线程,处理的任务超过maximumPoolSize+capacity时进入拒绝策略
new LinkedBlockingQueue<Runnable>(4),
// 拒绝策略
new DemoPolicy()
);
/**
* 自定义发起15个线程,那么每批会有3个线程执行,等核心线程释放后,才会再次执行
* 超过队列边界值则会进入拒绝策略
*/
for (int i = 0; i < 15; i++) {
int finalI = i;
threadPoolExecutor.execute(new Runnable() {
@Override
public void run() {
try {
System.out.println("线程====="+ finalI);
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
}
/**
* 自定义拒绝策略
*/
public static class DemoPolicy implements RejectedExecutionHandler {
/**
* 创建构造方法
*/
public DemoPolicy() { }
/**
* 拒绝策略逻辑自定义
*/
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
// 自定义逻辑
System.out.println("自定义拒绝策略");
}
}
}
4.1.3、ArrayBlockingQueue有界队列
当使用有限的 maximumPoolSizes 时,有界队列(如 ArrayBlockingQueue)有助于防止资源耗尽,但是可能较难调整和控制。队列大小和最大池大小可能需要相互折衷:使用大型队列和小型池可以最大限度地降低 CPU 使用率、操作系统资源和上下文切换开销,但是可能导致人工降低吞吐量。如果任务频繁阻塞(例如,如果它们是 I/O 边界),则系统可能为超过您许可的更多线程安排时间。使用小型队列通常要求较大的池大小,CPU 使用率较高,但是可能遇到不可接受的调度开销,这样也会降低吞吐量。
ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序。
public static void main(String[] args) {
/**
* 这样设置以后,如果任务处理函数出现长时间挂起的情况,会依次发生下列现象:
*
* 线程池线程数量达到核心线程数,向ArrayBlockingQueue中放入任务
*
* ArrayBlockingQueue达到上限,创建新的线程进行处理
*
* 线程池中的线程数量达到10个,调用DemoPolicy处理新提交的任务
*/
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
3, // 核心线程数
10, // 总线程数
2, // 超过核心线程数量的线程,多久秒之后会退出
TimeUnit.SECONDS, // 退出时间的单位
new ArrayBlockingQueue<Runnable>(2), // 队列长度为2
new DemoPolicy() // 拒绝策略
);
/**
* 自定义发起15个线程,那么会有3个线程进入自定义拒绝策略,
* 因为核心线程数是3,总线程数是10,等待队列是2,那么最大可执行线程位12个,
* 所以有3个进入拒绝策略
*/
for (int i = 0; i < 15; i++) {
threadPoolExecutor.execute(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
}
/**
* 自定义拒绝策略
*/
public static class DemoPolicy implements RejectedExecutionHandler {
/**
* 创建构造方法
*/
public DemoPolicy() { }
/**
* 拒绝策略逻辑自定义
*/
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
// 自定义逻辑
System.out.println("自定义拒绝策略");
}
}
5、如何设置合理的线程数
一般多线程执行的任务类型可以分为 CPU 密集型和 I/O 密集型,根据不同的任务类型,计算线程数的方法也不一样。
-
CPU 密集型任务
:这种任务消耗的主要是 CPU 资源,可以将线程数设置为N(CPU 核心数)+1
,比 CPU 核心数多出来的一个线程是为了防止线程偶发的缺页中断,或者其它原因导致的任务暂停而带来的影响。一旦任务暂停,CPU 就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间。 -
I/O 密集型任务
:这种任务应用起来,系统会用大部分的时间来处理 I/O 交互,而线程在处理 I/O 的时间段内不会占用 CPU 来处理,这时就可以将 CPU 交出给其它线程使用。因此在 I/O 密集型任务的应用中,我们可以多配置一些线程,具体的计算方法是2N
。 -
综合型业务
:线程数= N(CPU核数)*(1+WT(线程等待时间)/ST(线程时间运行时间))- 可以通过JDK 自带的工具 VisualVM 来查看 WT/ST 比例
WT(线程等待时间)= [线程运行总时间] - [ST(线程时间运行时间)]
可以根据自己的业务场景,从“N+1”和“2N”两个公式中选出一个适合的,计算出一个大概的线程数量,之后通过实际压测,逐渐往“增大线程数量”和“减小线程数量”这两个方向调整,然后观察整体的处理时间变化,最终确定一个具体的线程数量。
要提高线程池的处理能力,一定要先保证一个合理的线程数量,也就是保证 CPU 处理线程的最大化。在此前提下,我们再增大线程池队列,通过队列将来不及处理的线程缓存起来。在设置缓存队列时,我们要尽量使用一个有界队列,以防因队列过大而导致的内存溢出问题。
5.1、思路
-
非核心业务
可以将核心线程数设置小一些,最大线程数量设置为计算线程数量。 -
核心业务中
两者可以设置一样。 -
阻塞队列
可以根据具体业务场景设置,如果线程处理业务非常迅速,我们可以考虑将阻塞队列设置大一些,处理的请求吞吐量会大些;如果线程处理业务非常耗时,阻塞队列设置小些,防止请求在阻塞队列中等待过长时间而导致请求已超时。
在程序中,除了并行段代码,还有串行段代码。当程序同时存在串行和并行操作时,优化并行操作是不是优化系统的关键呢?
- 串行执行的代码会成为影响性能的关键,应尽量减少执行时间,比如减少锁持有时间,这样才能达到最大程度的并发
- 优化并行操作带来的收益是有上限的
- Amdahl’s(阿姆达尔)定律指出
优化串行是优化系统性能的关键
,我们应该从算法入手,减少程序中串行的部分,而不是增加线程数来提高系统的并发处理能力。
对于应用而言,可能有多种类型的任务要执行,我们是分别创建不同的线程池还是创建一个统一的线程池来控制资源的使用呢?
- 分别创建。如果过分彼此相互影响,建议拆开服务,分别部署。