程序员scholar 程序员scholar
首页
  • Java 基础

    • JavaSE
    • JavaIO
    • JavaAPI速查
  • Java 高级

    • JUC
    • JVM
    • Java新特性
    • 设计模式
  • Web 开发

    • Servlet
    • Java网络编程
  • Web 标准

    • HTML
    • CSS
    • JavaScript
  • 前端框架

    • Vue2
    • Vue3
    • Vue3 + TS
    • 微信小程序
    • uni-app
  • 工具与库

    • jQuery
    • Ajax
    • Axios
    • Webpack
    • Vuex
    • WebSocket
    • 第三方登录
  • 后端与语言扩展

    • ES6
    • Typescript
    • node.js
  • Element-UI
  • Apache ECharts
  • 数据结构
  • HTTP协议
  • HTTPS协议
  • 计算机网络
  • Linux常用命令
  • Windows常用命令
  • SQL数据库

    • MySQL
    • MySQL速查
  • NoSQL数据库

    • Redis
    • ElasticSearch
  • 数据库

    • MyBatis
    • MyBatis-Plus
  • 消息中间件

    • RabbitMQ
  • 服务器

    • Nginx
  • Spring框架

    • Spring6
    • SpringMVC
    • SpringBoot
    • SpringSecurity
  • SpringCould微服务

    • SpringCloud基础
    • 微服务之DDD架构思想
  • 日常必备

    • 开发常用工具包
    • Hutoll工具包
    • IDEA常用配置
    • 开发笔记
    • 日常记录
    • 项目部署
    • 网站导航
    • 产品学习
    • 英语学习
  • 代码管理

    • Maven
    • Git教程
    • Git小乌龟教程
  • 运维工具

    • Docker
    • Jenkins
    • Kubernetes
  • 算法笔记

    • 算法思想
    • 刷题笔记
  • 面试问题常见

    • 十大经典排序算法
    • 面试常见问题集锦
关于
GitHub (opens new window)
首页
  • Java 基础

    • JavaSE
    • JavaIO
    • JavaAPI速查
  • Java 高级

    • JUC
    • JVM
    • Java新特性
    • 设计模式
  • Web 开发

    • Servlet
    • Java网络编程
  • Web 标准

    • HTML
    • CSS
    • JavaScript
  • 前端框架

    • Vue2
    • Vue3
    • Vue3 + TS
    • 微信小程序
    • uni-app
  • 工具与库

    • jQuery
    • Ajax
    • Axios
    • Webpack
    • Vuex
    • WebSocket
    • 第三方登录
  • 后端与语言扩展

    • ES6
    • Typescript
    • node.js
  • Element-UI
  • Apache ECharts
  • 数据结构
  • HTTP协议
  • HTTPS协议
  • 计算机网络
  • Linux常用命令
  • Windows常用命令
  • SQL数据库

    • MySQL
    • MySQL速查
  • NoSQL数据库

    • Redis
    • ElasticSearch
  • 数据库

    • MyBatis
    • MyBatis-Plus
  • 消息中间件

    • RabbitMQ
  • 服务器

    • Nginx
  • Spring框架

    • Spring6
    • SpringMVC
    • SpringBoot
    • SpringSecurity
  • SpringCould微服务

    • SpringCloud基础
    • 微服务之DDD架构思想
  • 日常必备

    • 开发常用工具包
    • Hutoll工具包
    • IDEA常用配置
    • 开发笔记
    • 日常记录
    • 项目部署
    • 网站导航
    • 产品学习
    • 英语学习
  • 代码管理

    • Maven
    • Git教程
    • Git小乌龟教程
  • 运维工具

    • Docker
    • Jenkins
    • Kubernetes
  • 算法笔记

    • 算法思想
    • 刷题笔记
  • 面试问题常见

    • 十大经典排序算法
    • 面试常见问题集锦
关于
GitHub (opens new window)
npm

