从ThreadPoolExecutor源码着手来谈一谈并发中使用的线程池
线程池,关键在于一个**“池”字,你可以联想一下数据库连接池、字符串常量池等等,都是实际上都是利用池化技术**来减少每次获取资源的消耗,提高对资源的利用率。在高并发多线程的场景下,某个时间段内肯定不只有一个线程在工作,而是有大量的线程同时在执行任务。当面对大量的请求等待处理时,我们不可能一个一个的创建出线程,然后分配请求任务让这个线程去处理,这样效率也太低了。怎么解决呢?我们想如果说事先创建好了很多线程,随时待命,一旦有请求发送过来,我们就挑一个线程立刻去执行,这样效率不就提高了吗,而这就是所谓的线程池。线程池其实就是维护了多个线程,以及线程的相关信息,提供了对线程的统一管理。
在并发场景下使用线程池有3个好处:
-
降低资源消耗
一个线程在执行完分配的任务之后,并不是立刻销毁掉,而是重新放回到线程池中,这样通过重复利用已创建的线程,来降低线程创建和销毁造成的消耗。
-
提供响应速度
线程池会提前创建好多个线程,随时待命,一旦有请求任务来了就可以分配给空闲的线程立即执行。
-
统一管理线程
并发场景下,会有多个线程同时在执行任务,线程的创建、销毁,请求任务的分配,可以都交给线程池来统一管理和监控。
既然我们说了线程池可以实现对线程的统一管理,那么当请求发送过来时,到底谁来统一调度和管理这些线程呢?这时候就需要用到Executor框架了,想提供了线程的执行机制,统一调度、分配、管理线程。你可以把线程池比作一个军队,现在所有的士兵都集合完毕,时刻准备着,但是必须有一个将军总指挥,下达给士兵作战命令。而这个总指挥就是我们这里所说的Executor框架。
Executor框架
首先我们要知道在JVM中,Java的线程是被一对一映射为本地操作系统线程的。Java线程启动时会创建一个本地操作系统线程,操作系统会调度这个线程并将它们分配给可用的CPU,当这个Java线程终止时,这个操作系统线程也会被回收。一个Java程序通常会被分解为若干个任务,而Executor框架就是充当调度器的作用,在应用层,Executor将这些任务映射为固定数量的线程,然后底层的操作系统将这些线程映射到硬件处理器上,这也就是Executor框架的两级调度模型。
Executor框架的结构主要由3大部分组成:
-
任务
执行任务需要实现Runnable接口或者Callable接口,这两个接口的实现类可以被ThreadPoolExecutor执行。
-
任务的执行
Executor接口提供任务执行机制的核心方法,将任务的提交和执行分离开来。以及继承了Executor接口了ExecutorService接口。Executor框架有两个关键类实现了ExecutorService接口,即ThreadPoolExecutor(核心实现类,用来执行被提交的文物)和ScheduledThreadPoolExecutor。
-
异步计算的结果
Future接口以及该接口的实现类FutureTask都可以返回一个异步计算结果
根据上述的Executor的结构,我们可以总结出Executor框架的使用流程图
- 主线程首先要创建实现Runnable接口或者Callable接口的任务对象,工具类Executors可以把一个Runnable对象封装为一个Callable对象。
- 然后主线程可以把这个Runnable对象调用execute方法直接提交给ExecutorService执行;或者也可以把Runnable对象或Callable对象调用submit方法直接提交给ExecutorService执行。
- 如果是使用submit方法提交的任务对象,那么ExecutorService将返回一个实现Future接口的对象,即FutureTask对象。FutureTask对象实际上也实现了Runnable接口,所以也可以直接将FutureTask对象提交给ExecutorService执行。
- 最后主线程可以执行FutureTask对象的get方法来等待任务执行完成,也可以执行FutureTask对象的cancel方法来取消此任务的执行。
其实Executor框架你可以类比于之前我们讲的AQS,它们都是一个框架,一种规范。所以我们可以使用Executor框架来实现一个线程池,Executor框架对应的工具类是Executors,我们可以使用Executors来创建出不同种类的线程池。
最常用的是以下三种线程池:
-
FixedThreadPool
创建一个固定大小的线程池,可以控制线程最大并发数,超出的线程会在队列中等待。这类线程池比较适合执行长期任务
-
SingleThreadExecutor
创建一个单线程化的线程池,它只会使用唯一的工作线程来执行任务,保证所有任务按照指定的顺序执行。这类线程池适合一个任务一个任务执行的场景。
-
CachedThreadPool
创建一个可缓存的线程池,会根据需要创建线程。如果线程池有可回收的空闲线程,就会重用这个空闲线程,如果说没有可回收的空闲线程,就新建线程。这类线程池适合执行很多短期异步的小任务
不知道你发现没有这3种最常用的线程池,底层的源码实现实际上都是使用了ThreadPoolExecutor这个类的构造方法。ThreadPoolExecutor是Executor子接口ExecutorService的实现类,从名字上看就知道,这个类是专门用来处理线程的调度器,ThreadPoolExecutor也是实现线程池最核心的类之一。接下来我们就先弄清楚ThreadPoolExecutor底层的实现原理,也就弄懂了这3种最常见线程池的实现原理了。
ThreadPoolExecutor类
我们从ThreadPoolExecutor的构造方法入手,通过参数列表中的7大参数,来看看底层的实现。
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;
}
-
corePoolSize
线程池中的常驻核心线程数。在创建了线程池后,当有请求任务来了之后,就会安排池中的线程去执行请求任务。
-
maximumPoolSize
线程池能够容纳同时执行的最大线程数。
-
unit
keepAliveTime的时间单位。当线程池中的线程数量大于 corePoolSize 的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了 keepAliveTime才会被回收销毁;
-
workQueue
任务队列,用来暂时保存被提交但还没执行的任务。当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。
-
threadFactory
线程工厂,用来创建线程,使用默认的即可
-
handler
拒绝策略。当任务队列满了并且工作线程大于等于线程池的最大线程数时,决定采用哪种拒绝策略。
拒绝策略有4种:
-
AbortPolicy
拒绝新来的任务的时,会抛出异常
-
CallerRunsPolicy
将新的任务返回给调用者来执行这个任务
-
DiscardPolicy
不处理新来的任务,直接丢弃掉
-
DiscardOldestPolicy
直接丢弃掉最老的还没有处理的任务,也就是任务队列中等待时间最久的那个任务会被丢弃,然后把新任务加入到任务队列中。
-
所以说,线程池底层的实现原理或者说主要的处理流程如下图所示
-
在创建了线程池后,线程池会等待提交的任务请求
-
当调用execute()方法提交一个任务时,线程池会做判断
(1)如果正在运行的线程数小于corePoolSize核心线程数,那么就会马上创建线程运行这个任务
(2)如果正在运行的线程数大于或等于corePoolSize核心线程数,那么就会将这个任务放入队列中
(3)如果这时候队列满了并且正在运行的线程数小于maximumPoolSize最大线程数,那么就会创建非核心线程马上运行这个任务
(4)如果队列满了并且正在运行的线程数大于或等于maximumPoolSize最大线程数,那么线程池就会执行拒绝策略
-
当一个线程完成任务时,它就会从队列中取下一个任务来执行
-
如果线程执行完任务并且空闲时间超过了keepAliveTime存活时间,并且当前运行的线程数大于核心线程数,就会把这个空闲线程给终止掉。所以说线程池的所有任务完成后,最终会收缩到核心线程数大小。
了解了ThreadPoolExecutor的构造方法的7大参数以及底层实现的具体执行流程之后,接下来我们就深入源码中,看看这个线程池的底层到底是怎么实现的!
-
成员变量ctl
ThreadPoolExecutor类有一个成员变量ctl。它是一个Integer类型的原子变量 ,用来记录线程池状态和线程池中线程数,类似于ReentrantReadWriteLock使用一个变量来保存两种信息。integer类型是32bit二进制表示,其中高3 位用来表示线程池状态,低29位用来记录线程池中线程个数。默认是Running状态,线程个数为0。将高位和低位按位或,来记录线程池状态和线程池中线程数
-
线程池状态
- RUNNING :接受新任务并且处理阻塞队列里的任务
- SHUTDOWN :拒绝新任务但是处理阻队列里的任务
- STOP :拒绝新任务并且放弃阻塞队列里的任务 ,同时会中断正在处理的任务。
- TIDYING :所有任务都执行完(包含阻队列里面的任务)后当前线程池活动线程数为0,将要调用terminated 方法
- TERMINATED: 终止状态 ,terminated 方法调用完成以后的状态
这些线程池状态转换如下图所示
-
execute()方法
public void execute(Runnable command) { if (command == null) throw new NullPointerException(); int c = ctl.get(); //当前线程数量小于核心线程数,此次提交任务,直接创建一个新的worker线程 if (workerCountOf(c) < corePoolSize) { if (addWorker(command, true)) return; c = ctl.get(); } //当前线程数量大于等于核心线程数,首先判断当前线程池是否处于RUNNING状态,如果是则尝试将提交的任务放入到workQueue中 if (isRunning(c) && workQueue.offer(command)) { int recheck = ctl.get(); /*再次判断当前线程池是否为RUNNINT状态,因为有可能提交任务到队列之后,线程池状态被其他线程给修改了,比如调用shutdown()/shutdownNow()等。这种情况就需要把刚刚提交到队列中的的任务删除掉。然后执行拒绝策略*/ if (! isRunning(recheck) && remove(command)) reject(command); else if (workerCountOf(recheck) == 0) addWorker(null, false); } //线程数超过核心线程数且workQueue中数据已满,这里添加失败可能是线程数已经超过了maximumPoolSize else if (!addWorker(command, false)) reject(command); }
在执行execute()方法需要关注几个关键的点:
- 提交新任务时,当前线程数量和核心线程数之间的大小关系
- 提交新任务时,当前线程池的状态
-
addWorker()方法
上面分析提交任务的方法execute()时多次用到addWorker()方法,addWorker()方法接收任务后将任务封装成工作线程Worker。
Worker是ThreadPoolExecutor的内部类,继承AQS并且实现了Runable接口,封装了工作线程。继承AQS实现了不可重入独占锁。从Worker这个内部类的源码中我们可以看出,实际上是把一个Runnable对象和一个Thread线程封装在了一起,也就是说一个任务映射一个线程,这也就体现出我们之前所说的Executor框架的两级调度模型中的应用层的调度。
private boolean addWorker(Runnable firstTask, boolean core) { retry: for (;;) { int c = ctl.get(); int rs = runStateOf(c); //外层循环主要是判断线程池的状态,只有Running状态和SHUTDOWN状态合法, //SHUTDOWN状态还可以添加一个worker,但是firstTask任务必须为null,因为SHUTDOWN状态虽然不接收新任务,但是还是需要有线程去执行队列中剩下的任务 if (rs >= SHUTDOWN && ! (rs == SHUTDOWN && firstTask == null && ! workQueue.isEmpty())) return false; //内层循环主要是通过CAS操作更新工作线程的数量,如果更新成功则往线程池中添加线程 for (;;) { int wc = workerCountOf(c); //判断当前工作线程数是否大于等于了核心线程数或者最大线程数,如果超出则添加失败 if (wc >= CAPACITY || wc >= (core ? corePoolSize : maximumPoolSize)) return false; //CAS操作更新工作线程数,如果成功则直接跳出最外层循环 if (compareAndIncrementWorkerCount(c)) break retry; //有可能多线程并发修改工作线程数,竞争导致更新失败,重新获取ctl值 c = ctl.get(); //判断是否线程池状态发生了改变,如果没有状态没有发生改变,说明只是多线程CAS操作导致的失败,并不是线程池状态的改变导致的失败,那么就继续自旋更新工作线程数量。如果是状态发生了改变,那么就重新来一轮的外层循环,重新判断线程池状态 if (runStateOf(c) != rs) continue retry; } } //如果CAS更新工作线程数量成功,那么就继续执行,准备封装一个worker工作线程添加到线程池中 boolean workerStarted = false; boolean workerAdded = false; Worker w = null; try { w = new Worker(firstTask);//封装worker final Thread t = w.thread; if (t != null) { //在往池子中添加Worker的时候,是需要先加锁的,因为针对全局的workers(线程池中的工作线程集合,就是一个HashSet)操作并不是线程安全的。 final ReentrantLock mainLock = this.mainLock; mainLock.lock(); try { int rs = runStateOf(ctl.get()); //向线程池中添加工作线程时需要进行二次检查,第一次是在execute()方法中 //需要判断线程池的状态,当状态是Running或者SHUTDOWN并且firstTask为null才可能添加成功 if (rs < SHUTDOWN || (rs == SHUTDOWN && firstTask == null)) { //判断Worker中的线程是否在还没有封装完成,就已经start了,必须保证在没封装完成返回给外部之前,线程不能运行起来 if (t.isAlive()) // precheck that t is startable throw new IllegalThreadStateException(); workers.add(w);//向工作线程集合中添加新创建出来的工作线程 int s = workers.size(); if (s > largestPoolSize) largestPoolSize = s; workerAdded = true;//代表当前提交的任务所创建的Worker已经添加到线程池中了 } } finally { mainLock.unlock(); } if (workerAdded) { t.start();//工作线程添加成功,可以启动线程去处理对应的任务了,Worker重写了run方法,start-》run-》runWorker workerStarted = true;//表示工作线程已启动 } } } finally { if (! workerStarted) addWorkerFailed(w); } return workerStarted; }
-
runWorker()方法
上面的addWorker()方法正常的执行逻辑会创建一个Worker,然后启动Worker中的线程,这里其实就会执行到runWorker方法。
Worker中的线程会调用start来启动线程,Worker中重新了run()方法,而这个run()方法实际上调用的是runWorker()方法
final void runWorker(Worker w) { Thread wt = Thread.currentThread(); Runnable task = w.firstTask; w.firstTask = null; w.unlock(); // allow interrupts boolean completedAbruptly = true; try { //判断task是否为空,如果是一个空任务,那么就去workQueue中获取任务,如果两者都为空就会退出循环。 while (task != null || (task = getTask()) != null) { //工作线程执行前要加锁 w.lock(); // If pool is stopping, ensure thread is interrupted; // if not, ensure thread is not interrupted. This // requires a recheck in second case to deal with // shutdownNow race while clearing interrupt 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()启动当前任务 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 = null; w.completedTasks++;//当前工作线程处理的任务数加1 w.unlock();//工作线程执行完释放锁 } } completedAbruptly = false;//执行到这置为false,说明是任务正常完成退出,不是异常中断退出 } finally { processWorkerExit(w, completedAbruptly); } }
runWorker()中只是启动了当前线程工作,还需要源源不断通过getTask()方法从workQueue来获取任务执行。在workQueue没有任务的时候,根据线程池工作线程数和核心线程数的对比结果来使用processWorkerExit()执行清理工作。
-
getTask()方法
getTask方法用于从阻塞队列中获取任务,如果阻塞队列为空并且非核心线程空闲时间超时,就会回收这个空闲线程。
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. if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) { decrementWorkerCount(); return null; } int wc = workerCountOf(c); // Are workers subject to culling? boolean timed = allowCoreThreadTimeOut || wc > corePoolSize; if ((wc > maximumPoolSize || (timed && timedOut)) && (wc > 1 || workQueue.isEmpty())) { if (compareAndDecrementWorkerCount(c)) return null; continue; } try { //获取阻塞队列中的任务,采用poll还是take取决于allowCoreThreadTimeOut和线程数量, //allowCoreThreadTimeOut如果设置为true则代表核心线程数下的线程也是可以被回收的 //如果使用take则表明workQueue中没有任务当前线程就会被阻塞挂起,直到有了新的任务才会被唤醒。 //add、remove:抛异常 //offer、poll:不抛异常,有返回值true or false //put、take:队列为空会阻塞挂起,非空时转为就绪状态 Runnable r = timed ? workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) : workQueue.take(); if (r != null) return r; timedOut = true; } catch (InterruptedException retry) { timedOut = false; } } }
我们在了解了ThreadPoolExecutor的底层实现之后,再来看看最常用的3种线程池是怎么实现。实际上这3种线程池就是通过对ThreadPoolExecutor构造方法的参数设置不同的参数值,来实现不同的种类的线程池。
FixedThreadPool——可重用固定线程数的线程池
从源码中我们可以看到,FixedThreadPool其实是创建了核心线程数和最大线程数相同的ThreadPoolExecutor,相当于从线程池一创建出来,完成预热之后,就直接把可用工作线程拉满。
- 如果当前运行的线程数小于 corePoolSize, 如果再来新任务的话,就创建新的线程来执行任务;
- 当前运行的线程数等于 corePoolSize 后, 如果再来新任务的话,会将任务加入 LinkedBlockingQueue阻塞队列中等待;
- 线程池中的线程执行完任务后,会在循环中反复从 LinkedBlockingQueue 中获取任务来执行;
- 如果阻塞队列也满了,那么就执行拒绝策略;
但是不推荐使用FixedThreadPool,因为FixedThreadPool的阻塞队列使用的使用无界队列,即队列容量为Integer.MAX_VALUE,使用无界队列将会对线程池带来一些影响:
- 当线程池中的线程数达到 corePoolSize 后,新任务将在无界队列中等待,因此线程池中的线程数不会超过corePoolSize
- 由于使用无界队列时 maximumPoolSize 将是一个无效参数,因为不可能存在任务队列满的情况。所以,通过创建 FixedThreadPool的源码可以看出创建的 FixedThreadPool 的 corePoolSize 和 maximumPoolSize 被设置为同一个值。
- 使用无界队列时 keepAliveTime 也将是一个无效参数,设置为0,也就是说有空闲线程就会立即回收
- 运行中的 FixedThreadPool(未执行 shutdown()或 shutdownNow())不会执行拒绝策略,因为不存在队列满这么一说,满相当于直接OOM溢出了。在任务比较多的时候会导致 OOM(内存溢出)
SingleThreadExecutor——单线程的线程池
从源码中我们可以看到,SingleThreadExecutor其实是创建了核心线程数和最大线程数相同都为1的ThreadPoolExecutor
- 如果当前运行的线程数少于 corePoolSize,则创建一个新的线程执行任务;
- 当前线程池中有一个运行的线程后,新来的任务将放入 LinkedBlockingQueue阻塞队列中;
- 当这个唯一的线程执行完当前的任务后,会在循环中反复从LinkedBlockingQueue 中获取任务来继续执行任务;
同时也不推荐使用SingleThreadExecutor,因为SingleThreadExecutor的阻塞队列也是使用的无界队列,很可能造成OOM
CachedThreadPool——按需创建线程的线程池
CachedThreadPool 会根据需要来创建线程,corePoolSize 被设置为0,maximumPoolSize被设置为 Integer.MAX.VALUE,即它是无界的,这也就意味着如果主线程提交任务的速度高于 maximumPool 中线程处理任务的速度时,CachedThreadPool 会不断创建新的线程。极端情况下,这样会导致耗尽 cpu 和内存资源。同时把KeepaliveTime设置为了60秒,空闲线程等待新任务的时间超过60秒就会被回收。所以说CachedThreadPool线程池属于一种动态的弹性伸缩的线程池。
主要注意的是,CachedThreadPool 使用的阻塞队列是SynchronousQueue同步队列,是一个没有容量的阻塞队列。每一个插入操作必须等待另一个线程对应的移除操作,当插入和移除配对时,SynchronousQueue会把主线程提交的任务传递给空闲线程去执行。
确定线程池大小
实际上我们上面所说的通过Executors类去创建的3种常见的线程池,在平时的开发中,我们不会这样做的,因为它们都有可能造成OOM。一般来说都是使用ThreadPoolExecutor去创建一个线程池,那么问题来了,在使用ThreadPoolExecutor创建线程池的时候,我们应该将线程池的大小设置为多大?核心线程数和最大线程数分别设置为多少才比较合适?
你可能想是不是把线程池配置的越大越好,这样就可能处理更多的请求,提高了吞吐量。其实并不是越大越好,线程数量过多时,会增加上下文切换的成本,多线程编程中一般线程的个数都大于 CPU 核心的个数,而一个 CPU 核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU 采取的策略是为每个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换。概括来说就是:当前任务在执行完 CPU 时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是一次上下文切换。
如果我们设置的线程池数量太小的话,如果同一时间有大量任务需要处理,可能会导致大量的任务在任务队列中排队等待执行,甚至会出现任务队列满了之后任务无法处理的情况,或者大量任务堆积在任务队列导致 OOM。这样很明显是有问题的! CPU 根本没有得到充分利用。
如果我们设置线程数量太大,大量线程可能会同时在争取 CPU 资源,这样会导致大量的上下文切换,从而增加线程的执行时间,影响了整体执行效率。
那么到底该设置多大的线程池呢?适合的才是最好的!
-
对于CPU密集型任务
这种任务消耗的主要是 CPU 资源,可以将线程数设置为 N(CPU 核心数)+1,比 CPU 核心数多出来的一个线程是为了防止线程偶发的缺页中断,或者其它原因导致的任务暂停而带来的影响。一旦任务暂停,CPU 就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间。
-
对于IO密集型任务
这种任务应用起来,系统会用大部分的时间来处理 I/O 交互,而线程在处理 I/O 的时间段内不会占用 CPU 来处理,这时就可以将 CPU 交出给其它线程使用。因此在 I/O 密集型任务的应用中,我们可以多配置一些线程,具体的计算方法是 2N。