规范使用线程池与底层原理详解
什么是线程池
“线程池”顾名思义,就是存放线程的池子,这个池子可以存放多少线程取决于采用哪种线程池,取决于有多少并发线程,有多少计算机的硬件资源。
线程池优势
线程池最主要的工作在于控制运行线程的数量,从而做到线程复用、控制最大并发数量、管理线程。其具体的优势在于:
- 降低资源消耗:通过重复利用已经创建的线程降低线程创建和销毁造成的消耗;
- 提高响应速度:当任务到达时,任务可以不需要等到线程创建就能执行;
- 提高线程的可管理性:线程是稀缺资源,不能无限创建,否则会消耗系统资源、降低系统的稳定性,使用线程可以进行统一分配,调优和监控;
如何创建线程池
线程池继承结构图:
jdk自带创建线程池的四种常见方式:
- Executors.newFixedThreadPool(int):创建一个固定线程数量的线程池,可控制线程最大并发数,超出的线程需要在队列中等待。注意它内部corePoolSize和maximumPoolSize的值(就是第一和第二个参数 nThreads)是相等的,并且使用的是LinkedBlockingQueue:
源码:
public static ExecutorService newFixedThreadPool(int nThreads) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>()); }
- Executors.newSingleThreadExecutor():创建一个单线程的线程池,它只有唯一的线程来执行任务,保证所有任务按照指定顺序执行。注意它内部corePoolSize和maximumPoolSize的值都为1,它使用的是LinkedBlockingQueue:
源码:
public static ExecutorService newSingleThreadExecutor() { return new FinalizableDelegatedExecutorService (new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>())); }
- Executors.newCachedThreadPool():创建一个可缓存的线程池,如果线程长度超过处理需要,可灵活回收空闲线程,若无可回收线程,则创建新线程。注意它内部将corePoolSize值设为0,maximumPoolSize值设置为Integer.MAX_VALUE,并且使用的是SynchronizedQueue,keepAliveTime值为60,即当线程空闲时间超过60秒,就销毁线程:
源码:
public static ExecutorService newCachedThreadPool() { return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>()); }
- Executors.newScheduledThreadPool(int):创建一个固定线程数量的线程池,相比于newFixedThreadPool(int)固定个数的线程池强大在 ①可以执行延时任务,②也可以执行带有返回值的任务,并且使用的是DelayedWorkQueue:
源码:
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) { return new ScheduledThreadPoolExecutor(corePoolSize); } | | | | | | | | | | | | V V V V V V V V V V V V public ScheduledThreadPoolExecutor(int corePoolSize) { super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS, | | new DelayedWorkQueue()); } | | V V //ScheduledThreadPoolExecutor继承了ThreadPoolExecutor public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) { this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, Executors.defaultThreadFactory(), defaultHandler); }
注意:
- 以上四种创建线程的方式内部都是由ThreadPoolExecutor这个类完成的,该类的构造方法有5个参数,称为线程池的5大参数(还有另外两个参数);
- 线程池使用完毕之后需要关闭,应该配合try-finally代码块,将线程池关闭的代码放在finally代码块中;
线程池的7大参数
ThreadPoolExecutor对构造函数进行了重载,实际内部使用了7个参数:
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) {
..............//已省略 参数合法校验 ..............//已省略 参数赋值 }
- corePoolSize:线程池中常驻核心线程池(当线程池中的线程数目达到了corePoolSize后,就会把任务放到缓存队列中;)
- maximumPoolSize:线程池中能够容纳同时执行最大线程数,该值必须大于等于1
- keepAliveTime:多余线程的最大存活时间
- unit:keepAliveTime的单位
- workQueue:任务队列,被提交但尚未被执行的任务(阻塞队列)
- threadFactory:生成线程池中工作线程的线程工厂,一般使用默认即可
- handler:拒绝策略,表示当任务队列满并且工作线程大于等于线程池的最大线程数时,对即将到来的线程的拒绝策略
线程池底层原理
线程池具体工作流程:
- 在创建线程后,等待提交过来的任务请求
- 当调用execute()/submit()方法添加一个请求任务时,线程池会做出以下判断:
- 如果正在运行的线程数量小于corePoolSize,会立刻创建线程运行该任务
- 如果正在运行的线程数量大于等于corePoolSize,会将该任务放入阻塞队列中
- 如果队列也满但是正在运行的线程数量小于maximumPoolSize,线程池会进行拓展
- 将线程池中的线程数拓展到最大线程数
- 如果队列满并且运行的线程数量大于等于maximumPoolSize,那么线程池会启动相应的拒绝策略来拒绝相应的任务请求
- 当一个线程完成任务时,它会从队列中取下一个任务来执行
- 当一个线程空闲时间超过给定的keepAliveTime时,线程会做出判断:
- 如果当前运行线程大于corePoolSize,那么该线程将会被停止。也就是说,当线程池的所有任务都完成之后,它会收缩到corePoolSize的大小
线程池的拒绝策略
当线程池的阻塞队列满了同时线程池中线程数量达到了最大maximumPoolSize时,线程池将会启动相应的拒绝策略来拒绝请求任务。
4种拒绝策略具体为:
- AbortPolicy(默认):直接抛出RejectedExecutionException异常阻止系统正常运行
- CallerRunsPolicy:调用者运行的一种机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者
- DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加入到队列中尝试再次提交当前任务
- DiscardPolicy:直接丢弃任务,不予任何处理也不抛出异常。如果任务允许丢失,那么该策略是最好的方案
注意:以上4种拒绝策略均实现了RejectedExecutionHandler接口
规范创建线程池
实际开发中不允许使用内置的线程池:必须明确地通过ThreadPoolExecutor方式,指定相应的线程池参数创建自定义线程或者使用其它框架提供的线程池。因为内置线程池的第五个参数阻塞队列允许的请求队列长度为 Integer.MAX_VALUE(从上面的源码上可以看出),可能造成大量请求堆积,导致OOM:
阿里巴巴规范中指出不能使用Executors去创建:
自定义线程池:使用不同的拒绝策略:
package com.raicho.mianshi.threadpool; import java.util.concurrent.ExecutorService; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; /** * @author: Raicho * @Description: 自定义线程池的各个参数 * @program: mianshi * @create: 2020-08-12 10:44 **/ public class CustomThreadPool { public static void main(String[] args) { ExecutorService executorService = new ThreadPoolExecutor( 2, 5, 1L, TimeUnit.SECONDS, new LinkedBlockingQueue<>(3), // new ThreadPoolExecutor.AbortPolicy() new ThreadPoolExecutor.CallerRunsPolicy() // 注意使用该拒绝策略,可能会回退给main线程执行 // new ThreadPoolExecutor.DiscardOldestPolicy() //new ThreadPoolExecutor.DiscardPolicy() ); try { for (int i = 0; i < 9; i++) { executorService.submit(() -> { System.out.println(Thread.currentThread().getName() + ": 执行任务"); try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } }); } } catch (Exception e) { e.printStackTrace(); } finally { executorService.shutdown(); } } }
线程池配置合理线程数量
线程池合理配置线程数量需要考虑业务具体是CPU密集型还是IO密集型:
- CPU密集型:该任务需要大量运算,而没有阻塞,CPU一直在全速运行,CPU密集型只有在真正的多核CPU上才能进行加速。
CPU密集型任务配置应该尽可能少的线程数量,一般公式为:
CPU核数 + 1个线程的线程池
- IO密集型:任务需要大量的IO操作,即大量的阻塞。在单线程上进行IO密集型的任务会浪费大量的CPU运算能力在等待操作上。
所以在IO密集型任务中使用多线程可以大大加速程序运行:
CPU核数 / (1 - 阻塞系数) 阻塞系数在0.8-0.9 CPU核数 * 2