一、简介
什么是线程池?
线程池(ThreadPool)是一种基于池化思想管理和使用线程的机制。它是将多个线程预先存储在一个“池子”内,当有任务出现时可以避免重新创建和销毁线程所带来性能开销,只需要从“池子”内取出相应的线程执行对应的任务即可。
池化思想在计算机的应用也比较广泛,比如以下这些:
内存池(Memory Pooling):预先申请内存,提升申请内存速度,减少内存碎片。
连接池(Connection Pooling):预先申请数据库连接,提升申请连接的速度,降低系统的开销。
实例池(Object Pooling):循环使用对象,减少资源在初始化和释放时的昂贵损耗。
线程池的优势主要体现在以下 4 点:
1.降低资源消耗:通过池化技术重复利用已创建的线程,降低线程创建和销毁造成的损耗。
2.提高响应速度:任务到达时,无需等待线程创建即可立即执行。
3.提高线程的可管理性:线程是稀缺资源,如果无限制创建,不仅会消耗系统资源,还会因为线程的不合理分布导致资源调度失衡,降低系统的稳定性。使用线程池可以进行统一的分配、调优和监控。
4.提供更多更强大的功能:线程池具备可拓展性,允许开发人员向其中增加更多的功能。比如延时定时线程池ScheduledThreadPoolExecutor,就允许任务延期执行或定期执行。
线程池的工作机制?
在线程池的编程模式中,任务提交并不是直接提交给线程,而是提交给池。线程池在拿到任务之后,就会寻找有没有空闲的线程,有则分配给空闲线程执行,暂时没有则会进入等待队列,继续等待空闲线程。如果超出最大接受的工作数量,则会触发线程池的拒绝策略。
为什么使用线程池?
线程的创建与销毁需要消耗大量资源,重复的创建与销毁明显不必要。而且池的好处就是响应快,需要的时候自取,就不会存在等待创建的时间。线程池可以很好地管理系统内部的线程,如数量以及调度。
二、线程池实现原理
提交一个任务到线程池中,线程池的处理流程如下:
1、判断线程池里的核心线程是否都在执行任务,如果不是(核心线程空闲或者还有核心线程没有被创建)则创建一个新的工作线程来执行任务。如果核心线程都在执行任务,则进入下个流程。
2、线程池判断工作队列是否已满,如果工作队列没有满,则将新提交的任务存储在这个工作队列里。如果工作队列满了,则进入下个流程。
3、判断线程池里的线程是否都处于工作状态,如果没有,则创建一个新的工作线程来执行任务。如果已经满了,则交给饱和策略来处理这个任务。
三、源码解读
1.线程池的构造方法
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();
}
}
}
线程池的核心参数有如上七个,各个参数意义如下:
corePoolSize:线程池核心线程数量
maximumPoolSize:线程池最大线程数量
keepAliverTime:当活跃线程数大于核心线程数时,空闲的多余线程最大存活时间
unit:存活时间的单位
workQueue:存放任务的队列
handler:超出线程范围和队列容量的任务的处理程序
2.execute()方法
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
//如果线程数大于等于基本线程数或者线程创建失败,将任务加入队列
if (poolSize >= corePoolSize || !addIfUnderCorePoolSize(command)) {
//线程池处于运行状态并且加入队列成功
if (runState == RUNNING && workQueue.offer(command)) {
if (runState != RUNNING || poolSize == 0)
ensureQueuedTaskHandled(command);
}
//线程池不处于运行状态或者加入队列失败,则创建线程(创建的是非核心线程)
else if (!addIfUnderMaximumPoolSize(command))
//创建线程失败,则采取阻塞处理的方式
reject(command); // is shutdown or saturated
}
}
3.线程池中的Worker
线程池为了掌握线程的状态并维护线程的生命周期,设计了线程池内的工作线程Worker。Worker首先继承了AbstractQueuedSynchronizer父类(Java并发编程核心在于 java.concurrent.util 包而juc当中的大多数同步器实现都是围绕着共同的基础行为,比如等待队列、条件队列、独占获取、共享获取等,而这个行为的抽象就是基于 AbstractQueuedSynchronizer 简称AQS,AQS定义了一套多线程访问共享资源的同步器框架,是一个依赖状态(state)的同步器。子类们必须定义改变state变量的protected方法,这些方法定义了state是如何被获取或释放的),并实现了Runnable接口,实现了Runnable接口。
private final class Worker
extends AbstractQueuedSynchronizer
implements Runnable
{
private static final long serialVersionUID = 6138294804551838833L;
// 实际执行任务的线程
final Thread thread;
// 如果当前线程数少于核心线程数,创建线程并将提交的任务交给 worker处理,此时 firstTask 即为此提交的任务,如果 worker 从 workQueue 中获取任务,则 firstTask 为空
Runnable firstTask;
// 统计完成的任务数
volatile long completedTasks;
Worker(Runnable firstTask) {
// 初始化为 -1,这样在线程运行前(调用runWorker)禁止中断,在 interruptIfStarted() 方法中会判断 getState()>=0
setState(-1);
this.firstTask = firstTask;
// 根据线程池的 threadFactory 创建一个线程,将 worker 本身传给线程(因为 worker 实现了 Runnable 接口)
this.thread = getThreadFactory().newThread(this);
}
public void run() {
// thread 启动后会调用此方法
runWorker(this);
}
// 1 代表被锁住了,0 代表未锁
protected boolean isHeldExclusively() {
return getState() != 0;
}
// 尝试获取锁
protected boolean tryAcquire(int unused) {
// 从这里可以看出它是一个独占锁,因为当获取锁后,cas 设置 state 不可能成功,这里我们也能明白上文中将 state 设置为 -1 的作用,这种情况下永远不可能获取得锁,而 worker 要被中断首先必须获取锁
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(); }
// 中断线程,这个方法会被 shutdowNow 调用,从中可以看出 shutdownNow 要中断线程不需要获取锁,也就是说如果线程正在运行,照样会给你中断掉,所以一般来说我们不用 shutdowNow 来中断线程,太粗暴了,中断时线程很可能在执行任务,影响任务执行。
void interruptIfStarted() {
Thread t;
// 中断也是有条件的,必须是 state >= 0 且 t != null 且线程未被中断
// 如果 state == -1 ,不执行中断,再次明白了为啥上文中 setState(-1) 的意义
if (getState() >= 0 && (t = thread) != null && !t.isInterrupted()) {
try {
t.interrupt();
} catch (SecurityException ignore) {
}
}
}
}
总结:通过将线程封装为Worker,可以更好的管理线程和通过锁来中断线程。
4.addWorker
private boolean addWorker(Runnable firstTask, boolean core) {
// 1、retry标记
retry:
for (;;) {
int c = ctl.get();
// 获取线程池的状态
int rs = runStateOf(c);
// 如果线程池的状态 >= SHUTDOWN,即为 SHUTDOWN,STOP,TIDYING,TERMINATED 这四个状态,只有一种情况有可能创建线程,即线程状态为 SHUTDOWN, 且队列非空时,firstTask == null 代表创建一个不接收新任务的线程(此线程会从 workQueue 中获取任务再执行),这种情况下创建线程是为了加速处理完 workQueue 中的任务
if (rs >= SHUTDOWN &&
! (rs == SHUTDOWN &&
firstTask == null &&
! workQueue.isEmpty()))
return false;
for (;;) {
// 获取线程数
int wc = workerCountOf(c);
// 如果超过了线程池的最大 CAPACITY(5 亿多,基本不可能)
// 或者 超过了 corePoolSize(core 为 true) 或者 maximumPoolSize(core 为 false) 时
// 则返回 false
if (wc >= CAPACITY ||
wc >= (core ? corePoolSize : maximumPoolSize))
return false;
// 2、校验通过后,增加线程的数量,如果成功跳出双重循环
if (compareAndIncrementWorkerCount(c))
break retry;
c = ctl.get(); // Re-read ctl
// 如果线程运行状态发生变化,跳到外层循环继续执行
if (runStateOf(c) != rs)
continue retry;
// 说明是因为 CAS 增加线程数量失败所致,继续执行 retry 的内层循环
}
}
boolean workerStarted = false;
boolean workerAdded = false;
Worker w = null;
try {
// 3、创建Worker,然后把线程任务添加进去
w = new Worker(firstTask);
final Thread t = w.thread;
if (t != null) {
// 4、添加锁,是因为下文要把 w 添加进 workers 中, workers 是 HashSet,不是线程安全的,所以需要加锁予以保证
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
// 再次 check 线程池的状态以防执行到此步时发生中断等
int rs = runStateOf(ctl.get());
// 如果线程池状态小于 SHUTDOWN(即为 RUNNING),
// 或者状态为 SHUTDOWN 但 firstTask == null(代表不接收任务,只是创建线程处理 workQueue 中的任务),则满足添加 worker 的条件
if (rs < SHUTDOWN ||
(rs == SHUTDOWN && firstTask == null)) {
// 如果线程已启动,抛出异常,因为线程还没start
if (t.isAlive())
throw new IllegalThreadStateException();
// 将Worker添加到HashSet中
workers.add(w);
int s = workers.size();
// 记录最大的线程池大小以作监控之用
if (s > largestPoolSize)
largestPoolSize = s;
// 设置添加状态
workerAdded = true;
}
} finally {
// 解除独占锁
mainLock.unlock();
}
//5、往 workers 中添加 worker 成功后,则启动线程
if (workerAdded) {
t.start();
workerStarted = true;
}
}
} finally {
// 6、添加线程失败,执行 addWorkerFailed 方法
if (! workerStarted)
addWorkerFailed(w);
}
return workerStarted;
5.Executors
newFixedThreadPool, 可以看到需要传入一个线程数量的参数nThreads,这样线程池的核心线程数和最大线程数都会设成nThreads, 而它的等待队列是一个LinkedBlockingQueue,它的容量限制是Integer.MAX_VALUE, 可以认为是没有边界的。核心线程keepAlive时间0,allowCoreThreadTimeOut默认false。所以这个方法创建的线程池适合能估算出需要多少核心线程数量的场景。
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
newSingleThreadExecutor, 有且只有一个线程在工作,适合任务顺序执行,缺点但是不能充分利用CPU多核性能
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
newCachedThreadPool, 核心线程数0,最大线程数Integer.MAX_VALUE, 线程keepAlive时间60s,用的队列是SynchronousQueue,这种队列本身不会存任务,只做转发,所以newCachedThreadPool适合执行大量的,轻量级任务。
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
newScheduledThreadPool, 执行周期性任务,类似定时器。
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}