(进入注册为作者充电)

  • JavaSE - 基础篇

    • Java环境搭建
    • Java基础语法
    • Java数据类型
    • Java常量和变量
    • Java进制和存储
    • Java运算符
    • Java流程控制
    • Java数组
    • Java面向对象上
    • Java面向对象下
    • Java异常机制
    • Java枚举
    • Java反射机制
    • Java代理模式
    • Java泛型
    • Java序列化
    • Java多线程详解
    • Java线程池相关
      • 1. 前言
      • 2. 线程状态介绍
      • 3. 线程池
        • 3.1 线程池-概述
        • 3.2 线程池解决了什么问题
        • 3.3 ExecutorService接口
        • 3.3.1 提交方法
        • 3.3.2 关闭方法
        • 3.4 JDK中自带的线程池-Executors
        • 3.5 基于ThreadPoolExecutor构建线程池
        • 3.6 线程池的工作原理
      • 4. 线程池的参数
        • 4.1 任务队列(workQueue)
        • 4.2 线程工厂(threadFactory)
        • 4.3 拒绝策略(handler)
      • 5. 详解Executors提供的线程池
        • 5.1 定长线程池(FixedThreadPool)
        • 5.2 定时线程池(ScheduledThreadPool )
        • 5.3 可缓存线程池(CachedThreadPool)
        • 5.4 单线程化线程池(SingleThreadExecutor)
        • 5.5 对比
      • 6. 总结
  • Java
  • JavaSE - 基础篇
scholar
2024-01-17
目录

Java线程池相关

# Java基础篇 | 线程池相关

思维导图

下载 (3)

# 1. 前言

大家好,我是scholar (opens new window),[继上一篇我们没有学习完的多线程],这一节我们主要学习多线程中的线程池相关内容。好了,话不多说让我们开始吧😎😎😎。

# 2. 线程状态介绍

在学习线程池之前,我们有必要先了解一下关于线程的集中状态。

当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。线程对象在不同的时期有不同的状态。那么Java中的线程存在哪几种状态呢?Java中的线程

状态被定义在了java.lang.Thread.State枚举类中,State枚举类的源码如下:

image-20231108211401746

通过源码我们可以看到Java中的线程存在6种状态,每种线程状态的含义如下:

下载 (4)

线程状态 具体含义
NEW 一个尚未启动的线程的状态。也称之为初始状态、开始状态。线程刚被创建,但是并未启动。还没调用start方法。MyThread t = new MyThread()只有线程象,没有线程特征。
RUNNABLE 当我们调用线程对象的start方法,那么此时线程对象进入了RUNNABLE状态。那么此时才是真正的在JVM进程中创建了一个线程,线程一经启动并不是立即得到执行,线程的运行与否要听令与CPU的调度,那么我们把这个中间状态称之为可执行状态(RUNNABLE)也就是说它具备执行的资格,但是并没有真正的执行起来而是在等待CPU的度。
BLOCKED 当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入Blocked状态;当该线程持有锁时,该线程将变成Runnable状态。
WAITING 一个正在等待的线程的状态。也称之为等待状态。造成线程等待的原因有两种,分别是调用Object.wait()、join()方法。处于等待状态的线程,正在等待其他线程去执行一个特定的操作。例如:因为wait()而等待的线程正在等待另一个线程去调用notify()或notifyAll();一个因为join()而等待的线程正在等待另一个线程结束。
TIMED_WAITING 一个在限定时间内等待的线程的状态。也称之为限时等待状态。造成线程限时等待状态的原因有三种,分别是:Thread.sleep(long),Object.wait(long)、join(long)。
TERMINATED 一个完全运行完成的线程的状态。也称之为终止状态、结束状态

那么线程这几种状态是如何进行转换的呢,我们来看一下下面这张图

1591163781941

# 3. 线程池

# 3.1 线程池-概述

线程池:一个容纳多个线程的容器,容器中的线程可以重复使用,省去了频繁创建和销毁线程对象的操作。

线程池作用:

  • 降低资源消耗,减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
  • 提高响应速度,当任务到达时,如果有线程可以直接用,不会出现系统僵死。
  • 提高线程的可管理性,如果无限制的创建线程,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

线程池的核心思想: 把线程创建好了之后,放到池子里,需要使用线程,就直接从池子里取,而不是通过系统来创建。当线程用完了,又放回池子里,而不是通过系统来销毁!!(线程复用,同一个线程可以被重复使用,来处理多个任务。)

为什么从线程池取线程要比从系统申请效率更高呢

因为从线程池取线程是纯粹的用户态操作,而从系统创建线程会涉及到内核态和用户态的切换。

用户态切换到内核态需要进行“上下文切换”。这个过程包括保存当前进程的状态、加载内核进程的状态等一系列操作,消耗相对较大的时间和资源。而在用户态执行的操作不需要这种切换,因此成本更低。

# 3.2 线程池解决了什么问题

线程池解决的核心问题就是资源管理问题。在并发环境下,系统不能够确定在任意时刻中,有多少任务需要执行,有多少资源需要投入。这种不确定性将带来以下若干问题:

  1. 频繁申请/销毁资源和调度资源,将带来额外的消耗,可能会非常巨大。
  2. 对资源无限申请缺少抑制手段,易引发系统资源耗尽的风险。
  3. 系统无法合理管理内部的资源分布,会降低系统的稳定性。

