文章目录
- 线程池ThreadPoolExecutor原理
- 核心参数如何设置
- 核心线程数和最大线程数
- 线程空闲时间
- 阻塞队列设置
- 线程池的五种状态
- 原理
- 执行流程
- 拒绝策略
- 线程淘汰机制
线程池ThreadPoolExecutor原理
核心参数如何设置
核心线程数和最大线程数
线程池中线程数量我们一般要区分任务的类型,
-
如果是cpu密集性任务那么线程数一般为cpu核数+1;
// 查看cpu核心数 int cores = Runtime.getRuntime().availableProcessors();
-
如果是IO密集型任务可以按照
cpu核数 * (1 + cpu等待时长/总时长)
cpu的等待时长其实就是除了计算之外io操作耗时,cpu等待时长和任务总时长可以通过
jvisualvm
工具来查看,cpu等待时长 = 总时间 - 总时间(CPU)
当然这只是理论值,实际项目中肯定会存在多个线程池,具体还要通过压测选出较合适的线程数。
当线程数确定后,那么如何设置核心线程数和最大线程数嘞?
其实这就主要看我们要执行的任务是不是核心业务,请求是否频繁。如果是核心业务每秒都有很高的请求那么我们就可以把核心线程数和最大线程数设置一样或者相近。如果不是核心业务,几分钟或者几十分钟才来一些请求,那么核心线程数就没必要设置过大,设置最大线程数一半 或者1/3都行。
线程空闲时间
线程空闲时间没有具体的要求,一般就设置半分钟或者一分钟都行
阻塞队列设置
队列的容量设置多大,主要就是看队列中最后一个任务的等待时长业务是否能够容忍。
我们首先要计算出每个任务的执行耗时,然后再看所有核心线程数去拿队列中的最后一个任务的耗时,业务能否接收,如果能接收那么队列长度的设置就可以。
假如现在核心线程数是10个,每个任务的耗时是1s,阻塞队列的长度是100。那么队列中最后的任务就需要等待9s,然后自己再执行1s。如果业务系统能接受这个耗时那么队列长度就不用缩短。
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(10, 100, 60, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(100));
线程池的五种状态
线程池有五种状态:
- RUNNING:会接收新任务并且会处理队列中的任务
- SHUTDOWN:不会接收新任务并且会处理队列中的任务
- STOP:不会接收新任务并且不会处理队列中的任务,并且会中断在处理的任务(注意:一个任务能不能被中断得看任务本身)
- TIDYING:所有任务都终止了,线程池中也没有线程了,这样线程池的状态就会转为TIDYING,一旦达到此状态,就会调用线程池的terminated()
- TERMINATED:terminated()执行完之后就会转变为TERMINATED
这五种状态并不能任意转换,只会有以下几种转换情况:
- RUNNING -> SHUTDOWN:手动调用shutdown()触发,或者线程池对象GC时会调用finalize()从而调用shutdown()
- (RUNNING or SHUTDOWN) -> STOP:调用shutdownNow()触发,如果先调shutdown()紧着调shutdownNow(),就会发生SHUTDOWN -> STOP
- SHUTDOWN -> TIDYING:队列为空并且线程池中没有线程时自动转换
- STOP -> TIDYING:线程池中没有线程时自动转换(队列中可能还有任务)
- TIDYING -> TERMINATED:terminated()执行完后就会自动转换
原理
执行流程
-
创建线程池时,线程池中是不会创建线程的。除非我们自己再显示调用
prestartAllCoreThreads()
方法才会去创建核心线程。 -
刚开始线程池中是没有线程的,如果来了任务那么就直接去创建线程处理,如果核心线程处理完任务了,但是线程数量还没有达到核心线程数,此时来了任务也还是会去创建新线程处理,直到线程数量达到了核心线程数。
-
线程数 >= 核心线程数后,再来新任务就会直接放入阻塞队列中,并唤醒等待的线程去处理,当队列中没有任务了那么线程就阻塞。
-
假如队列中放满了,那么才会去创建新的线程去处理任务。
-
如果新创建的线程达到了最大线程数量那么就会触发拒绝策略
我们直接看线程池执行任务的源码:
public void execute(Runnable command) {if (command == null)throw new NullPointerException();int c = ctl.get();// 首先看当前线程池数量是否小于核心线程数,如果小于则直接调用addWorker()方法 创建新线程// 这里的addWorker()方法最后一个参数是trueif (workerCountOf(c) < corePoolSize) {if (addWorker(command, true))return;c = ctl.get();}// 核心线程数量达到后,就会调用offer()方法入队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);}// 如果队列满了 入队失败 那么就会调用addWorker()再去创建新线程处理任务// 这里的addWorker()方法最后一个参数是false。在addWorker()方法中最后一个boolean的传参就是判断比较核心线程数还是最大线程数 // 当前线程池工作线程数量 >= (boolean ? corePoolSize : maximumPoolSize)else if (!addWorker(command, false))// 如果达到了最大线程数则触发拒绝策略reject(command);
}
而Tomcat中的线程池对整体流程做了一些改动:
- 创建线程池时就会初始化核心线程
- 来任务后先使用核心线程,核心线程处理不过来时就创建新线程,当达到最大线程数时再入队
拒绝策略
线程池中有四种拒绝策略
- AbortPolicy:抛异常,也是默认的拒绝策略
- CallerRunsPolicy:哪里来的回哪里,比如回main线程。
- DiscardOldestPolicy:满了,会尝试丢弃队列头部第一个,如果第一个没结束,会抛弃任务,但是也不会抛异常。
- DiscardPolicy:会抛弃任务,但是不抛异常。
线程淘汰机制
线程池中的淘汰机制有三种:
- 线程池非核心线程空闲时间超过设定的最大空闲时间
- 执行任务时出异常
- 线程池调用了
shutdown()/shutdownNow()
方法
三种线程淘汰策略接下来就根据源码分析
这里简单介绍一下核心源码,首先从上面的代码中我们可以知道创建线程执行任务是调用的addWorker()
方法
private boolean addWorker(Runnable firstTask, boolean core) {......boolean workerStarted = false;boolean workerAdded = false;Worker w = null;try {// 这里会把我们传入了runnable对象封装为一个Worker对象,Worker也实现了Runnable接口 也是一个线程类w = new Worker(firstTask);// 取出线程 变量tfinal Thread t = w.thread;if (t != null) {final ReentrantLock mainLock = this.mainLock;mainLock.lock();try {int rs = runStateOf(ctl.get());if (rs < SHUTDOWN ||(rs == SHUTDOWN && firstTask == null)) {if (t.isAlive()) // precheck that t is startablethrow new IllegalThreadStateException();workers.add(w);int s = workers.size();if (s > largestPoolSize)largestPoolSize = s;workerAdded = true;}} finally {mainLock.unlock();}if (workerAdded) {// 启动线程t.start();workerStarted = true;}}} finally {if (! workerStarted)addWorkerFailed(w);}return workerStarted;
}
看看Worker
类的源码
private final class Worker extends AbstractQueuedSynchronizer implements Runnable {private static final long serialVersionUID = 6138294804551838833L;/** 上面addWorker()方法中获取的就是这个属性的值 */final Thread thread;/** 这个属性就是我们自定义要执行的任务 */Runnable firstTask;volatile long completedTasks;Worker(Runnable firstTask) {setState(-1); // AQS的statethis.firstTask = firstTask; // 把我们真正要执行的任务赋值给了firstTask属性this.thread = getThreadFactory().newThread(this); // 这里创建了一个新线程,传参是this 也就是当前对象实例}// 所以addWorker()方法中启动线程,实际上会调用这个run()方法public void run() {runWorker(this);}...
}
接下来我们再看看runWorker(this);
方法
final void runWorker(Worker w) {Thread wt = Thread.currentThread();// 这里把我们真正要执行的任务取出来Runnable task = w.firstTask;w.firstTask = null;w.unlock(); boolean completedAbruptly = true;try {// 这是一个循环,表示线程不断的从阻塞队列中拿任务,如果没有任务就阻塞,如果有任务就继续执行任务// 所以,如果getTask()方法返回了一个null,那么也就表示当前线程对象的run()方法要执行完了,那么这个线程对象也就没了while (task != null || (task = getTask()) != null) {w.lock();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 = null;w.completedTasks++;w.unlock();}}// 如果出异常 这行代码也不会执行,值还是true,下面方法会用到这个变量completedAbruptly = false;} finally {// 如果线程执行任务时抛异常了,那么就会跳出上方的循环,进入到这里,这个方法就是会再创建一个新的线程补进线程池中processWorkerExit(w, completedAbruptly);}
}
我们再看看看 getTask()
从阻塞队列中获取任务的方式
private Runnable getTask() {// 标识当前线程是否空闲超过指定时间boolean timedOut = false; // 这是一个死循环for (;;) {int c = ctl.get();int rs = runStateOf(c);// 线程池的状态,如果线程池调用了shutdown()/shutdownNow()方法,那么这里就会满足 然后返回null。// 接着调用getTask()方法的循环也就会跳出,线程池中的线程run()方法也就都执行完了 线程也就都释放了// 不同点是shutdown()是把状态改为SHUTDOWN,也就是说会把队列中的任务执行完才会释放线程。而shutdownNow()是把状态改为STOP,这里就直接满足了if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {decrementWorkerCount();return null;}int wc = workerCountOf(c);// 这里的timed是判断当前工作线程是否大于了核心线程数boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;// 刚开始的循环 timedOut是为false 下面这个if不会满足。 如果工作线程小于了核心线程下面if也不满足// 如果经过下面代码的逻辑 timeOut为true后,那么就会CAS改变当前工作线程 数量-1 因为是CAS操作 只有一个线程会成功,// 返回null,调用getTask()方法的循环也就会跳出,线程池中的线程run()方法也就都执行完了 线程也就都释放了if ( (wc > maximumPoolSize || (timed && timedOut)) && (wc > 1 || workQueue.isEmpty()) ) {if (compareAndDecrementWorkerCount(c))return null;continue;}try {// timed是判断当前工作线程是否大于了核心线程数,如果大于了那么线程就阻塞特定超时时长,时间达到后如果队列中没有任务这里的r=null// 如果小于核心线程数,那么就一直阻塞,直到生产者唤醒// 阻塞队列中,消费者阻塞后,生产者生产后是调用的signal() 而不是signalAll()Runnable r = timed ?workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :workQueue.take();// 拿到了任务就返回if (r != null)return r;// 没有拿到任务就将下面变量置为true,重新进行一次循环timedOut = true;} catch (InterruptedException retry) {timedOut = false;}}
}