前言
本文提要:介绍线程池,了解线程池的参数及使用,深入理解线程池工作原理
学习java,JUC是绕不过去的一部分内容,JUC也就是java.util.concurrent包,为开发者提供了大量高效的线程并发工具,方便我们可以开发出更高性能的代码。而juc其实涉及到的内容有很多,主要包含以下部分:
线程池
并发集合
同步器
原子变量
锁
并发工具类
今天我们就来讲其中比较核心和常用的一个内容——线程池
一、线程池是什么
线程池是一种线程使用模式,我们都知道线程的创建与销毁会消耗资源,为了减少无谓的消耗而引入了池化技术。而线程池、连接池、对象池等池化技术都有一个共同的特征:重复利用。换句话说,存放在线程池里的线程在执行完一个任务后,可能还会存活,等待着下一次执行任务,这样就避免每一个任务都需要进行一次线程的创建和销毁。
二、管理线程池
1. 线程池种类
我们可以看到预置的线程池有三种:
- ThreadPoolExecutor
- 最常见的线程池
- ScheduledThreadPoolExecutor
- 定时线程池,主要用于执行周期性任务
- ForkJoinPool
拆分合并线程池,把一个大任务切分为若干个子任务并行地执行,最后合并得到这个大任务的结果
更细分的每种线程池也有预置的构建方法,这些构建方法在Executors类下,一般情况下,我们创建线程池会使用这些预置的构建方法来获取
2. 线程池参数
线程池不单单有存放线程的作用,还具备“管理”的作用,比如我们的任务提交给线程池,线程池会帮我们找到合适的线程来执行;又比如没有任务或任务太多时,线程池也会自动“休息”或“满负荷运行”。这些管理功能需要我们进行参数的配置,不同配置出来的线程池自然效果不同
可以看到,我们最常用的线程池类就是ThreadPoolExecutor,而它的构造方法支持7个参数,这也就是所谓的线程池的七大参数:
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) (1)corePoolSize:线程池中常驻核心线程数 (2)maximumPoolSize:线程池能够容纳同时执行的最大线程数 (3)keepAliveTime:多余的空闲线程存活时间 (4)unit:keepAliveTime的时间单位 (5)workQueue:任务队列,被提交但尚未执行的任务 (6)threadFactory:表示生成线程池中的工作线程的线程工厂 (7)handler:拒绝策略,表示当队列满了并且工作线程大于等于线程池的最大线程数(maximumPoolSize)时如何拒绝
我们先知道这七大参数的意思即可,更具体的影响我们会在后面原理阶段细说。
3. 创建线程池
我们前面说了,Executors类下为我们预置了不同种类线程池的预建方法,我们简单介绍下Executors类下常用的五种线程池
newSingleThreadExecutor (单线程化)
// 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务, // 保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。 public static ExecutorService newSingleThreadExecutor() { return new FinalizableDelegatedExecutorService (new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>())); }
newFixedThreadPool (定长)
// 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。 // 定长线程池;适用于执行负载重,cpu使用频率高的任务; // 这个主要是为了防止太多线程进行大量的线程频繁切换,得不偿失; // 比如同时很多人进行商品秒杀。 public static ExecutorService newFixedThreadPool(int nThreads) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>()); }
newScheduledThreadPool (可定期)
// 创建一个可定期或者延时执行任务的定长线程池,支持定时及周期性任务执行 public ScheduledThreadPoolExecutor(int corePoolSize) { super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS, new DelayedWorkQueue()); }
newCachedThreadPool (可缓存)
// 创建一个可缓存线程池,如果线程池长度超过处理需要, // 可灵活回收空闲线程,若无可回收,则新建线程 // 适用于执行大量(并发)短期异步的任务;注意,任务量的负载要轻; // 比如同时给很多人发送从磁盘读取的消息通知 public static ExecutorService newCachedThreadPool() { return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>()); }
newWorkStealingPool(分治-任务窃取)
// 分治线程池,线程之间会窃取任务来执行 public static ExecutorService newWorkStealingPool() { return new ForkJoinPool (Runtime.getRuntime().availableProcessors(), ForkJoinPool.defaultForkJoinWorkerThreadFactory, null, true); }
但是使用预置的方式创建线程池并非没有弊端,比如
newFixedThreadPool和newSingleThreadExecutor,主要问题是使用的无界队列,堆积的请求处理队列可能会耗费非常大的内存,甚至OOM。
newCachedThreadPool和newScheduledThreadPool设置的线程数最大数是Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至OOM。
基于这种考虑,所以很多公司的代码规范会强制要求程序员手动创建线程池,如下例子
ThreadPoolExecutor tp = new ThreadPoolExecutor(3, 5, 2000L, TimeUnit.SECONDS, new ArrayBlockingQueue<>(200), new ThreadFactory() { private AtomicInteger threadNumber = new AtomicInteger(1); @Override public Thread newThread(Runnable r) { return new Thread(r, "money caculate thread: " + threadNumber.getAndAdd(1)); } }, new ThreadPoolExecutor.CallerRunsPolicy() );
注意,在上面的创建里,
我们使用了线程安全的有界队列,并设定了长度200;
我们自定义了线程工厂,这样该线程池创建的线程都有特殊的线程名;
我们选择了CallerRunsPolicy 拒绝策略,一旦线程池满负荷,再往里提交任务就由提交任务的线程自己去运行
其实拒绝策略一共有好几种:
- AbortPolicy,这种拒绝策略在拒绝任务时,会直接抛出异常 RejectedExecutionException (属于RuntimeException),让你感知到任务被拒绝了,于是你便可以根据业务逻辑选择重试或者放弃提交等策略。
- DiscardPolicy,这种拒绝策略正如它的名字所描述的一样,当新任务被提交后直接被丢弃掉,也不会给你任何的通知,相对而言存在一定的风险,因为我们提交的时候根本不知道这个任务会被丢弃,可能造成数据丢失。
- DiscardOldestPolicy,如果线程池没被关闭且没有能力执行,则会丢弃任务队列中的头结点,通常是存活时间最长的任务,这种策略与第二种不同之处在于它丢弃的不是最新提交的,而是队列中存活时间最长的,这样就可以腾出空间给新提交的任务,但同理它也存在一定的数据丢失风险。
- CallerRunsPolicy,相对而言它就比较完善了,当有新任务提交后,如果线程池没被关闭且没有能力执行,则把这个任务交于提交任务的线程执行,也就是谁提交任务,谁就负责执行任务,这样做任务可以保证不丢失。
而关于核心线程、最大线程的设置是一个经验问题,因此一个常见的经验配置如下,但实际上因为可能因为一个项目包含不止一个线程池,所以数目设置仅供参考
任务特性 | 特点 | 常用线程池 |
CPU密集型任务 | 应配置尽可能小的线程 | cpu数 +1 |
IO密集型任务 | 并不是一直在执行任务,则应配置尽可能多的线程 | 2*cpu数 |
混合型的任务 | 可拆分成cpu密集型任务和IO密集型任务 |
另外,很多框架也会提供创建线程池的方式,比如Spring的ThreadPoolTaskExecutor,但因为我们这里主要还是说JUC,所以不再展开。