为解决资源分配这个问题,线程池采用了“池化”(Pooling)思想。池化,顾名思义,是为了最大化收益并最小化风险,而将资源统一在一起管理的一种思想。

# 3.3 ExecutorService接口

ExecutorService是一个接口,它定义了一组方法,这些方法提供了线程池的操作和管理功能,如任务提交、线程池关闭、任务执行情况查询等。任何实现了ExecutorService接口的类都必须提供这些方法的具体实现。

  • 通过Executors创建的线程池:如Executors.newFixedThreadPool()或Executors.newCachedThreadPool()等方法,返回的都是ExecutorService接口的实现。
  • 直接使用ThreadPoolExecutor创建的线程池:ThreadPoolExecutor本身就是ExecutorService接口的一个具体实现,提供了更灵活的配置选项。

# 3.3.1 提交方法

ExecutorService 类 API:

方法 说明
void execute(Runnable command) 执行任务(Executor 类 API)
Future<?> submit(Runnable task) 提交任务 task()
Future submit(Callable task) 提交任务 task,用返回值 Future 获得任务执行结果
List<Future> invokeAll(Collection<? extends Callable> tasks) 提交 tasks 中所有任务
List<Future> invokeAll(Collection<? extends Callable> tasks, long timeout, TimeUnit unit) 提交 tasks 中所有任务,超时时间针对所有task,超时会取消没有执行完的任务,并抛出超时异常
T invokeAny(Collection<? extends Callable> tasks) 提交 tasks 中所有任务,哪个任务先成功执行完毕,返回此任务执行结果,其它任务取消

execute 和 submit 都属于线程池的方法,对比:

  • execute 只能执行 Runnable 类型的任务,没有返回值; submit 既能提交 Runnable 类型任务也能提交 Callable 类型任务,底层是封装成 FutureTask,然后调用 execute 执行。
  • execute():如果任务中有未捕获的异常,它会在执行该任务的线程中抛出,可能导致线程终止,但不会影响调用 execute() 的线程。
  • submit():如果任务中有异常,它会被封装在 Future 对象中,可以通过调用 Future.get() 方法在调用 submit() 的线程中捕获并处理。

# 3.3.2 关闭方法

ExecutorService 类 API:

方法 说明
void shutdown() 线程池状态变为 SHUTDOWN,等待任务执行完后关闭线程池,不会接收新任务,但已提交任务会执行完,而且也可以添加线程(不绑定任务)
List shutdownNow() 线程池状态变为 STOP,用 interrupt 中断正在执行的任务,直接关闭线程池,不会接收新任务,会将队列中的任务返回
boolean isShutdown() 不在 RUNNING 状态的线程池,此执行者已被关闭,方法返回 true
boolean isTerminated() 线程池状态是否是 TERMINATED,如果所有任务在关闭后完成,返回 true
boolean awaitTermination(long timeout, TimeUnit unit) 调用 shutdown 后,由于调用线程不会等待所有任务运行结束,如果它想在线程池 TERMINATED 后做些事情,可以利用此方法等待

# 3.4 JDK中自带的线程池-Executors

Executors类是Java标准库(Java Standard Edition,JDK)中提供的一个工具类,它属于java.util.concurrent包。Executors类提供了一系列静态方法来创建不同类型的线程池,这些线程池底层实际上是基于ThreadPoolExecutor和ScheduledThreadPoolExecutor这两个类的实现。

image-20240316235207926

Executors类中提供的静态工厂方法主要包括:

  • newFixedThreadPool(int nThreads): 创建一个固定数量的线程池。这种类型的线程池在应用程序的生命周期内拥有固定数量的线程,适用于负载相对稳定的场景。
  • newCachedThreadPool(): 创建一个可缓存的线程池,可以灵活回收空闲线程,若无可回收则新建线程。适合处理大量短暂异步任务。
  • newSingleThreadExecutor(): 创建一个单线程的Executor。这保证了所有提交的任务都按顺序执行,并且在任一时间点,不会有多个线程活动。
  • newScheduledThreadPool(int corePoolSize): 创建一个可以延迟执行或周期性执行任务的线程池。

小试牛刀:

