从零开始学Java之详解Java线程池的使用方式

作者 :孙玉昌,昵称【一一哥 】,另外【壹壹哥】也是我哦

千锋教育高级教研员、CSDN博客专家、万粉博主、阿里云专家博主、掘金优质作者

前言

在前面的两篇文章中,壹哥 给大家讲解了线程的创建方式,以及线程的核心API方法。在讲解这些内容时,我们涉及到了线程池的概念,有不少同学对线程池很好奇,但又不太了解。再加上以后我们进行多线程的开发,很多时候都是利用线程池进行线程的管理与创建,所以今天壹哥要再写一篇文章,详细地讲解线程池。

------------------------------前戏已做完,精彩即开始----------------------------

全文大约【3800】 字,不说废话,只讲可以让你学到技术、明白原理的纯干货!本文带有丰富的案例及配图视频,让你更好地理解和运用文中的技术概念,并可以给你带来具有足够启迪的思考......

配套开源项目资料

Github: github.com/SunLtd/Lear...

Gitee: gitee.com/sunyiyi/Lea...

一. 线程池

1. 概念

所谓的线程池,就是一个管理了一组线程的池子,我们可以把它理解为一个包含了一定数量的线程"集合"。在线程池中,包含了一个任务队列和若干线程,当需要执行任务时,线程池中的线程就会自动分配任务并执行。利用线程池可以让我们避免频繁地创建和销毁线程对象,其优点主要体现在以下方面:

  • 提高程序性能:频繁地创建和销毁线程比较耗费资源,线程池可以重复利用已创建的线程,避免频繁创建和销毁线程的较大开销;
  • 提高程序稳定性:线程池可以限制并发线程的数量,避免过多的线程导致系统资源耗尽或系统崩溃;
  • 提高编程效率:通过线程池,我们可以将线程的管理和任务的执行分离,提高代码的可读性和可维护性。

2. 核心API

既然线程池有这么多的优点,那我们该怎么使用线程池呢?如果我们要想进行线程池的开发,主要是使用以下几个API:

  • Executor;
  • ExecutorService;
  • ThreadPoolExecutor;
  • Executors;
  • ForkJoinPool

以上4个核心API之间的关系如下图所示:

接下来壹哥分别给大家简要地介绍一下这几个类。

2.1 Executor

Executor是Java线程池的核心接口,它定义了一组执行任务的方法,可以把线程的创建和执行分离开,提高了程序的可扩展性和可维护性,简化了线程的管理。Executor接口中只有一个方法:

方法名称 方法描述
execute(Runnable command) 将一个任务提交到线程池中执行。

2.2 ExecutorService

ExecutorService是用于管理线程池的接口,它继承自Executor接口。我们知道,Executor接口给我们定义了一个execute方法,用于执行一个任务。而ExecutorService接口则在此基础上增加了一些新的方法,比如submit、invokeAll、invokeAny等,用于提交任务、批量执行任务等。同时,ExecutorService接口还增加了一些管理线程池的方法,比如shutdown、awaitTermination等。因此,ExecutorService可以对线程池进行任务的提交、任务的管理和线程池本身的管理等,可以说ExecutorService是更加强大、更加灵活的线程池接口。该接口中有如下几个常用的方法:

方法名 描述
execute(Runnable command) 将一个任务提交到线程池中执行。
submit(Callable task) 将一个带有返回值的任务提交到线程池中执行。
invokeAll() 批量提交Callable任务给线程池执行
invokeAny() 批量提交Callable任务到线程池,返回第一个成功执行的任务结果
shutdown() 关闭线程池,不再接受新的提交任务
awaitTermination() 等待线程池中所有任务执行完成,并且所有的线程都已销毁

2.3 ThreadPoolExecutor

ThreadPoolExecutor是用于实现线程池的具体子类,它实现了ExecutorService接口,继承自AbstractExecutorService类。与Executors工厂类相比,ThreadPoolExecutor使用起来更加灵活。我们可以直接通过new对象的方式来创建一个ThreadPoolExecutor线程池对象,并且可以在构造ThreadPoolExecutor线程池对象时自定义线程池的核心参数,如下图所示:

尤其是ThreadPoolExecutor类中的核心参数,更是面试官经常考察的一个点,比如经常有面试官会问我们这样一个问题:"请说一下线程池的几个核心参数及其作用" 。所以壹哥再次就把ThreadPoolExecutor中的核心参数列举出来,供大家掌握理解。

