线程池【Java并发编程】

2023-08-04 11:27:21 浏览数 (1)

在真实高并发场景下,一般不会直接使用 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 个参数的构造方法,

代码如下。

代码语言:javascript复制
public ThreadPoolExecutor(int corePoolSize, 
int maximumPoolSize, 
long keepAliveTime, 
TimeUnit unit, 
BlockingQueue<Runnable> 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原创专题推荐~

  • 900原创合集
  • 2021年原创合集
  • 2022年原创合集
  • 接口功能测试专题
  • 性能测试专题
  • Groovy专题
  • Java、Groovy、Go、Python
  • 单测&白盒
  • FunTester社群风采
  • 测试理论鸡汤
  • FunTester视频专题
  • 案例分享:方案、BUG、爬虫
  • UI自动化专题
  • 测试工具专题

-- By FunTester

0 人点赞