public class ThreadPoolDemo {
    public static void main(String[] args) {
        
        // 创建一个固定数量的线程池
        ExecutorService executorService = Executors.newFixedThreadPool(2);

        // 提交第一个任务给线程池执行
        executorService.submit(() -> {
            // 打印当前线程的名称,显示它在执行任务
            System.out.println(Thread.currentThread().getName() + " 在执行任务1");
        });

        // 提交第二个任务给线程池执行
        executorService.submit(() -> {
            // 同样打印当前线程的名称,显示它在执行任务
            System.out.println(Thread.currentThread().getName() + " 在执行任务2");
        });

        // 关闭线程池,不接受新任务,已提交的任务继续执行
        executorService.shutdown();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# 3.5 基于ThreadPoolExecutor构建线程池

ThreadPoolExecutor是Java并发框架中提供的一个灵活且强大的线程池实现,允许开发者详细配置线程池的各个方面,包括核心线程数、最大线程数、空闲线程存活时间、任务队列等。它直接实现了ExecutorService接口,并提供了更为丰富的构造器参数,使得开发者可以根据自己的具体需求来创建和管理线程池。

ThreadPoolExecutor中定义了七大核心属性,这些属性是线程池实现的基石。

下载 (5)

线程池的真正实现类是 ThreadPoolExecutor,其构造方法有如下4种:

image-20240316234919779

可以看到,其需要如下几个参数:

  1. corePoolSize(必需):核心线程数。默认情况下,核心线程会一直存活,但是当将 allowCoreThreadTimeout 设置为 true 时,核心线程也会超时回收。
  2. maximumPoolSize(必需):线程池所能容纳的最大线程数。当活跃线程数达到该数值后,后续的新任务将会阻塞。
  3. keepAliveTime(必需):线程闲置超时时长。如果超过该时长,非核心线程就会被回收。如果将 allowCoreThreadTimeout 设置为 true 时,核心线程也会超时回收。
  4. unit(必需):指定 keepAliveTime 参数的时间单位。常用的有:TimeUnit.MILLISECONDS(毫秒)、TimeUnit.SECONDS(秒)、TimeUnit.MINUTES(分)。
  5. workQueue(必需):任务队列。通过线程池的 execute() 方法提交的 Runnable 对象将存储在该参数中。其采用阻塞队列实现。
  6. threadFactory(可选):线程工厂。用于指定为线程池创建新线程的方式。
  7. handler(可选):拒绝策略。当达到最大线程数时需要执行的饱和策略。

核心线程数与最大线程数的关系和区别?

  1. 容量上限:核心线程数定义了线程池的基本大小,最大线程数定义了线程池的最大边界。
  2. 线程回收:默认情况下,核心线程即使处于空闲状态也不会被回收(除非allowCoreThreadTimeout设置为true),而非核心线程(即超过核心线程数的部分)在空闲一定时间(keepAliveTime)后会被回收。
  3. 任务增长响应:当任务数增加,超过核心线程能处理的范围时,线程池可以暂时增加线程到最大线程数来处理增加的任务。一旦任务量减少,超过核心线程数的线程在空闲一定时间后会被回收。

线程池使用流程如下:

public class ThreadPoolExample {

    public static void main(String[] args) {
        // 定义核心线程数
        int CORE_POOL_SIZE = 5;
        // 定义最大线程数
        int MAXIMUM_POOL_SIZE = 10;
        // 定义空闲线程的存活时间
        long KEEP_ALIVE = 1L;

        // 创建任务队列,用于存放等待执行的任务
        BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<>(50);

        // 创建线程工厂,用于创建新线程
        ThreadFactory threadFactory = new ThreadFactory() {
            private final AtomicInteger threadNumber = new AtomicInteger(1);

            @Override
            public Thread newThread(Runnable r) {
                Thread t = new Thread(r, "my-pool-" + threadNumber.getAndIncrement());
                return t;
            }
        };

        // 创建线程池
        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
            CORE_POOL_SIZE, // 核心线程数
            MAXIMUM_POOL_SIZE, // 最大线程数
            KEEP_ALIVE, // 空闲线程等待新任务的最长时间
            TimeUnit.SECONDS, // KEEP_ALIVE的时间单位
            workQueue, // 任务队列
            threadFactory, // 线程工厂
            new ThreadPoolExecutor.DiscardOldestPolicy() // 拒绝策略
        );

        // 向线程池提交任务
        for (int i = 0; i < 20; i++) {
            int finalI = i;
            threadPool.execute(() -> {
                System.out.println(Thread.currentThread().getName() + " 执行任务 " + finalI);
                try {
                    // 模拟任务执行时间
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            });
        }

        // 关闭线程池
        // 逐步关闭线程池,不再接收新任务,但会等待队列里现有任务执行完毕
        // 注意:在实际使用场景中,通常只需要调用shutdown()或shutdownNow()中的一个。
        threadPool.shutdown();
        // 或者,如果需要立即关闭线程池,可以调用shutdownNow()
        // List<Runnable> notExecutedTasks = threadPool.shutdownNow();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57

# 3.6 线程池的工作原理

下面来描述一下线程池工作的原理,提交一个任务到线程池中,线程池的处理流程如下:

  1. 核心线程检查:首先检查当前运行的线程是否少于核心线程数(corePoolSize)。如果是,即使其他工作线程是空闲的,线程池也会创建新的线程来执行新提交的任务。
  2. 任务队列检查:如果当前运行的线程数等于或超过核心线程数,线程池会尝试将提交的任务放入队列。这个队列通常是BlockingQueue类型,用于存放等待执行的任务。
    • 如果任务能成功入队,那么任务暂时会等待在队列中直到有工作线程可用来执行它。
    • 如果任务无法入队(比如队列已满),线程池将进入下一个处理阶段。
  3. 最大线程数检查:如果任务无法入队,线程池会继续检查当前运行的线程数是否少于最大线程数(maximumPoolSize)。
    • 如果当前运行的线程数少于最大线程数,线程池会尝试创建新的线程来执行任务。
    • 如果当前运行的线程数已达到最大线程数,线程池将无法处理更多任务,此时会采用拒绝策略(RejectedExecutionHandler)。
  4. 拒绝策略:当线程池无法接受新任务时(即任务队列已满且已达到最大线程数),将会根据配置的拒绝策略来处理新提交的任务。默认的拒绝策略是抛出RejectedExecutionException异常,但还有其他几种策略可用,比如调用者运行、静默丢弃等。

下载 (3)

总结

线程池的工作原理是先尽量使用核心线程,然后使用任务队列缓存任务,最后在必要时扩展到最大线程数,如果还处理不了就根据拒绝策略处理新提交的任务。这个设计旨在为执行多个任务提供灵活的线程管理方式,同时最大限度地减少资源消耗。

# 4. 线程池的参数

# 4.1 任务队列(workQueue)

任务队列是基于阻塞队列实现的,即采用生产者消费者模式,在 Java 中需要实现 BlockingQueue 接口。但 Java 已经为我们提供了 7 种阻塞队列的实现:

  1. ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列(数组结构可配合指针实现一个环形队列)。
  2. LinkedBlockingQueue: 一个由链表结构组成的有界阻塞队列,在未指明容量时,容量默认为 Integer.MAX_VALUE。
  3. PriorityBlockingQueue: 一个支持优先级排序的无界阻塞队列,对元素没有要求,可以实现 Comparable 接口也可以提供 Comparator 来对队列中的元素进行比较。跟时间没有任何关系,仅仅是按照优先级取任务。
  4. DelayQueue:类似于PriorityBlockingQueue,是二叉堆实现的无界优先级阻塞队列。要求元素都实现 Delayed 接口,通过执行时延从队列中提取任务,时间没到任务取不出来。
  5. SynchronousQueue: 一个不存储元素的阻塞队列,消费者线程调用 take() 方法的时候就会发生阻塞,直到有一个生产者线程生产了一个元素,消费者线程就可以拿到这个元素并返回;生产者线程调用 put() 方法的时候也会发生阻塞,直到有一个消费者线程消费了一个元素,生产者才会返回。
  6. LinkedBlockingDeque: 使用双向队列实现的有界双端阻塞队列。双端意味着可以像普通队列一样 FIFO(先进先出),也可以像栈一样 FILO(先进后出)。
  7. LinkedTransferQueue: 它是ConcurrentLinkedQueue、LinkedBlockingQueue 和 SynchronousQueue 的结合体,但是把它用在 ThreadPoolExecutor 中,和 LinkedBlockingQueue 行为一致,但是是无界的阻塞队列。

注意事项

注意有界队列和无界队列的区别:如果使用有界队列,当队列饱和时并超过最大线程数时就会执行拒绝策略;而如果使用无界队列,因为任务队列永远都可以添加任务,所以设置 maximumPoolSize 没有任何意义。

# 4.2 线程工厂(threadFactory)

线程工厂指定创建线程的方式,需要实现 ThreadFactory 接口,并实现 newThread(Runnable r) 方法。该参数可以不用指定,Executors 框架已经为我们实现了一个默认的线程工厂:

/**
 * The default thread factory.
 */
private static class DefaultThreadFactory implements ThreadFactory {
    // 使用原子整数记录工厂创建的线程池数量,确保每个线程池的编号是唯一的
    private static final AtomicInteger poolNumber = new AtomicInteger(1);
    // 线程组,所有通过这个工厂创建的线程都属于这个组
    private final ThreadGroup group;
    // 记录该工厂创建的线程数量,确保每个线程的编号是唯一的
    private final AtomicInteger threadNumber = new AtomicInteger(1);
    // 线程名称前缀,包含了线程池编号,用于区分不同线程池中的线程
    private final String namePrefix;
 
    // 构造方法
    DefaultThreadFactory() {
        // 获取系统的安全管理器
        SecurityManager s = System.getSecurityManager();
        // 如果存在安全管理器,则使用该管理器的线程组;否则,使用当前线程的线程组
        group = (s != null) ? s.getThreadGroup() :
                              Thread.currentThread().getThreadGroup();
        // 设置线程名称前缀,格式为"pool-N-thread-",其中N是线程池编号
        namePrefix = "pool-" +
                      poolNumber.getAndIncrement() +
                     "-thread-";
    }
 
    // 创建新线程的方法
    public Thread newThread(Runnable r) {
        // 创建新线程,设置线程的线程组、运行任务、线程名称和堆栈大小(这里设置为0,表示忽略这个参数)
        Thread t = new Thread(group, r,
                              namePrefix + threadNumber.getAndIncrement(),
                              0);
        // 如果新线程被创建为守护线程,则设置为非守护线程
        if (t.isDaemon())
            t.setDaemon(false);
        // 如果新线程的优先级不是正常优先级,则设置为正常优先级
        if (t.getPriority() != Thread.NORM_PRIORITY)
            t.setPriority(Thread.NORM_PRIORITY);
        // 返回创建的线程
        return t;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

# 4.3 拒绝策略(handler)

当线程池的线程数达到最大线程数时,需要执行拒绝策略。拒绝策略需要实现 RejectedExecutionHandler 接口,并实现 rejectedExecution(Runnable r, ThreadPoolExecutor executor) 方法。不过 ThreadPoolExecutor 已经为我们实现了 4 种拒绝策略:

使用语法:new ThreadPoolExecutor.DiscardOldestPolicy() // 拒绝策略

  1. AbortPolicy(默认):会直接丢弃任务并抛出 拒绝执行异常(RejectedExecutionException) 。
  2. CallerRunsPolicy:把任务交给提交任务的(main)线程来执行。
  3. DiscardPolicy:丢弃任务,但是不抛出异常。可以配合这种模式进行自定义的处理方式。
  4. DiscardOldestPolicy:丢弃队列最早的未处理任务,然后重新尝试执行任务。

# 5. 详解Executors提供的线程池

下面将详细介绍Executors已经为我们封装好了的 4 种常见的功能线程池,如下:

  1. 定长线程池(FixedThreadPool): 通过Executors.newFixedThreadPool(int nThreads)方法创建。这种类型的线程池具有固定的线程数量。一旦创建,线程池的大小不变。如果所有线程都在工作,额外的任务会在队列中等待。这种类型的线程池适合于负载比较重的服务器。
  2. 定时线程池(ScheduledThreadPool): 通过Executors.newScheduledThreadPool(int corePoolSize)方法创建。这种线程池支持定时及周期性任务执行。比如,可以调度一个任务在某段时间后执行,或者周期性地重复执行。
  3. 可缓存线程池(CachedThreadPool): 通过Executors.newCachedThreadPool()方法创建。这种线程池没有固定大小,可以根据需求自动的增加新的线程,空闲线程会被回收。这种类型的线程池适合执行大量的短期异步任务。
  4. 单线程化线程池(SingleThreadExecutor): 通过Executors.newSingleThreadExecutor()方法创建。这种线程池中只有一个线程,它确保所有的任务都在同一个线程中按顺序执行。适用于需要顺序执行任务的场景,并且也能保证在任意时间点不会有多个线程是活动的。

# 5.1 定长线程池(FixedThreadPool)

创建方法的源码:

// 创建一个固定线程数的线程池
public static ExecutorService newFixedThreadPool(int nThreads) {
    // nThreads: 指定了线程池中的线程数量
    // 0L: 非核心线程的闲置超时时间,这里设置为0表示非核心线程不会因闲置被回收(实际上在FixedThreadPool中不存在非核心线程)
    // TimeUnit.MILLISECONDS: 超时时间的单位,这里用毫秒
    // new LinkedBlockingQueue<Runnable>(): 任务队列,使用链表结构的无界队列来存储待执行的任务
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}

// 这个重载版本允许用户指定一个ThreadFactory来创建新线程
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
    // threadFactory: 用户可以自定义线程创建的方式,例如设置线程名字,设置为守护线程等
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>(),
                                  threadFactory);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
  • 特点:只有核心线程,线程数量固定,执行完立即回收,任务队列为链表结构的有界队列。
  • 应用场景:控制线程最大并发数。

使用示例:

// 1. 创建定长线程池对象 & 设置线程池线程数量固定为3
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
// 2. 创建好Runnable类线程对象 & 需执行的任务
Runnable task =new Runnable(){
  public void run() {
     System.out.println("执行任务啦");
  }
};
// 3. 向线程池提交任务
fixedThreadPool.execute(task);
1
2
3
4
5
6
7
8
9
10

# 5.2 定时线程池(ScheduledThreadPool )

创建方法的源码:

private static final long DEFAULT_KEEPALIVE_MILLIS = 10L;
 
// Executors类中提供的创建定时线程池的静态方法
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
    // 创建一个具有指定核心线程数的ScheduledThreadPoolExecutor实例
    return new ScheduledThreadPoolExecutor(corePoolSize);
}

// ScheduledThreadPoolExecutor的构造方法
public ScheduledThreadPoolExecutor(int corePoolSize) {
    // 调用ThreadPoolExecutor的构造方法,设置线程池参数
    super(corePoolSize, // 核心线程数,也是线程池的最小线程数
          Integer.MAX_VALUE, // 最大线程数设置为Integer的最大值,表示线程数可以非常大
          DEFAULT_KEEPALIVE_MILLIS, // 非核心线程的空闲存活时间,默认为10毫秒
          TimeUnit.MILLISECONDS, // 空闲存活时间的单位,这里为毫秒
          new DelayedWorkQueue()); // 使用DelayedWorkQueue,这是一个无界的延迟队列,用于存放待执行的定时任务
}

// 提供一个重载的构造方法,允许指定一个ThreadFactory
public static ScheduledExecutorService newScheduledThreadPool(
        int corePoolSize, ThreadFactory threadFactory) {
    // 使用自定义的线程工厂来创建线程
    return new ScheduledThreadPoolExecutor(corePoolSize, threadFactory);
}

// ScheduledThreadPoolExecutor的构造方法,接受一个ThreadFactory参数
public ScheduledThreadPoolExecutor(int corePoolSize,
                                   ThreadFactory threadFactory) {
    // 调用ThreadPoolExecutor的构造方法,设置线程池参数,包括自定义的线程工厂
    super(corePoolSize, // 核心线程数
          Integer.MAX_VALUE, // 最大线程数为Integer的最大值
          DEFAULT_KEEPALIVE_MILLIS, // 非核心线程的空闲存活时间
          TimeUnit.MILLISECONDS, // 存活时间单位
          new DelayedWorkQueue(), // 使用DelayedWorkQueue作为工作队列
          threadFactory); // 使用自定义的线程工厂
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
  • 特点:核心线程数量固定,非核心线程数量无限,执行完闲置 10ms 后回收,任务队列为延时阻塞队列。
  • 应用场景:执行定时或周期性的任务。

使用示例:

// 1. 创建 定时线程池对象并设置线程池核心线程数量固定为5
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
// 2. 创建好Runnable类线程对象 & 需执行的任务
Runnable task =new Runnable(){
  public void run() {
     System.out.println("执行任务啦");
  }
};
// 3. 向线程池提交任务
scheduledThreadPool.schedule(task, 1, TimeUnit.SECONDS); // 延迟1s后执行任务
scheduledThreadPool.scheduleAtFixedRate(task,10,1000,TimeUnit.MILLISECONDS);// 延迟10ms后、每隔1000ms执行任务
1
2
3
4
5
6
7
8
9
10
11

# 5.3 可缓存线程池(CachedThreadPool)

创建方法的源码:

// 无参数版本的newCachedThreadPool方法
public static ExecutorService newCachedThreadPool() {
    // 创建并返回一个ThreadPoolExecutor实例,具有以下特性:
    return new ThreadPoolExecutor(
        0, // 核心线程数设置为0,意味着线程池不保留任何空闲线程
        Integer.MAX_VALUE, // 允许创建线程数量的上限设置为Integer的最大值,理论上允许无限创建新线程
        60L, // 空闲线程的存活时间设置为60秒
        TimeUnit.SECONDS, // 存活时间单位为秒
        new SynchronousQueue<Runnable>()); // 使用SynchronousQueue作为工作队列,这种队列不存储元素,每个插入操作必须等待另一个线程的移除操作
}

// 带有ThreadFactory参数的newCachedThreadPool方法
public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
    // 该构造方法允许用户提供一个ThreadFactory来自定义创建线程的方式
    return new ThreadPoolExecutor(
        0, // 核心线程数为0
        Integer.MAX_VALUE, // 最大线程数为Integer的最大值
        60L, // 空闲线程在终止前的最长空闲时间为60秒
        TimeUnit.SECONDS, // 时间单位为秒
        new SynchronousQueue<Runnable>(), // 使用SynchronousQueue作为工作队列
        threadFactory); // 使用用户提供的ThreadFactory来创建新线程
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
  • 特点:无核心线程,非核心线程数量无限,执行完闲置 60s 后回收,任务队列为不存储元素的阻塞队列。
  • 应用场景:执行大量、耗时少的任务。

使用示例:

// 1. 创建可缓存线程池对象
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
// 2. 创建好Runnable类线程对象 & 需执行的任务
Runnable task =new Runnable(){
  public void run() {
     System.out.println("执行任务啦");
  }
};
// 3. 向线程池提交任务
cachedThreadPool.execute(task);
1
2
3
4
5
6
7
8
9
10

# 5.4 单线程化线程池(SingleThreadExecutor)

创建方法的源码:

// 无参数版本的newSingleThreadExecutor方法
public static ExecutorService newSingleThreadExecutor() {
    // 返回一个ExecutorService对象,这里使用了FinalizableDelegatedExecutorService包装了ThreadPoolExecutor
    // 以提供单线程执行环境,并确保线程池的最终化
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(
            1, // 核心线程数设置为1,确保同时只有一个任务在执行
            1, // 最大线程数也设置为1,保持线程池中始终只有一个线程
            0L, // 空闲线程的存活时间设置为0,因为线程数固定,此设置实际上无效
            TimeUnit.MILLISECONDS, // 设置时间单位,这里的设置实际上不影响操作,因为存活时间是0
            new LinkedBlockingQueue<Runnable>() // 使用无界的LinkedBlockingQueue作为工作队列
        ));
}

// 带有ThreadFactory参数的newSingleThreadExecutor方法
public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
    // 与上面相似,但允许用户通过ThreadFactory参数自定义线程的创建
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(
            1, // 核心线程数为1
            1, // 最大线程数为1
            0L, // 空闲线程存活时间为0
            TimeUnit.MILLISECONDS, // 时间单位为毫秒
            new LinkedBlockingQueue<Runnable>(), // 使用LinkedBlockingQueue作为队列
            threadFactory // 使用自定义的线程工厂
        ));
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
  • 特点:只有 1 个核心线程,无非核心线程,执行完立即回收,任务队列为链表结构的有界队列。
  • 应用场景:不适合并发但可能引起 IO 阻塞性及影响 UI 线程响应的操作,如数据库操作、文件操作等。

使用示例:

// 1. 创建单线程化线程池
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
// 2. 创建好Runnable类线程对象 & 需执行的任务
Runnable task =new Runnable(){
  public void run() {
     System.out.println("执行任务啦");
  }
};
// 3. 向线程池提交任务
singleThreadExecutor.execute(task);
1
2
3
4
5
6
7
8
9
10

# 5.5 对比

下载 (4)

# 6. 总结

Executors 的 4 个功能线程池虽然方便,但现在已经不建议使用了,而是建议直接通过使用 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。

其实 Executors 的 4 个功能线程有如下弊端:

  • FixedThreadPool 和 SingleThreadExecutor:这两种线程池默认使用无界的LinkedBlockingQueue作为工作队列。在高负载情况下,未处理的任务可能会不断积累,最终导致内存消耗过大,甚至出现OutOfMemoryError异常。
  • CachedThreadPool 和 ScheduledThreadPool:这两种线程池允许创建的线程数量最大可达Integer.MAX_VALUE,这在理论上意味着线程数量可以无限增长。在实际应用中,过多的线程会消耗大量的系统资源,不仅会降低系统性能,还可能导致系统崩溃。

阿里巴巴的Java开发规范指出,直接使用Executors类提供的快捷方法创建线程池虽然方便,但可能会导致资源耗尽的风险,因此不推荐在生产环境中使用。

image-20240316224302049

以上便是本文的全部内容,本人才疏学浅,文章有什么错误的地方,欢迎大佬们批评指正!我是scholar,一个在互联网行业的小白,立志成为更好的自己。

如果你想了解更多关于scholar (opens new window),可以关注公众号-书生带你学编程,后面文章会首先同步至公众号。

image-20240119044850990

编辑此页 (opens new window)
上次更新: 2024/12/28, 18:32:08
Java多线程详解

← Java多线程详解

Theme by Vdoing | Copyright © 2019-2025 程序员scholar
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式