前言
在部分老项目里,你可能需要对线程的使用进行优化,将以前使用Executor创建的四种类型线程池以及单独new Thread方式就行优化管理。最近刚好在对老项目的优化,采用的方案就是众所周知的全局线程池管理类,利用ThreadPoolExecutor来创建线程池。
线程生命周期相关可了解 线程优化需要了解的一些点
线程池中的CAS部分可了解 CAS、Synchronized、ReentrantLock原理
目录
一、线程池简介
ScheduledExecutorService 继承自 ExecutorService,增加了定时任务相关方法
ForkJoinPool 是一种支持任务分解的线程池,一般要配合可分解任务接口 ForkJoinTask 来使用,最适合的是计算密集型的任务
方便开发者使用线程池,JDK在Executors中提供了不同的静态方法给我们使用,如下
newFixedThreadPool
- 创建固定长度的、可重用的线程池,如果运行中的某个线程由于发生了未预期的
Exception而结束,那么线程会补充一个新的线程
newSingleThreadExecutor
- 创建一个单线程的
Executor,如果这个线程异常结束,会创建另一个线程来替代,能确保依照任务在队列中的顺序来串行执行,即任务先进先出的规则执行
newCachedThreadPool
- 创建一个可缓存的线程池,如果线程池的当前规模超过了处理需求时,那么将回收空闲的线程
newScheduledThreadPool
- 创建一个固定长度的线程池,而且以延迟或定时的方式来执行任务,类似
Timer(在阿里巴巴Java规范中Timer已不建议使用,因为单个任务异常会导致后续任务都不能执行)
在阿里Java规范中已经不建议使用Executors创建线程池了,那这是为什么呢?我们先了解原理。
二、线程池工作原理
1、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
线程池最大能够容纳同时执行的线程数,必须大于或等于 1。如果和 corePoolSize 相等即是固定大小线程
keepAliveTime
非核心线程的超时时长,线程池中的线程空闲时间,当空闲时间达到此值时,线程会被销毁直到剩下 corePoolSize 个线程。
如果ThreadPoolExecutor的allowCoreThreadTimeOut属性设置为true,则该参数也表示核心线程的超时时长。
unit
用来指定 keepAliveTime 的时间单位,有 MILLISECONDS、SECONDS、MINUTES、HOURS 等
workQueue
等待队列,BlockingQueue 类型。当请求任务数大于 corePoolSize 时,任务将被缓存在此 BlockingQueue 中
threadFactory
线程工厂,线程池中使用它来创建线程,如果传入的是 null,则使用默认工厂类 DefaultThreadFactory。
handler
执行拒绝策略的对象。当 workQueue 满了之后并且活动线程数大于 maximumPoolSize 的时候,线程池通过该策略处理请求
2、常用阻塞队列
阻塞队列:队列已满,放元素被阻塞。队列为空时,拿数据会被阻塞。
在生产者、消费者模型中,为了防止生产者或者消费者生产、消费过快,一般会使用阻塞队列实现。
ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列
LinkedBlockingQueue:一个由链表结构组成的有界阻塞队列
PriorityBlockingQueue:一个支持优先级排序的无界阻塞队列
SynchronousQueue:一个不存储元素的阻塞队列
LinkedTransferQueue:一个由链表结构组成的无界阻塞队列
LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列
DelayQueue:一个使用优先级队列实现的无界阻塞队列
3、ThreadPoolExecutor.execute
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
//步骤1
int c = ctl.get();
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);
}
总共3个步骤
步骤1
-
ctl是AtomicInteger类型,二进制高3位用来标识线程池的状态,低29位用来记录线程池中的数量private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0)); private static int ctlOf(int rs, int wc) { return rs | wc; } private static final int RUNNING = -1 << COUNT_BITS; private static final int COUNT_BITS = Integer.SIZE - 3;计算当前运行状态
private static int runStateOf(int c) { return c & ~CAPACITY; }计算当前线程数量
private static int workerCountOf(int c) { return c & CAPACITY; }线程池所拥有的状态如下
// runState is stored in the high-order bits //默认状态,接受新任务并处理排队任务 private static final int RUNNING = -1 << COUNT_BITS; //不接受新任务,但处理排队任务,调用shutDown方法触发 private static final int SHUTDOWN = 0 << COUNT_BITS; //不接受新任务,不处理排队任务,中断正在运行的任务,调用shutDowNow方法触发 private static final int STOP = 1 << COUNT_BITS; //所有任务都已终止,workerCount为0时,线程状态转换到该状态,并执行terminate方法 private static final int TIDYING = 2 << COUNT_BITS; private static final int TERMINATED = 3 << COUNT_BITS;shutDown线程池状态立即变成
SHUTDOWN状态,等任务执行完后才中断线程shutDowNow线程池立即变为
STOP状态,不等任务执行完就中断线程 -
workerCountOf(c)< corePoolSize即线程池中运行的线程数量还没有达到corePoolSize大小时,线程池会创建一个新线程执行提交任务,无论之前的线程是否处于空闲状态if (workerCountOf(c) < corePoolSize) { if (addWorker(command, true)) return; c = ctl.get(); } -
addWorker方法,注释在代码中,core代表核心线程,如果活动线程数小于设定的corePoolSize,core就为trueprivate boolean addWorker(Runnable firstTask, boolean core) { retry: for (;;) { int c = ctl.get();//1.获取线程池运行状态 int rs = runStateOf(c); // Check if queue empty only if necessary. //2.是否可以添加任务 if (rs >= SHUTDOWN && ! (rs == SHUTDOWN && firstTask == null && ! workQueue.isEmpty())) return false; for (;;) { int wc = workerCountOf(c); //3.线程数数量是否大于线程池上限或者核心线程数或者最大线程数 if (wc >= CAPACITY || wc >= (core ? corePoolSize : maximumPoolSize)) return false; //4.利用CAS操作来增加线程数的数量 if (compareAndIncrementWorkerCount(c)) break retry; c = ctl.get(); // Re-read ctl //5.线程数数量没有增加、线程池状态改变了,重新走流程 if (runStateOf(c) != rs) continue retry; // else CAS failed due to workerCount change; retry inner loop } } //6.一般情况会走到4处,CAS增加线程数量,然后执行下面逻辑 boolean workerStarted = false; boolean workerAdded = false; Worker w = null; try { //7.新建Worker对象 w = new Worker(firstTask); //8.获取当前线程,此线程经过默认ThreadFactory创建的不会是守护线程 final Thread t = w.thread; if (t != null) { //8.上锁 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()); //9.线程池没有关闭状态,不处于不接受新任务状态 if (rs < SHUTDOWN || (rs == SHUTDOWN && firstTask == null)) { if (t.isAlive()) // precheck that t is startable throw new IllegalThreadStateException(); //10.workers是HashSet类型,上锁保证了多线程问题 workers.add(w); int s = workers.size(); if (s > largestPoolSize) largestPoolSize = s; workerAdded = true; } } finally { mainLock.unlock(); } if (workerAdded) { //11.添加成功,开启线程进入到就绪状态,等待CPU调度执行 t.start(); workerStarted = true; } } } finally { if (! workerStarted) //12.线程添加失败,从workers中移除w,然后恢复线程数量 addWorkerFailed(w); } return workerStarted; }
步骤2
-
步骤1中,如果线程数不小于
corePoolSize或者是添加线程对应的Worker对象失败,则会执行到步骤2 -
主要功能是当前线程池中运行的线程数量已经达到
corePoolSize大小时,线程池会把任务加入到等待队列中,直到某一个线程空闲了,线程池会根据我们设置的等待队列规则,从队列中取出一个新的任务执行if (isRunning(c) && workQueue.offer(command)) {//线程池是RUNNING状态,将任务添加到BlockingQueue队列里面 //重新检查线程池的状态,不是RUNNING,直接移除任务,并执行拒绝策略,否则如果线程池数量为0,单独创建线程,而不指定任务 int recheck = ctl.get(); if (! isRunning(recheck) && remove(command)) reject(command); else if (workerCountOf(recheck) == 0) addWorker(null, false); } -
为什么在上面的判断检查过程中,会单独创建一个线程,添加空的任务?
之前已经把
command提交到阻塞队列了workQueue.offer(command)。所以提交一个空线程,直接从阻塞队列里面取就可以了。Thread创建时传递了Worker作为一个任务,执行时会调用Worker的run方法,run方法内调用了runWorker方法,该方法内通过判断传递的Runnable是否为空,为空则从workQueue中取。 -
如果
corePoolSize = 0呢?线程池执行哪个方法?如何工作?corePoolSize为0,肯定是直接执行步骤2了,然后判断线程数数量为0,则会通过addWorker(null,false)来尝试新建一个空线程
步骤3
-
如果线程数大于
corePoolSize数量但是还没有达到最大线程数maximumPoolSize,并且等待队列已满,则线程池会创建新的线程来执行任务else if (!addWorker(command, false)) reject(command); -
如果线程池不是
RUNNING或者offer加入阻塞队列失败,那么开启非核心线程池来处理,启动线程数大于maximumPoolSize,任务就会被拒绝
4、AbstractExecutorService.submit
public Future<?> submit(Runnable task) {
if (task == null) throw new NullPointerException();
RunnableFuture<Void> ftask = newTaskFor(task, null);
execute(ftask);
return ftask;
}
submit方法内部还是调用execute(Runnable runnable)方法,只不过这个Runnable被包装成了RunnableFuture
protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) {
return new FutureTask<T>(runnable, value);
}
也就是说被包装的Runnable真实类型是FutureTask,这个就不陌生了吧,它间接实现了Runnable和Future两个接口。
三、线程池总结
创建线程池时使用ThreadPoolExecutor类,来指定核心线程数,和最大线程数。
加入的任务会放到指定的阻塞队列中,然后创建核心线程来执行任务,如果活动线程数大于核心线程数,则任务添加到队列中,等待核心线程数去执行(例如固定数量为3的线程池,提交了10个任务,会先由这3个核心线程执行)。当阻塞队列已满,则开启非核心线程数执行。否则执行拒绝策略。
AbortPolicy
丢弃任务并抛出RejectedExecutionException异常,是线程池的默认拒绝策略,在任务不能再提交的时候,抛出异常,及时上报服务器,反馈业务状态,开发人员及时排查。
DiscardPolicy
丢弃任务,但是不能抛出异常。使用此策略,可能会使我们无法发现系统的异常状态。一般无关紧要的业务采用此策略。
DiscardOldestPolicy
丢弃队列最前面的任务,然后重新提交被拒绝的任务。是否采用此种拒绝策略,根据业务判断。
CallerRunsPolicy
由调用方(提交任务的线程)处理该任务。这种情况是需要让所有任务都执行完毕,适合大量计算任务类型去执行,看实际业务场景,提高吞吐量但是任务全都要执行可选。
四、为什么禁止使用Executors
我们直接看Executors创建线程的几个方法
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue());
}nousQueue<Runnable>()); } public ScheduledThreadPoolExecutor(int corePoolSize) { super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS, new DelayedWorkQueue()); }
newSingleThreadExecutor 和 newFixedThreadPool所传递的阻塞队列,都是没有指定大小的,无界,在创建的任务很多情况下,可能会有很多没处理的任务放到队列里面,队列无限大,可能会导致OOM。
newCachedThreadPool和ScheduledThreadPoolExecutor例外,它是线程数最大值为 Integer.MAX_VALUE。这样又会有什么问题呢?
在任务很多的情况下,会创建很多线程,超出手机对单个进程创建线程的限制,直接OOM。
阿里巴巴JAVA规范也有说明,如下图