ThreadPoolExecutor构造方法核心参数的含义如下:

  • corePoolSize线程池核心线程数,即线程池中始终保持存在的线程数。当提交的任务数少于等于核心线程数时,线程池中会始终保持这些核心线程的存在。如果有新的任务提交时,核心线程会优先处理。如果核心线程都在执行任务,新的任务就会被加入到任务队列中进行等待。
  • maximumPoolSize线程池的最大线程数,即线程池中最多同时执行任务的线程数。当任务数大于核心线程数时,线程池会创建新的线程来处理任务,但线程数不会超过最大线程数。如果任务量持续增加,超过了最大线程数,就会根据设定的拒绝策略来处理这些任务。
  • keepAliveTime :该线程池中非核心线程的闲置超时时长。 一个非核心线程,如果不干活(闲置状态)的时长超过了这个参数所设定的时长,就会被销毁掉,这样可以避免线程池中的线程数无限增长,浪费系统资源。如果设置了allowCoreThreadTimeOut = true,只会作用于核心线程,核心线程会受到超时时长的影响;而如果为false,核心线程会一直驻留,即使该线程闲着,但非核心线程会一直受到超时时长的影响。
  • unitkeepAliveTime参数的时间单位。可以设置为秒、毫秒等。
  • workQueue线程池的任务队列, 这是一个阻塞队列用于存放等待执行的任务。当所有的核心线程都在执行任务时,新来的任务就会被加入到这个任务队列中。任务队列可以选用不同的实现方式,比如ArrayBlockingQueue、LinkedBlockingQueue等。任务队列可以减轻线程池的压力,当任务量过大时,线程池可以将任务放入任务队列中,由线程池中的线程逐一取出并执行任务,避免线程池中的线程数量过多,浪费系统资源。
  • threadFactory线程工厂,用于创建新的线程。线程工厂可以自定义线程的属性,比如线程名、线程优先级等。
  • handler拒绝策略,用于处理无法执行的任务。当任务队列已满且线程池中的线程数达到最大线程数时,新的任务就会被拒绝。拒绝策略可以选择不同的实现方式,比如AbortPolicy、CallerRunsPolicy等。

为了让大家更好地理解这些参数的作用,壹哥给大家举个例子,具体分析一下这些参数的作用。

比如我们现在创建了一个核心线程数为5,最大线程数为10的线程池,那么当有新的任务提交时,此时如果当前正在工作的线程数小于5,就会创建出新的线程来执行任务;如果当前线程数等于5,新的任务就会被加入到任务队列中;如果任务队列已满且当前线程数小于10,就会再创建新的线程来执行任务;如果任务队列已满且当前线程数等于10,新的任务就会根据预定的拒绝策略进行处理。同时,当线程池中的线程数超过核心线程数时,假如空闲线程的存活时间为60秒,则超过60秒后,其他没有可执行任务的线程就会被销毁。

这就好比有一个银行,这个银行最多只有10个办事窗口,但为了节约人工成本,一般情况下只会开放5个常用的窗口对外办公。假如现在银行正有3个客户办理业务,但此时又来了一个新的客户,但发现目前只开放了3个窗口,于是银行就会再开放一个新的窗口。如果银行发现已经开放了5个窗口并正在服务5个客户,此时又来了第6个客户,就不再开放新窗口,而是让第6个客户进入到一个等待区(任务队列)中进行等待。但是如果等待区的客户太多,把等待区挤满了,且此时银行还是只开放了5个窗口,这时银行就会打开新的窗口,直到把10个窗口都开放完毕为止,就不再开放新的窗口了。后面如果还是不断地涌入新的客户,此时银行就会拒绝接待这些新的客户,停止为他们进行服务。最后如果银行的客户减少了,不需要太多窗口对外服务了,就会按照预先设置好的时间,把没有客户的窗口给关闭掉,只保留几个常用窗口。

2.4 Executors

Executors是用于创建线程池的工具类,它提供了几个静态方法,用于创建不同类型的线程池,比如newFixedThreadPool、newCachedThreadPool等方法,这些方法返回的都是实现了ExecutorService接口的线程池对象。因此,可以说Executors是用于创建线程池的快捷方式,它封装了ThreadPoolExecutor的创建过程,让用户可以更加方便地创建线程池。Executors创建线程池的常用方法如下:

方法名称 方法描述
newFixedThreadPool(int nThreads) 创建一个固定大小的线程池。
newCachedThreadPool() 创建一个有缓存的线程池。
newSingleThreadExecutor() 创建一个单线程的线程池。
newScheduledThreadPool(int corePoolSize) 创建一个支持定时及周期性执行任务的线程池。

2.5 ForkJoinPool

