Java支持多线程,虽然启动一个新线程很容易,但是创建线程需要向操作系统申请资源,消耗大量时间,因此便有了线程池。线程池是池化技术的一种使用,池化技术指的是提前准备一定的资源,只要池中有足够的资源,在需要时可以随时从池中取出资源使用,使用完后将资源归还到池中。池化技术带来了显著的优点:主要是可重复使用资源,避免动态创建资源、释放资源带来的额外开销和等待时间。在服务器资源有限的情况下,使用池化技术可以提高资源的利用率,从而提升性能。池化技术比较典型的用途有:连接池、线程池、内存池、对象池。
线程池继承关系
1 | Executor(I) |
所有的线程池最终都实现自 ExecutorService
接口,线程池可分为带定时调度的线程池和普通线程池。
线程池的使用
使用线程池时,可配置的选项有 线程名称、核心线程数、最大线程数、队列容量、空闲时间、空闲时间的时间单位、拒绝策略 等。
线程名称可自行指定用于调试、日志时方便区分,也可以使用 Google Guava提供的 ThreadFactory 来构建带数字后缀的线程。
线程池中至少会有一个队列用于线程繁忙时暂存工作任务,等待线程空闲后执行。
核心线程数是常驻的线程,在存储任务信息的队列未满之前,线程池中的线程数量等于核心线程数;当添加一个任务时,核心线程数都在工作中,但线程池中的线程数量还没达到最大线程数,存储信息的工作队列已满的情况下,会新建一个临时线程执行任务,新建的临时线程经过空闲时间没有分配任务则会被回收。
线程池使用完成后如果不需要重复使用,必须调用 shutdown()
方法关闭线程池,工程中通常会将线程池注册为 Spring Bean 常驻内存,随时取用,此时就不需要关闭线程池。
- ThreadPoolExecutor
ThreadPoolExecutor 是常用的线程池,线程池中所有的线程共用一个队列。
1 | public ThreadPoolExecutor newExecutor() { |
- ForkJoinPool
ForkJoinPool 线程池使用分治算法,用相对少的线程处理大量的任务,将一个大任务一拆为二,以此类推,每个子任务再拆分一半,直到达到最细颗粒度为止,即设置的阈值停止拆分,然后从最底层的任务开始计算,往上一层一层合并结果。最适合计算密集型任务,而且最好是非阻塞任务。与 ThreadPoolExecutor 稍有不同的地方是 ForkJoinPool 的每个线程都有各自独立的双端队列(Deque),当某个线程的任务队列中没有可执行任务的时候,从其他线程的任务队列尾部窃取任务来执行,以充分利用工作线程的计算能力,减少线程由于获取不到任务而造成的空闲浪费。JDK8 Stream 中的 parallelStream()
是基于此线程池实现的,CompletableFuture 的异步回调 future 内部使用的线程池也是 ForkJoinPool。
- ScheduledThreadPoolExecutor
ScheduledThreadPoolExecutor 是一个带定时调度功能的线程池.
1 | // 只在delay的时间后执行一次 |
- Executors
JDK 提供了 Executors 类来快速创建线程池,但 Executors 创建的线程池并没有定义暂存队列的容量,默认队列是无界队列,最多可容纳 Integer.MAX_VALUE
的任务信息,可能导致应用出现OOM的情况,因此在工程中最好手动创建线程池。
线程池拒绝策略
当线程池线程数已满,并且工作队列达到限制,新提交的任务使用拒绝策略处理。可以自定义拒绝策略,拒绝策略需要实现 RejectedExecutionHandler
接口。
JDK默认的拒绝策略有四种:
- AbortPolicy:丢弃任务并抛出
RejectedExecutionException
异常。 - DiscardPolicy:丢弃任务,但是不抛出异常。可能导致无法发现系统的异常状态。
- DiscardOldestPolicy:丢弃队列最前面的任务,然后重新提交被拒绝的任务。
- CallerRunsPolicy:任务将转回发起调用的线程完成,可能会阻塞发起调用的线程。
线程池中线程数量应该如何设置
线程池中的线程数量需要根据实际情况恰当设置,如果线程数量太多会导致频繁的上下文切换,影响线程的运行效率,上下文切换时线程暂停的时间用于执行任务可能任务已经结束了;如果线程数量太少则无法充分的利用处理器的多核心。理想的线程数取决于处理器的核心数,在理想情况下,每个核心运行一个线程是最高效的,线程池只需要设置为处理器的核心数量即可。实际上线程数的设定需要根据应用程序的需求和运行环境来决定,没有一个固定的最佳值。
如果是CPU密集型的任务,可将线程数设置为处理器核心数量的1到1.5倍(如N+1);如果是IO密集型任务,因为任务涉及到大量的阻塞等待,可以配置2倍或者更多的数量(如2N+1)。相关的公式仅供参考,可在业务部署时先设置为公式的最大值,然后进行压力测试,根据压力测试的反馈动态调整线程数量。