扫码阅读
手机扫码阅读

线程池【Java并发编程】

245 2023-07-18

在真实高并发场景下,一般不会直接使用 Thread 类创建线程,而是使用线程池来创建并管理线程。可以这么说,学好线程池对于并发编程是非常重要的。


01

线程池简介

线程池的创建和回收是一个非常消耗系统资源的过程,如果在系统中频繁地创建和回收线程,会极大降低程序的执行性能。并且,短时间内创建大量的线程可能造成 CPU 占用 100%、死机或内存溢出等问题。而使用线程池就能非常轻松地解决这些问题。

线程池核心类继承关系

线程池是 Java 从 JDK 1.5 版本开始提供的一种线程使用模式,能够自动创建和回收线程,并管理线程的生命周期。在线程池中能够管理和维护多个线程。

Java 的线程池主要是通过 Executor 框架实现的,涉及 Executor 接口、ExecutorServcie 接口、AbstractExecutorService 抽象类、ScheduledExecutorService 接口、ThreadPoolExecutor 类和ScheduledThreadPoolExecutor 类。线程池核心类继承关系如下图所示。

实现线程池最核心的类是ThreadPoolExecutor,而 ScheduledThreadPoolExecutor 类实现了定时任务功能,能够使提交到线程池中的任务定时、定期执行。为了便于创建线程池,除了上图所示的接口和类,JDK 还提供了一个 Executors 工具类,Executors 类中封装了创建线程池的各种方法,专门用于创建线程池。不过,在真实的高并发场景下,并不推荐使用 Executors 工具类创建线程池,而是推荐直接使用 ThreadPoolExecutor 类创建线程池。


02

线程池的优点

这里,综合对比直接使用 Thread 类创建线程的弊端与使用线程池的优点,来加深读者对线程池的理解。

1.直接使用 Thread 类创建线程的缺点

直接在程序中使用 Thread 类创建线程的方式是非常不可取的,主要体现在如下几方面。

(1)每次通过 Thread 类创建一个线程对象的性能是非常差的,每次创建 Thread 对象后,调用 Thread 的 start()方法都会在操作系统层面分配一个与之对应的线程,这个过程比较耗时。

(2)直接使用 Thread 类创建线程缺乏有效的统一管理机制,如果在短时间内创建大量线程,线程之间就会竞争系统资源,可能造成 CPU 占用 100%、死机或者内存溢出等问题。

(3)直接使用 Thread 类创建线程提供的线程功能非常有限,例如,无法让线程执行更多的任务、无法定期执行某些任务等。

(4)直接使用 Thread 类创建线程,无法对线程进行有效监控。

2.使用线程池管理线程的优点

使用线程池能够非常容易地解决直接使用 Thread 创建线程产生的问题,主要体现在如下几方面。

(1)线程池能够复用线程资源,有效减少了线程的创建和回收频率,减少了线程的创建与回收对系统性能造成的影响,比直接使用 Thread 类创建线程的系统性能高。

(2)使用线程池能够有效控制最大并发线程数,提高系统资源的利用率。创建的线程数是可控的,短时间内不会因为创建大量的线程导致线程过多地竞争资源,引起线程阻塞。

(3)在线程池中可以定时或定期执行某个或某些任务,提供了单线程执行任务的机制,也能够控制并发线程数。线程池提供了监控线程资源的方法,可以对线程池中的线程资源进行实时监控。


03

ThreadPoolExecutor 类

ThreadPoolExecutor 是线程池中最核心的类,通过查看 ThreadPoolExecutor 的代码可以得知,在使用 ThreadPoolExecutor 类的构造方法创建线程池时,最终会调用具有 7 个参数的构造方法,

代码如下。

public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue, ThreadFactory threadFactory, RejectedExecutionHandler rejectHandler)

接下来,对 ThreadPoolExecutor 类构造方法中每个参数的具体含义进行简单的介绍。

(1)corePoolSize 参数。表示线程池的核心线程数。

(2)maximumPoolSize 参数。表示线程池中的最大线程数。

(3)keepAliveTime 参数。表示线程没有任务执行状态保持的最长时间。当线程池中的线程数量大于 corePoolSize 时,如果没有新的任务提交,则核心线程外的线程不会立即销毁,需要等待,直到等待的时间超过 keepAliveTime 才会终止。

(4)unit 参数。表示 keepAliveTime 的时间单位。

(5)workQueue 参数。表示线程池中的阻塞队列,存储等待执行的任务。

(6)threadFactory 参数。线程工厂,用来创建线程池中的线程。提供一个默认的线程工厂来创建线程,当使用默认的线程工厂创建线程时,会为线程设置一个名称,使新创建的线程具有相同的优先级,并且是非守护线程。 

(7)rejectHandler 参数。表示拒绝处理任务时的策略。当 workQueue 阻塞队列已满、线程池中的线程数已经达到最大,且线程池中没有空闲线程时,如果继续提交任务,就需要采取一种策略来处理这个任务。

其中,在 ThreadPoolExecutor 类的构造方法中,最重要的 3 个参数是 corePoolSize、maximumPoolSize 和 workQueue,这 3 个参数会对线程池的运行过程产生重大的影响。

三者的关系如下

  • 如果线程池中运行的线程数小于 corePoolSize,则直接创建新线程处理任务,即使线程池中的其他线程是空闲的。

  • 如果运行的线程数大于或等于 corePoolSize 并且小于 maximumPoolSize,则只有当workQueue 队列满时,才会创建新的线程处理任务。如果 workQueue 队列不满,则将新提交的任务放入 workQueue 队列中。当设置的 corePoolSize 与 maximumPoolSize 相同时,创建的线程池大小是固定的,如果满足有新任务提交、线程池中没有空闲线程,且 workQueue 未满的条件,就把请求放入workQueue,等待空闲的线程从 workQueue 中取出任务进行处理。

  • 如果运行的线程数量大于 maximumPoolSize,同时 workQueue 已满,则通过拒绝策略参数 rejectHandler 来指定处理策略。

    线程池提供了 4 种拒绝策略,分别如下。

  • 直接抛出异常,这也是默认的策略。实现类为 AbortPolicy。

  • 使用调用者所在的线程来执行任务。实现类为 CallerRunsPolicy。

  • 丢弃队列中最靠前的任务并执行当前任务。实现类为 DiscardOldestPolicy。

  • 直接丢弃当前任务。实现类为 DiscardPolicy。

本文节选自《深入理解高并发编程:JDK核心技术》一书,本书是冰河编写的专注介绍JDK高并发编程技术的书籍。

FunTester原创专题推荐~-- By FunTester
原文链接: https://mp.weixin.qq.com/s?__biz=MzU4MTE2NDEyMQ==&mid=2247499310&idx=1&sn=bf3dfb51fb227c2346805d37f48a34a5