ForkJoinPool是Java 7中新增的线程池类,专门用于支持分治任务的并行处理。在ForkJoinPool中,一个大的线程任务会被分割成多个小任务,每个小任务都可以独立地执行,最终将它们的结果合并起来得到最终的结果,这样我们就可以充分利用线程资源,提高程序的性能和效率。ForkJoinPool具有如下特点:

  • 工作窃取算法:在ForkJoinPool中,每个线程都有一个任务队列,用于存放等待执行的任务。当线程执行完自己任务队列中的所有任务后,它就可以从其他线程的任务队列中窃取任务来执行,以充分利用线程的资源。
  • 分治任务支持:ForkJoinPool专门用于支持分治任务的并行处理。在ForkJoinPool中,一个大的任务会被分割成多个小任务,每个小任务都可以独立地执行,最后它们的结果会被合并起来得到最终的结果。
  • 工作线程管理:ForkJoinPool中的工作线程采用守护线程的方式创建,当所有的非守护线程结束时,守护线程也会自动结束,这样可以避免线程在等待空闲任务时浪费系统资源。

接下来壹哥就结合以上API,来给大家讲解一下具体的线程池创建过程。

二. 实现案例

1. ThreadPoolExecutor手动创建线程池

如果我们需要更加灵活地控制线程池的参数,可以手动创建ThreadPoolExecutor类对象,比如下面的示例代码:

java 复制代码
//创建线程池对象
ThreadPoolExecutor executor = new ThreadPoolExecutor(
    5, 10, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());

//向线程池中添加要执行的任务
executor.execute(new Runnable() {
    @Override
    public void run() {
        // do something
    }
});

这段代码创建了一个核心线程数为5,最大线程数为10,任务队列无限大的线程池,并通过execute方法向线程池中添加任务。

2. 使用Executors工厂类创建线程池

Executors是一个线程池工厂类,它提供了多种创建线程池的静态方法,我们可以采用如下代码进行创建:

java 复制代码
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * @author 一一哥Sun
 */
public class Demo15 {
    public static void main(String[] args) throws InterruptedException {
        //创建只有单个线程的线程池对象
        //ExecutorService service = Executors.newSingleThreadExecutor();
		
        //创建带有缓存功能的线程池对象
        //ExecutorService service = Executors.newCachedThreadPool();
		
        //创建支持定时及周期性任务的线程池对象
        //ScheduledExecutorService service = Executors.newScheduledThreadPool(5);
		
        //创建固定大小为5的线程池对象,默认创建的是无界线程池,默认的最大容量是Integer.MAX_VALUE,相当于是无界的阻塞队列
        ExecutorService service = Executors.newFixedThreadPool(5);
		
        //提交线程任务
        service.submit(new Runnable() {
            @Override
            public void run() {
                //执行线程任务
            }
        });
    }
}

我们在上面的代码中创建了一个固定大小为5的线程池对象,并通过submit方法向线程池中要添加任务。

3. 使用ForkJoinPool创建线程池对象

ForkJoinPool是Java 7中新增的一个线程池类,它专门用于支持分治任务的并行处理,比如下面的代码:

java 复制代码
//创建一个ForkJoinPool对象
ForkJoinPool pool = new ForkJoinPool();
//往线程池中添加要执行的任务
pool.invoke(new RecursiveAction() {
    @Override
    protected void compute() {
        // do something
    }
});

上面这段代码,我们创建了一个默认大小的ForkJoinPool线程池对象,并通过invoke方法向线程池中添加了一个RecursiveAction任务。

------------------------------正片已结束,来根事后烟----------------------------

三. 结语

以上就是线程池的概念及其使用详情,现在你学会了吗?当我们的项目中,要使用多线程时,请大家尽量利用线程池进行创建,这会比直接通过new Thread或实现Runnable接口的方式效率高得多。尤其是

Executor、ExecutorService、ThreadPoolExecutor、Executors 这几个类之间的关系,以ForkJoinPool等的使用方式,大家要牢牢记住哦。

另外如果你独自学习觉得有很多困难,可以加入壹哥的学习互助群,大家一起交流学习。

相关推荐
小比卡丘1 小时前
C语言进阶版第17课—自定义类型:联合和枚举
android·java·c语言
xmh-sxh-13141 小时前
java 数据存储方式
java
liu_chunhai1 小时前
设计模式(3)builder
java·开发语言·设计模式
姜学迁1 小时前
Rust-枚举
开发语言·后端·rust
爱学习的小健2 小时前
MQTT--Java整合EMQX
后端
北极小狐2 小时前
Java vs JavaScript:类型系统的艺术 - 从 Object 到 any,从静态到动态
后端
ya888g2 小时前
GESP C++四级样题卷
java·c++·算法
【D'accumulation】2 小时前
令牌主动失效机制范例(利用redis)注释分析
java·spring boot·redis·后端
小叶学C++2 小时前
【C++】类与对象(下)
java·开发语言·c++
2401_854391082 小时前
高效开发:SpringBoot网上租赁系统实现细节
java·spring boot·后端