一、介绍
1. 什么是线程池
线程池(ThreadPool)是一种基于池化思想管理和使用线程的机制:它是将多个线程预先存储在一个“池子”内,当有任务出现时可以避免重新创建和销毁线程所带来性能开销,只需要从“池子”内取出相应的线程执行对应的任务即可。
在Java中,池化思想主要体现在资源管理上,以提高程序的性能和资源的利用效率。池化技术允许预先创建和维护一组可重用的对象,这些对象可以在需要时被快速分配,使用后归还给池,而不是每次需要时都创建新对象,这样可以显著减少对象创建和销毁的开销。
2.为什么要使用线程池
使用线程池的好处如下:
- 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
- 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。可以直接从线程池中取出已经创建好的线程。
- 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
二、如何创建线程池
线程池的创建方法总共有 7 种,但总体来说可分为 2 类:
- 通过 ThreadPoolExecutor 创建的线程池。
- 通过 Executors 创建的线程池。
线程池的创建方式总共包含以下 7 种(其中 6 种是通过 Executors 创建的,1 种是通过 ThreadPoolExecutor 创建的):
- Executors.newFixedThreadPool():创建一个固定大小的线程池,可控制并发的线程数,超出的线程会在队列中等待;
- Executors.newCachedThreadPool():创建一个可缓存的线程池,若线程数超过处理所需,缓存一段时间后会回收多余的线程,若线程数不够,则新建线程;
- Executors.newSingleThreadExecutor():创建单个线程数的线程池,它可以保证先进先出的执行顺序;
- Executors.newScheduledThreadPool():创建一个可以执行延迟任务的线程池;
- Executors.newSingleThreadScheduledExecutor():创建一个单线程的可以执行延迟任务的线程池;
- Executors.newWorkStealingPool():创建一个抢占式执行的线程池(任务执行顺序不确定)JDK 1.8 添加。
- ThreadPoolExecutor:最原始的创建线程池的方式,它包含了 7 个参数可供设置。
虽然有这么多创建线程的方式,但是在《阿里巴巴 Java 开发手册》强制线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 构造函数的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
Executors的使用非常简单和直观,但是它有一些默认配置上的缺陷,特别是在处理高并发或者长时间运行的应用程序时,这些缺陷可能导致系统资源耗尽,甚至崩溃。
三、通过ThreadPoolExecutor 创建
ThreadPoolExecutor 最多可以设置 7 个参数:
- corePoolSize:核心线程数,线程池中始终存活的线程数。
- maximumPoolSize:最大线程数,线程池中允许的最大线程数,当线程池的任务队列满了之后可以创建的最大线程数。
- keepAliveTime:最大线程数可以存活的时间,当线程中没有任务执行时,最大线程就会销毁一部分,最终保持核心线程数量的线程。
虽然但是,通过设置allowCoreThreadTimeOut为true,即这个方法:executor.allowCoreThreadTimeOut(true);核心线程也会像额外的非核心线程一样,在空闲时根据keepAliveTime超时设置被回收。
-
unit:单位是和参数 3 存活时间配合使用的,合在一起用于设定线程的存活时间。参数 keepAliveTime 的时间单位有以下 7 种可选:
- TimeUnit.DAYS:天
- TimeUnit.HOURS:小时
- TimeUnit.MINUTES:分
- TimeUnit.SECONDS:秒
- TimeUnit.MILLISECONDS:毫秒
- TimeUnit.MICROSECONDS:微妙
- TimeUnit.NANOSECONDS:纳秒
-
workQueue:一个阻塞队列,用来存储线程池等待执行的任务,均为线程安全。它一般分为直接提交队列、有界任务队列、无界任务队列、优先任务队列几种,包含以下 7 种类型:
- ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列。
- LinkedBlockingQueue:一个由链表结构组成的有界阻塞队列。
- SynchronousQueue:一个不存储元素的阻塞队列,即直接提交给线程不保持它们。
- PriorityBlockingQueue:一个支持优先级排序的无界阻塞队列。
- DelayQueue:一个使用优先级队列实现的无界阻塞队列,只有在延迟期满时才能从中提取元素
- LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。与SynchronousQueue类似,还含有非阻塞方法。
- LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。
较常用的是 LinkedBlockingQueue 和 Synchronous,线程池的排队策略与 BlockingQueue 有关
-
threadFactory:线程工厂,主要用来创建线程。
-
handler:拒绝策略,拒绝处理任务时的策略,系统提供了 4 种可选:
- AbortPolicy:拒绝并抛出异常。
- CallerRunsPolicy:使用当前调用的线程来执行此任务。
- DiscardOldestPolicy:抛弃任务队列头部(最旧)的一个任务,并执行当前任务。
- DiscardPolicy:忽略并抛弃当前任务。
默认策略为 AbortPolicy
四、线程池执行流程
- 如果当前运行的线程数小于核心线程数,那么就会新建一个线程来执行任务。
- 如果当前运行的线程数等于或大于核心线程数,但是小于最大线程数,那么就把该任务放入到任务队列里等待执行。
- 如果向任务队列投放任务失败(任务队列已经满了),但是当前运行的线程数是小于最大线程数的,就新建一个线程来执行任务。
- 如果当前运行的线程数已经等同于最大线程数了,新建线程将会使当前运行的线程超出最大线程数,那么当前任务会被拒绝,拒绝策略会调用RejectedExecutionHandler.rejectedExecution()方法。
为什么要先放到任务队列,只有任务队列满了才会新建一个线程来执行任务?
- 资源管理:
线程的创建和销毁是相对昂贵的操作,涉及操作系统资源的分配和回收。通过使用任务队列,可以避免频繁地创建和销毁线程,从而节省系统资源。- 任务缓冲:
任务队列作为缓冲区,可以暂时存储到来的任务,即使没有立即可用的线程来执行这些任务。这有助于平滑任务到达的峰值,避免因瞬间大量任务涌入而导致的系统过载。- 并发控制:
线程池通过限制线程的数量来控制并发级别。如果任务直接交给线程执行而不经过队列,可能会导致过多的线程同时运行,增加系统负载和上下文切换的开销,从而降低效率。
后言
希望本文可以让大家对自定义线程池有一个粗略的了解。如果有不对的地方欢迎大家指正。