一、线程池的简介
我们前面学习的线程的基本知识、知道线程的创建,就我们开发和数据库的连接而言,如果每次对数据库的请求,我们都创建一个线程响应连接,那如果在并发量大的情况下,每个请求到达就创建一个新线程,创建和销毁线程花费的时间和消耗的系统资源都相当大,甚至可能要比在处理实际的用户请求的时间和资源要多的多。如果在一个 Jvm 里创建太多的线程,可能会使系统由于过度消耗内存或“切换过度”而导致系统资源不足。为了解决这个问题,就有了线程池的概念
A、什么是线程池
线程池的核心逻辑是提前创建好若干个线程放在一个容器中。如果有任务需要处理,则将任务直接分配给线程池中的线程来执行就行,任务处理完以后这个线程不会被销毁,而是等待后续分配任务。同时通过线程池来重复管理线程还可以避免创建大量线程增加开销。、
B、线程池的优势
- 降低创建线程和销毁线程的性能开销
- 提高响应速度,当有新任务需要执行是不需要等待线程创建就可以立马执行
- 合理的设置线程池大小可以避免因为线程数超过硬件资源瓶颈带来的问题
二、java线程池API
在日常开发过程中,大家或多或少的都用过线程池,对如何创建一个线程池并不陌生,但是要想合理的使用线程池,那么势必要对线程池的原理有比较深的理解。
为了方便大家对于线程池的使用,在 Executors 里面提供了几个线程池的工厂方法,这样,很多新手就不需要了解太多关于 ThreadPoolExecutor 的知识了,他们只需要直接使用Executors 的工厂方法,就可以使用线程池:
newFixedThreadPool:该方法返回一个固定数量的线程池,线程数不变,当有一个任务提交时,若线程池中空闲,则立即执行,若没有,则会被暂缓在一个任务队列中,等待有空闲的线程去执行。
newSingleThreadExecutor: 创建一个线程的线程池,若空闲则执行,若没有空闲线程则暂缓在任务队列中。
newCachedThreadPool:返回一个可根据实际情况调整线程个数的线程池,不限制最大线程数量,若用空闲的线程则执行任务,若无任务则不创建线程。并且每一个空闲线程会在 60 秒后自动回收
newScheduledThreadPool: 创建一个可以指定线程的数量的线程池,但是这个线程池还带有延迟和周期性执行任务的功能,类似定时器。
三、 ThreadPoolExecutor
其实上面提到的四种线程池的构建,都是基于 ThreadPoolExecutor 来构建的,接下来将一起了解一下面试官最喜欢问到的一道面试题:
“请简单说下你知道的创建线程池和ThreadPoolExecutor 有哪些构造参数”
ThreadPoolExecutor 有多个重载的构造方法,我们可以基于它最完整的构造方法来分析,先来解释一下每个参数的作用,稍后我们在分析源码的过程中再来详细了解参数的意义
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;
}
通过上述构造方法发现,线程池初始化时是没有创建线程的,线程池里的线程的初始化与其他线程一样,但是在完成任务以后,该线程不会自行销毁,而是以挂起的状态返回到线程池。直到应用程序再次向线程池发出请求时,线程池里挂起的线程就会再度激活执行任务。这样既节省了建立线程所造成的性能损耗,也可以让多个任务反复重用同一线程,从而在应用程序生存期内节约大量开
销
newFixedThreadPool
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
newSingleThreadExecutor 的核心线程数和最大线程数都是指定值,也就是说当线程池中的线程数超过核心线程数后,任务都会被放到阻塞队列中。另外 keepAliveTime 为 0,也就是超出核心线程数量以外的线程空余存活时间
而这里选用的阻塞队列是 LinkedBlockingQueue,使用的是默认容量 Integer.MAX_VALUE,相当于没有上限
这个线程池执行任务的流程如下:
- 线程数少于核心线程数,也就是设置的线程数时,新建线程执行任务
- 线程数等于核心线程数后,将任务加入阻塞队列
- 由于队列容量非常大,可以一直添加
- 执行完任务的线程反复去队列中取任务执行
用途:FixedThreadPool 用于负载比较大的服务器,为了资源的合理利用,需要限制当前线程数量
newCachedThreadPool
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
newCachedThreadPool 创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程; 并且没有核心线程,非核心线程数无上限,但是每个空闲的时间只有 60 秒,超过后就会被回收。
它的执行流程如下:
- 没有核心线程,直接向 SynchronousQueue 中提交任务
- 如果有空闲线程,就去取出任务执行;如果没有空闲线程,就新建一个
- 执行完任务的线程有 60 秒生存时间,如果在这个时间内可以接到新任务,就可以继续活下去,否则就被回收
newSingleThreadExecutor
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory));
}
创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行
四、线程池的执行原理
线程池的各个参数含义和基本使用我们了解清楚了,接下来我们来了解一下线程池的实现原理。
我们把一个任务提交给线程池去处理的时候,线程池的处理过程是什么样的呢?
五、线程池实例
定义一个线程
public class ThreadDemo extends Thread {
@Override
public void run() {
System.out.println("线程"+Thread.currentThread().getName()+"执行");
}
}
线程池调用
public class Demo {
public static void main(String[] args) {
// 定义一个核心线程数为5,最大线程数为10,回收闲置时间为60秒,阻塞队列容量为5的线程池
ThreadPoolExecutor executor = new ThreadPoolExecutor(
5,10,60, TimeUnit.SECONDS,
new ArrayBlockingQueue(5));
for(int i = 0;i<100;i++){
ThreadDemo thread = new ThreadDemo();
executor.execute(thread);
}
}
}
打印结果
线程pool-1-thread-1执行
线程pool-1-thread-4执行
线程pool-1-thread-3执行
线程pool-1-thread-2执行
线程pool-1-thread-1执行
线程pool-1-thread-4执行
线程pool-1-thread-4执行
线程pool-1-thread-4执行
线程pool-1-thread-7执行
线程pool-1-thread-4执行
线程pool-1-thread-4执行
线程pool-1-thread-4执行
线程pool-1-thread-5执行
线程pool-1-thread-5执行
线程pool-1-thread-5执行
线程pool-1-thread-5执行
线程pool-1-thread-5执行
线程pool-1-thread-5执行
线程pool-1-thread-5执行
线程pool-1-thread-5执行
线程pool-1-thread-5执行
线程pool-1-thread-5执行
线程pool-1-thread-5执行
线程pool-1-thread-5执行
线程pool-1-thread-5执行
线程pool-1-thread-5执行
线程pool-1-thread-7执行
线程pool-1-thread-2执行
线程pool-1-thread-2执行
线程pool-1-thread-2执行
线程pool-1-thread-2执行
线程pool-1-thread-2执行
线程pool-1-thread-2执行
线程pool-1-thread-3执行
线程pool-1-thread-6执行
线程pool-1-thread-8执行
线程pool-1-thread-10执行
线程pool-1-thread-1执行
线程pool-1-thread-4执行
线程pool-1-thread-9执行
Exception in thread "main" java.util.concurrent.RejectedExecutionException: Task Thread[Thread-40,5,main] rejected from java.util.concurrent.ThreadPoolExecutor@682a0b20[Running, pool size = 10, active threads = 0, queued tasks = 0, completed tasks = 40]
at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2047)
at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:823)
at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1369)
at com.xufk.thread.example.excutors.Demo.main(Demo.java:17)
六、源码分析
在分析源码之前,我们先看一下ThreadPoolExecutor源码的几个参数
1.ctl的作用
你会发现这个参数贯穿在源码各个地方,那这个参数是什么呢
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING,0));
private static int ctlOf(int rs, int wc) { return rs | wc; }
它是一个原子类,主要作用是用来保存线程数量和线程池的状态
我们来分析一下上面这段代码,其实比较有意思,他用到了位运算。一个 int 数值是 32 个 bit 位,这里采用高 3 位来保存运行状态,低 29 位来保存线程数量。
其中 RUNNING =-1 << COUNT_BITS ; -1 左移 29 位. -1 的二进制是 32 个 1(1111 1111 1111 1111 1111 1111 1111 1111)
负数是以二进制补码的形式存储的,所以 -1的二进制计算方法 :
原码是 1000…001 . 高位 1 表示符号位。 然后对原码取反,高位不变得到 1111…110 然后对反码进行+1 ,也就是补码操作, 最后得到 1111…1111 那么-1 <<左移 29 位, 也就是 【111】 表示; rs | wc 。二进制的 111 | 000 。得到的结 果仍然是 111
那么同理可得其他的状态的 bit 位表示
//COUNT_BITS = 32-3
private static final int COUNT_BITS = Integer.SIZE - 3;
//将 1 的二进制向右位移 29 位,再减 1 表示最大线程容量
private static final int CAPACITY = (1 << COUNT_BITS) - 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;
// 所有的任务都已结束,线程数量为 0,处于该状态的线程池即将调用 terminated()方法
private static final int TIDYING = 2 << COUNT_BITS;
// terminated()方法执行完成
private static final int TERMINATED = 3 << COUNT_BITS;
// 解析线程状态
private static int runStateOf(int c) { return c & ~CAPACITY; }
// 解析线程池容量
private static int workerCountOf(int c) { return c & CAPACITY; }
涉及到的状态转化可以通过下图来理解
2.worker
这是一个内部类,他继承了AQS和Runnable,所以线程池的线程其实是放在这里面的,每个 worker,都是一条线程,理解了这个内部类对理解源码有很重要的作用
private final class Worker
extends AbstractQueuedSynchronizer
implements Runnable
{
/**
* This class will never be serialized, but we provide a
* serialVersionUID to suppress a javac warning.
*/
private static final long serialVersionUID = 6138294804551838833L;
//thread 是在调用构造方法时通过 ThreadFactory 来创建的线程,是用来处理任务的线程
final Thread thread;
// firstTask 用它来保存传入的任务
Runnable firstTask;
// 完成的任务数,用于线程池统计
volatile long completedTasks;
// 构造方法
// 通过 getThreadFactory().newThread(this);来新建一个线程,newThread 方法传入的参数是 this,因为 Worker 本身继承了 Runnable 接口,也就是一个线程,所以一个 Worker 对象在启动的时候会调用 Worker 类中的 run 方法。
Worker(Runnable firstTask) {
// 初始状态 -1, 防止在调用 runWorker() ,也就是真正执行 task前中断 thread 。
setState(-1);
this.firstTask = firstTask;
this.thread = getThreadFactory().newThread(this);
}
/** Delegates main run loop to outer runWorker */
public void run() {
// 最终执行任务的,是 runWorker()方法
runWorker(this);
}
// Lock methods
//
// The value 0 represents the unlocked state.
// The value 1 represents the locked state.
protected boolean isHeldExclusively() {
return getState() != 0;
}
//Worker 继承了 AQS,使用 AQS 来实现独占锁的功能。为什么不使用 ReentrantLock 来实现呢?可以看到 tryAcquire 方法,它是不允许重入的,而 ReentrantLock 是允许重入的:
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;
}
/** lock 方法一旦获取了独占锁,表示当前线程正在执行任务中;那么它会有以下几个作用
1. 如果正在执行任务,则不应该中断线程;
2. 如果该线程现在不是独占锁的状态,也就是空闲的状态,说明它没有在处理任务,这时可以对该线程进行中断;
3. 线程池在执行 shutdown 方法或 tryTerminate 方法时会调用 interruptIdleWorkers 方法来中断空闲的线程,interruptIdleWorkers 方法会使用 tryLock 方法来判断线程池中的线程是否是空闲状态
4. 之所以设置为不可重入,是因为我们不希望任务在调用像setCorePoolSize 这样的线程池控制方法时重新获取锁,这样会中断正在运行的线程
*/
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) {
}
}
}
}
3.execute方法
在上面分析ThreadPoolExecutor的构造方法参数的时候发现,创建线程池的时候并没有创建线程,那我们从线程池的执行方法excute()方法看
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);
}
4.addWorker方法
当工作线程数小于核心线程数,调用此方法,此方法作用
1)采用循环 CAS 操作来将线程数加 1
2)新建一个线程并启用
private boolean addWorker(Runnable firstTask, boolean core) {
retry: //goto 语句,避免死循环
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);
// Check if queue empty only if necessary.
/**
如果线程处于非运行状态,并且 rs 不等于 SHUTDOWN 且firstTask 不等于空且且workQueue 为空,直接返回 false (表示不可添加 work 状态)
1. 线程池已经 shutdown 后,还要添加新的任务,拒绝
2. (第二个判断) SHUTDOWN 状态不接受新任务,但仍然会执行已经加入任务队列的任务,所以当进入 SHUTDOWN 状态,而传进来的任务为空,并且任务队列不为空的时候,是允许添加
*/
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 来增加工作线程数,如果 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 {
//构建一个 Worker对象
w = new Worker(firstTask);
//从 worker 对象中取出线程
final Thread t = w.thread;
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();
//将新创建的 Worker 添加到 workers 集合中
// private final HashSet<Worker> workers = new HashSet<Worker>();这个Set集合可以说是线程池
workers.add(w);
int s = workers.size();
//如果集合中的工作线程数大于最大线程数,这个最大线程数表示线程池曾经出现过的最大线程数
if (s > largestPoolSize)
largestPoolSize = s;//更新线程池出现过的最大线程数
//表示工作线程创建成功了
workerAdded = true;
}
} finally {
mainLock.unlock();
}
if (workerAdded) {//如果 worker 添加成功
t.start();//启动线程,这个里调用的是Worker类的run方法,然后调用runWorker方法
workerStarted = true;// 更改启动线程成功标识
}
}
} finally {
if (! workerStarted)
/**
addWorker 方法中,如果添加 Worker 并且启动线程失败,则会做失败后的处理。这个方法主要做两件事
1. 如果 worker 已经构造好了,则从 workers 集合中移除这个 worker
2. 原子递减核心线程数(因为在 addWorker 方法中先做了原子增加)
3. 尝试结束线程池
*/
addWorkerFailed(w);
}
return workerStarted;
}
5.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) {
//上锁,不是为了防止并发执行任务,为了在 shutdown()时不终止正在运行的 worker
//线程池为 stop 状态时不接受新任务,不执行已经加入任务队列的任务,还中断正在执行的任务
// 所以对于 stop 状态以上是要中断线程的
//(Thread.interrupted() &&runStateAtLeast(ctl.get(), STOP) 确保线程中断标志位为 true 且是 stop 状态以上,接着清除了中断标志
//!wt.isInterrupted() 则再一次检查保证线程需要设置中断标志位
w.lock();
if ((runStateAtLeast(ctl.get(), STOP) ||
(Thread.interrupted() &&
runStateAtLeast(ctl.get(), STOP))) &&
!wt.isInterrupted())
wt.interrupt();
try {
// 执行前的动作 这里默认是没有实现的,在一些特定的场景中我们可以自己继承 ThreadPoolExecutor 自己重写
beforeExecute(wt, task);
Throwable thrown = null;
try {
//执行任务中的 run 方法,这个就是执行你自己写的线程了,他不是用start方法启动线程执行,而是直接调用的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 {
// 执行任务后置的动作 这里默认是没有实现的,在一些特定的场景中我们可以自己继承 ThreadPoolExecutor 自己重写
afterExecute(task, thrown);
}
} finally {
//置空任务(这样下次循环开始时,task 依然为 null,需要再通过 getTask()取) + 记录该 Worker 完成任务数量 + 解锁
task = null;
w.completedTasks++;
w.unlock();
}
}
completedAbruptly = false;
} finally {
processWorkerExit(w, completedAbruptly);
}
}
6.getTask方法
worker 线程会从阻塞队列中获取需要执行的任务
private Runnable getTask() {
boolean timedOut = false; // Did the last poll() time out?
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);
/** 对线程池状态的判断,两种情况会 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);
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
//1. 线程数量超过 maximumPoolSize 可能是线程池在运行时被调用了 setMaximumPoolSize()被改变了大小,否则已经 addWorker()成功不会超过 maximumPoolSize
//2. timed && timedOut 如果为 true,表示当前操作需要进行超时控制,并且上次从阻塞队列中获取任务发生了超时.其实就是体现了空闲线程的存活时间
/**
这里是重要的地方,目的是控制线程池的有效线程数量。由上文中的分析可以知道,在执行 execute 方法时,如果当前线程池的线程数量超过了 corePoolSize 且小于maximumPoolSize,并且 workQueue 已满时,则可以增加工作线程,但这时如果超时没有获取到任务,也就是 timedOut 为 true 的情况,说明 workQueue 已经为空了,也就说明了当前线程池中不需要那么多线程来执行任务了,可以把多于 corePoolSize 数量的线程销毁掉,保持线程数量在corePoolSize 即可。
什么时候会销毁?
当然是 runWorker 方法执行完之后,也就是 Worker 中的 run 方法执行完,由 JVM 自动回收。
getTask 方法返回 null 时,在 runWorker 方法中会跳出 while 循环,然后会执行processWorkerExit 方法
*/
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;
}
}
}
7.processWorkerExit
runWorker 的 while 循环执行完毕以后,在 finally 中会调用processWorkerExit, 来销毁工作线程。
七、线程池的注意事项
1.阿里开发手册不建议使用Executors创建线程池
大家如果看过阿里代码开发规范手册一定会发现手册里面关于线程池的要求中明确指出**线程池的构建不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式。**那为什么有这样的要求呢?
简单解释一下:用 Executors 使得用户可以不需要关心线程池的参数配置,意味着大家对于线程池的运行规则也会慢慢的忽略。这会导致一个问题,比如我们用 newFixdThreadPool 或者 singleThreadPool.允许的队列长度为Integer.MAX_VALUE,如果使用不当会导致大量请求堆积到队列中导致 OOM 的风险。而 newCachedThreadPool,允许创建线程数量为 Integer.MAX_VALUE,也可能会导致大量线程的创建出现 CPU 使用过高或者 OOM 的问题。而如果我们通过 ThreadPoolExecutor 来构造线程池的话,我们势必要了解线程池构造中每个参数的具体含义,使得开发者在配置参数的时候能够更加谨慎
2.如何合理配置线程池的大小
线程池大小不是靠猜,也不是说越多越好。在遇到这类问题时,先冷静下来分析
- 需要分析线程池执行的任务的特性: CPU 密集型还是 IO 密集型
- 每个任务执行的平均时长大概是多少,这个任务的执行时长可能还跟任务处理逻辑是否涉及到网络传输以及底层系统资源依赖有关系
如果是 CPU 密集型,主要是执行计算任务,响应时间很快,cpu 一直在运行,这种任务 cpu的利用率很高,那么线程数的配置应该根据 CPU 核心数来决定,CPU 核心数=最大同时执行线程数,假如 CPU 核心数为 4,那么服务器最多能同时执行 4 个线程。过多的线程会导致上下文切换反而使得效率降低。那线程池的最大线程数可以配置为 cpu 核心数+1
如果是 IO 密集型,主要是进行 IO 操作,执行 IO 操作的时间较长,这时 cpu 处于空闲状态,导致 cpu 的利用率不高,这种情况下可以增加线程池的大小。这种情况下可以结合线程的等待时长来做判断,等待时间越高,那么线程数也相对越多。一般可以配置 cpu 核心数的 2 倍。
3.线程池的初始化
默认情况下,创建线程池之后,线程池中是没有线程的,需要提交任务后才会创建线程。
在实际中如果需要线程池创建之后立即创建线程,可以通过以下两个方法办到:
prestartCoreThread():初始化一个核心线程; prestartAllCoreThreads():初始化所有核心线程
例如:
ThreadPoolExecutor tpe=(ThreadPoolExecutor)service;
tpe.prestartAllCoreThreads();
4.线程池的关闭
ThreadPoolExecutor 提供了两个方法,用于线程池的关闭,分别是 shutdown()和shutdownNow(),其中:
shutdown():不会立即终止线程池,而是要等所有任务缓存队列中的任务都执行完后才终止,但再也不会接受新的任务
shutdownNow():立即终止线程池,并尝试打断正在执行的任务,并且清空任务缓存队列,返回尚未执行的任务
5. 线程池容量的动态调整
ThreadPoolExecutor 提供了动态调整线程池容量大小的方法: setCorePoolSize()和setMaximumPoolSize(),
setCorePoolSize:设置核心池大小
setMaximumPoolSize:设置线程池最大能创建的线程数目大小
6.任务缓存队列及排队策略
在前面我们多次提到了任务缓存队列,即 workQueue,它用来存放等待执行的任务。workQueue 的类型为 BlockingQueue,通常可以取下面三种类型:
- ArrayBlockingQueue:基于数组的先进先出队列,此队列创建时必须指定大小;
- LinkedBlockingQueue:基于链表的先进先出队列,如果创建时没有指定此队列大小,则默认为 Integer.MAX_VALUE;
- SynchronousQueue:这个队列比较特殊,它不会保存提交的任务,而是将直接新建一个线程来执行新来的任务。
7.拒绝策略
1、AbortPolicy:直接抛出异常,默认策略;
2、CallerRunsPolicy:用调用者所在的线程来执行任务;
3、DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务;
4、DiscardPolicy:直接丢弃任务;
5、可以根据应用场景实现 RejectedExecutionHandler 接口,自定义饱和策略
8.线程池监控
线程池提供了相应的扩展方法,我们通过重写线程池的 beforeExecute、afterExecute 和 shutdown 等方法就可以实现对线程的监控,示例如下:
定义一个类继承ThreadPoolExecutor ,重写 beforeExecute、afterExecute 和 shutdown 等方法
public class Demo1 extends ThreadPoolExecutor {
// 保存任务开始执行的时间 , 当任务结束时 , 用任务结束时间减去开始时间计算任务执行时间
private ConcurrentHashMap<String, Date> startTimes;
public Demo1(int corePoolSize, int maximumPoolSize, long
keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> 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 = 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 < 10; i++) {
es.execute(new Test());
}
es.shutdown();
}
}
打印效果:
任务耗时:1000
初始线程数:10
核心线程数:0
正在执行的任务数量:10
已经执行的任务数:0
任务总数:10
最大允许的线程数:2147483647
线程空闲时间:60000
任务耗时:1002
初始线程数:9
核心线程数:0
正在执行的任务数量:9
已经执行的任务数:1
任务总数:10
最大允许的线程数:2147483647
线程空闲时间:60000
任务耗时:1002
初始线程数:8
核心线程数:0
正在执行的任务数量:8
已经执行的任务数:2
任务总数:10
最大允许的线程数:2147483647
线程空闲时间:60000
任务耗时:1006
初始线程数:7
核心线程数:0
任务耗时:1006
初始线程数:7
核心线程数:0
正在执行的任务数量:7
已经执行的任务数:3
任务总数:10
最大允许的线程数:2147483647
线程空闲时间:60000
任务耗时:1006
初始线程数:6
核心线程数:0
正在执行的任务数量:6
已经执行的任务数:4
任务总数:10
最大允许的线程数:2147483647
线程空闲时间:60000
正在执行的任务数量:7
任务耗时:1006
初始线程数:5
核心线程数:0
正在执行的任务数量:5
已经执行的任务数:5
任务总数:10
最大允许的线程数:2147483647
线程空闲时间:60000
任务耗时:1006
初始线程数:4
核心线程数:0
正在执行的任务数量:4
已经执行的任务数:6
任务总数:10
最大允许的线程数:2147483647
线程空闲时间:60000
已经执行的任务数:5
任务总数:10
最大允许的线程数:2147483647
线程空闲时间:60000
任务耗时:1036
初始线程数:2
核心线程数:0
正在执行的任务数量:2
已经执行的任务数:8
任务总数:10
最大允许的线程数:2147483647
线程空闲时间:60000
任务耗时:1036
初始线程数:1
核心线程数:0
正在执行的任务数量:1
已经执行的任务数:9
任务总数:10
最大允许的线程数:2147483647
线程空闲时间:60000
八、总结
线程池的原理一定要搞清楚,汇总成下面一幅图