线程池简述以及其具体实现
线程池具体参数
- corePoolSize:线程池核心线程数
- maximumePoolSize:线程池最大线程数
- keepAliveTime:空闲线程最大存活时间
- unit:存活时间单位
- threadFactory:线程工厂
- workQueue:任务等待队列
- handler:拒绝策略
四种线程池
- newFixedThreadPool:具有固定线程数的线程池,核心线程数与最大线程数一样,所以空闲线程最大存活时间keepAlive无意义,其任务阻塞队列是无界阻塞队列,所以有可能会造成大量任务阻塞在队列中导致内存溢出。
- newSingleThreadExecutor:核心线程数与最大线程数都是1,只有一个线程可以运行,也就是说使用该线程池的话线程只能顺序运行。
- newCachedThreadPool:缓存线程池,核心线程数为0,最大线程数是INT最大值,所以每当有任务提交都会创建新的线程来执行任务并且所有线程都不是核心线程所以在经过了最大存活时间60s就会被销毁掉,但还是存在内存溢出的风险。 适用场景:适合处理短时间内耗时较短的大量任务,如netty的NIO请求。
- newScheduledThreadPool:具有定时提交任务的功能。
四种拒绝策略:
-
CallerRunsPolicy - 当触发拒绝策略,只要线程池没有关闭的话,则使用调用线程直接运行任务。
使用场景:一般并发比较小,性能要求不高,不允许失败。但是,由于调用者自己运行任务,如果多次提交任务,可能导致程序阻塞,性能效率上必然的损失较大
-
AbortPolicy - 中止策略,抛出拒绝执行 RejectedExecutionException 异常信息。线程池默认的拒绝策略。我们在调用线程处必须处理好抛出的异常,否则会打断当前的执行流程,影响后续的任务执行。
-
DiscardPolicy - 直接丢弃,不执行其他操作
-
DiscardOldestPolicy - 当触发拒绝策略,只要线程池没有关闭的话,丢弃阻塞队列workQueue 中最老的一个任务,并将新任务加入
使用场景:这个策略还是会丢弃任务,丢弃时也是毫无声息,但是特点是丢弃的是老的未执行的任务,而且是待执行优先级较高的任务。基于这个特性,我能想到的场景就是,发布消息,和修改消息,当消息发布出去后,还未执行,此时更新的消息又来了,这个时候未执行的消息的版本比现在提交的消息版本要低就可以被丢弃了。因为队列中还有可能存在消息版本更低的消息会排队执行,所以在真正处理消息的时候一定要做好消息的版本比较
线程池的实现
线程池是通过new ThreadPoolExecutor()来实现的,ThreadPoolExecutor这个类实现与ExecutorService接口,来看下他的方法
public interface ExecutorService extends Executor {
void shutdown();//已经提交的任务继续执行,不接受新任务的提交
List<Runnable> shutdownNow();//立即终止
boolean isShutdown();
boolean isTerminated();
boolean awaitTermination(long timeout, TimeUnit unit)
throws InterruptedException;
<T> Future<T> submit(Callable<T> task);
<T> Future<T> submit(Runnable task, T result);
//提交任务,用于执行有返回值的任务
Future<?> submit(Runnable task);
//所有任务执行完毕,按需返回futureTask对象
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
throws InterruptedException;
//返回第一个执行完毕的任务
<T> T invokeAny(Collection<? extends Callable<T>> tasks)
throws InterruptedException, ExecutionException;
<T> T invokeAny(Collection<? extends Callable<T>> tasks,
long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException;
}
线程池运行底层源码分析
-
首先线程池执行任务是通过它的execute方法或者submit方法,我们重点关注execute方法
public void execute(Runnable command) { if (command == null) throw new NullPointerException(); /* *总结,过程分为4步 *1.如果当前线程数小于核心线程数,创建核心线程 *2.如果核心线程数已满则尝试添加任务到任务队列 *3.如果任务队列也已满则尝试创建一个普通线程 *4.如果以达到最大线程数创建失败则执行拒绝策略 */ int c = ctl.get(); //如果当前线程数少于线程池核心线程数,那么会使用addWorker方法添加一个核心线程,core为true if (workerCountOf(c) < corePoolSize) { if (addWorker(command, true)) return; c = ctl.get(); } //如果当前线程池是运行状态那么尝试将该任务添加进任务队列中 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); } //如果核心线程数以达到上限且任务队列已满则尝试创建一个普通线程,core为false,表是不是核心线程 else if (!addWorker(command, false)) reject(command); } -
创建线程并执行任务的逻辑是在addWorker方法中实现,查看addWorker方法(前半段)
private boolean addWorker(Runnable firstTask, boolean core) { /* * 总结:重点4步 *1.根据core是否为ture来判断创建的线程是否正确 *2.对当前工作线程数使用CAS原语修改 *3.对任务进行封装成worker对象,并用threadFactory创建了一个线程t *4.线程t.start */ retry: for (int c = ctl.get();;) { // Check if queue empty only if necessary. if (runStateAtLeast(c, SHUTDOWN) && (runStateAtLeast(c, STOP) || firstTask != null || workQueue.isEmpty())) return false; for (;;) { //这里workerCountOf(c)表示当前工作线程,如果core为true则跟核心线程数比较,如果core为false则表示创建普通线程则跟最大工作线程比较 if (workerCountOf(c) >= ((core ? corePoolSize : maximumPoolSize) & COUNT_MASK)) return false; //使用CAS原语将当前工作线程数进行修改,修改成功则跳出循环,进入后半段方法 if (compareAndIncrementWorkerCount(c)) break retry; c = ctl.get(); // Re-read ctl if (runStateAtLeast(c, SHUTDOWN)) continue retry; // else CAS failed due to workerCount change; retry inner loop } }boolean workerStarted = false; boolean workerAdded = false; Worker w = null; try { //这里对我们的输入参数firstTask进行了封装,创建了一个worker对象,该对象实现了runnable并继承了AQS队列 //这里封装主要是记录了我们的任务task以及新建了一个线程Thread w = new Worker(firstTask); 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 c = ctl.get(); if (isRunning(c) || (runStateLessThan(c, STOP) && firstTask == null)) { 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; } } finally { mainLock.unlock(); } if (workerAdded) { //这里start了线程,启动了worker对象中的runWorker方法 t.start(); workerStarted = true; } } } finally { if (! workerStarted) addWorkerFailed(w); } return workerStarted; }线程start()方法就是运行了worker对象中的runWorker()方法,查看runWorker()方法
final void runWorker(Worker w) { Thread wt = Thread.currentThread(); //提取该worker对象的任务 Runnable task = w.firstTask; w.firstTask = null; w.unlock(); // allow interrupts boolean completedAbruptly = true; try { 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); try { //在这里执行了任务 task.run(); afterExecute(task, null); } catch (Throwable ex) { afterExecute(task, ex); throw ex; } } finally { task = null; w.completedTasks++; w.unlock(); } } completedAbruptly = false; } finally { processWorkerExit(w, completedAbruptly); } }设计线程池应注意哪些因素?
要想合理的配置线程池,就必须首先分析任务特性,可以从以下几个角度来进行分析:
- 任务的性质:CPU密集型任务,IO密集型任务和混合型任务。
-
任务的优先级:高,中和低。
- 任务的执行时间:长,中和短。
- 任务的依赖性:是否依赖其他系统资源,如数据库连接。
任务性质不同的任务可以用不同规模的线程池分开处理。CPU密集型任务配置尽可能少的线程数量,如配置Ncpu+1个线程的线程池。IO密集型任务则由于需要等待IO操作,线程并不是一直在执行任务,则配置尽可能多的线程,如2*Ncpu。混合型的任务,如果可以拆分,则将其拆分成一个CPU密集型任务和一个IO密集型任务,只要这两个任务执行的时间相差不是太大,那么分解后执行的吞吐率要高于串行执行的吞吐率,如果这两个任务执行时间相差太大,则没必要进行分解。我们可以通过Runtime.getRuntime().availableProcessors()方法获得当前设备的CPU个数。
优先级不同的任务可以使用优先级队列PriorityBlockingQueue来处理。它可以让优先级高的任务先得到执行,需要注意的是如果一直有优先级高的任务提交到队列里,那么优先级低的任务可能永远不能执行。
执行时间不同的任务可以交给不同规模的线程池来处理,或者也可以使用优先级队列,让执行时间短的任务先执行。
依赖数据库连接池的任务,因为线程提交SQL后需要等待数据库返回结果,如果等待的时间越长CPU空闲时间就越长,那么线程数应该设置越大,这样才能更好的利用CPU。
建议使用有界队列,有界队列能增加系统的稳定性和预警能力,可以根据需要设大一点,比如几千。有一次我们组使用的后台任务线程池的队列和线程池全满了,不断的抛出抛弃任务的异常,通过排查发现是数据库出现了问题,导致执行SQL变得非常缓慢,因为后台任务线程池里的任务全是需要向数据库查询和插入数据的,所以导致线程池里的工作线程全部阻塞住,任务积压在线程池里。如果当时我们设置成无界队列,线程池的队列就会越来越多,有可能会撑满内存,导致整个系统不可用,而不只是后台任务出现问题。当然我们的系统所有的任务是用的单独的服务器部署的,而我们使用不同规模的线程池跑不同类型的任务,但是出现这样问题时也会影响到其他任务。
线程池的优点
- 降低资源消耗,频繁创建线程并销毁线程会消耗cpu资源
- 提高响应速度,当任务到达时,可以直接通过线程池里存活的线程来执行任务
- 提高线程可管理性,使用线程池可以对线程的状态进行监控并对线程进行控制分配,调优等
(本文仅用于记录平时个人学习心得,如有误导,恳请指教)