Java线程池解读

news2024/12/18 10:20:45

Java 线程池是一个提供多线程管理和调度的工具,通常用来处理多个并发任务。线程池能够帮助有效管理线程的创建、调度、执行和销毁,避免频繁的线程创建和销毁,提高系统性能。

前言

Java 线程池是面试中的常客,面试官经常会问线程池的作用和线程池的创建销毁这些基础问题,本文就Java线程池的基本概念、工作原理、实际案例展开阐述。

一、基本概念

线程池本质上是一个管理线程的容器,它包含了多个线程,可以用来执行多个任务。线程池的核心思想是:复用已创建的线程,避免频繁的线程创建和销毁操作。

为什么要使用线程池

  • 性能稳定性强:通过复用线程,避免频繁创建和销毁线程带来的开销。
  • 提高资源管理效率:线程池管理线程的最大数量,防止线程过多而导致资源竞争和系统崩溃。
  • 配置灵活:线程池的参数可以根据实际需求调整,允许在不同的任务量和工作负载下调整线程池的大小。
  • 支持定时以及周期性任务执行:线程池可以方便地支持定时任务和周期性任务的执行,这对于需要定时执行任务的应用非常有用。

二、核心接口

在Java中,线程池的核心接口和类主要位于java.util.concurrent包中。线程池结构图如下:
在这里插入图片描述

1. Executor 接口

Executor 是线程池的最基本接口,它负责提交任务。定义了执行任务的基本方法。

public interface Executor {
    void execute(Runnable command); // 提交任务给线程池
}

2. ExecutorService 接口

ExecutorServiceExecutor 的子接口,增加了用于管理线程池的方法。常用的方法包括:

  • submit() 提交任务并返回一个 Future 对象,允许获取任务执行结果或取消任务。
  • invokeAll() 提交多个任务并返回一个 List,用于批量处理任务。
  • shutdown()shutdownNow() 用于关闭线程池。
public interface ExecutorService extends Executor {
    <T> Future<T> submit(Callable<T> task); // 提交带返回值的任务
    <T> Future<T> submit(Runnable task, T result); // 提交Runnable任务,并返回结果
    List<Runnable> shutdownNow(); // 强制关闭线程池
    void shutdown(); // 优雅地关闭线程池
}

3. ThreadPoolExecutor 类

ThreadPoolExecutorExecutorService 的核心实现类,提供了灵活配置线程池参数的方法。常用的构造函数如下:

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue)
  • corePoolSize:核心线程数,线程池最小线程数。
  • maximumPoolSize:最大线程数,线程池最多允许的线程数。
  • keepAliveTime:当线程池的线程超过 corePoolSize 时,空闲线程的最大存活时间。
  • unitkeepAliveTime 的时间单位。
  • workQueue:任务队列,用于存储等待执行的任务。

常用线程池类

  • newFixedThreadPool(int nThreads):创建固定大小的线程池,nThreads 为线程数。
  • newCachedThreadPool():创建可缓存的线程池,能够根据需要创建线程,线程空闲时会被回收。
  • newSingleThreadExecutor():创建单线程池,始终只有一个工作线程。
  • newScheduledThreadPool(int corePoolSize):创建定时任务线程池,支持定时和周期性任务。

三、线程池创建关闭

1、使用 ExecutorService 创建和使用线程池

import java.util.concurrent.*;

public class ThreadPoolExample {
    public static void main(String[] args) {
        // 创建一个线程池,最大线程数为 10,核心线程数为 5,任务队列长度为 100
        ExecutorService executorService = new ThreadPoolExecutor(
            5, 10, 60L, TimeUnit.SECONDS,
            new LinkedBlockingQueue<>(100)
        );

        // 提交 10 个任务
        for (int i = 0; i < 10; i++) {
            final int taskId = i;
            executorService.submit(() -> {
                System.out.println("Executing task " + taskId + " by " + Thread.currentThread().getName());
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            });
        }

        // 关闭线程池
        executorService.shutdown();
    }
}

2、直接使用ThreadPoolExecutor类创建线程池

可以直接使用ThreadPoolExecutor类构造器来创建线程池,这样可以更灵活地设置参数,如核心线程数、最大线程数、工作队列、线程工厂、拒绝策略等。

int corePoolSize = 10;
int maximumPoolSize = 50;
long keepAliveTime = 120;
TimeUnit unit = TimeUnit.SECONDS;
BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<>(100);
ThreadFactory threadFactory = Executors.defaultThreadFactory();
RejectedExecutionHandler handler = new ThreadPoolExecutor.AbortPolicy();

ExecutorService threadPool = new ThreadPoolExecutor(
    corePoolSize,
    maximumPoolSize,
    keepAliveTime,
    unit,
    workQueue,
    threadFactory,
    handler
);

3、关闭线程池

  • shutdown():线程池会等到已经提交的任务执行完成后关闭,无法接受新的任务。
  • shutdownNow():线程池尝试停止所有正在执行的任务,并返回待执行任务的列表。

四、线程池工作原理

当一个任务被提交到线程池时,线程池根据其当前的状态决定如何处理任务:

  1. 如果当前工作线程数小于 corePoolSize,线程池会创建一个新的线程来处理任务。
  2. 如果当前工作线程数等于或大于 corePoolSize且队列尚未满,线程池将任务添加到队列中。
  3. 如果当前工作线程数大于 corePoolSize 且队列已满,线程池会创建新的线程,直到达到 maximumPoolSize
  4. 如果所有线程都在工作且队列已满,新的任务将被拒绝,具体的拒绝策略取决于 RejectedExecutionHandler的实现(默认是抛出异常)。

线程池任务处理流程图如下:
在这里插入图片描述

五、线程池异常处理

1. ThreadPoolExecutor 的异常处理机制
在使用 ThreadPoolExecutor 提交任务时,任务执行过程中的异常处理取决于几个因素,尤其是任务的类型(RunnableCallable)以及异常的传播方式。

  • Runnable 接口中的异常
    Runnable 是不返回结果的任务接口,它的 run() 方法不能抛出任何异常。因此,如果在 run() 方法中发生了异常,默认情况下,异常会被吞掉,线程池不会进行任何处理。
    默认行为
    线程池不会捕获 Runnable 中的异常。异常会在执行该任务的线程中被丢弃,不会传播到线程池外部。任务失败的信息不会返回给调用者。如果想要捕获并处理 Runnable 中的异常,你需要在 run() 方法内部手动捕获异常,并进行相应的处理。
Runnable task = () -> {
    try {
        // 任务代码
        throw new RuntimeException("Error in task");
    } catch (Exception e) {
        System.out.println("Exception caught: " + e.getMessage());
        // 处理异常,如记录日志
    }
};
executor.execute(task);
  • Callable 接口中的异常
    Callable 是带有返回值的任务接口,它的 call() 方法允许抛出异常。如果任务中的 call() 方法抛出异常,线程池会将异常封装到 Future 对象中,调用者可以通过 Future.get() 方法获取异常。
    处理 Callable 中的异常
    如果 call() 方法抛出异常,Future.get() 会抛出 ExecutionException,并且原始异常会作为 ExecutionExceptioncause
    可以通过 Future.get() 捕获并处理异常。
Callable<String> task = () -> {
    if (true) {
        throw new RuntimeException("Error in callable task");
    }
    return "Task Completed";
};

ExecutorService executorService = Executors.newFixedThreadPool(1);
Future<String> future = executorService.submit(task);

try {
    // 获取结果,如果有异常会抛出 ExecutionException
    String result = future.get();
    System.out.println(result);
} catch (ExecutionException e) {
    System.out.println("Task failed with exception: " + e.getCause());
} catch (InterruptedException e) {
    Thread.currentThread().interrupt(); // 恢复中断状态
}

2. 线程池的异常处理机制:RejectedExecutionHandler
当线程池的工作队列已满,或线程池的线程数已经达到最大值时,任务会被拒绝执行。这时,线程池使用 RejectedExecutionHandler 来处理拒绝的任务。

RejectedExecutionHandler 是一个接口,它有四个常用实现:

  • AbortPolicy:默认策略,抛出 RejectedExecutionException
  • CallerRunsPolicy:由提交任务的线程来执行该任务。
  • DiscardPolicy:丢弃被拒绝的任务。
  • DiscardOldestPolicy:丢弃队列中最旧的任务,并执行当前任务。

如果线程池中的任务被拒绝执行,RejectedExecutionHandler 会被调用,允许我们自定义如何处理这些被拒绝的任务。可以在此处捕获和处理任务拒绝相关的异常。

ThreadPoolExecutor executor = new ThreadPoolExecutor(
    1, 1, 0L, TimeUnit.MILLISECONDS, 
    new LinkedBlockingQueue<>(1),
    new ThreadPoolExecutor.DiscardOldestPolicy() // 自定义的拒绝策略
);

Runnable task = () -> {
    System.out.println("Executing task");
};

for (int i = 0; i < 5; i++) {
    executor.execute(task); // 会抛出被拒绝的任务
}

3. 自定义异常处理机制
可以通过 ThreadFactory 来为线程池中的每个线程提供自定义的异常处理机制。在 ThreadFactory 创建线程时,可以设置线程的 uncaughtExceptionHandler,这样可以捕获线程执行时未处理的异常。

ThreadFactory threadFactory = new ThreadFactory() {
    @Override
    public Thread newThread(Runnable r) {
        Thread thread = new Thread(r);
        thread.setUncaughtExceptionHandler((t, e) -> {
            System.out.println("Thread " + t.getName() + " failed with exception: " + e.getMessage());
        });
        return thread;
    }
};

ExecutorService executorService = new ThreadPoolExecutor(
    1, 1, 0L, TimeUnit.MILLISECONDS, 
    new LinkedBlockingQueue<>(1),
    threadFactory
);
executorService.submit(() -> {
    throw new RuntimeException("Exception in thread");
});

六、线程池应用场景

1、固定大小线程池

固定线程池FixedThreadPool)是线程池的一种实现方式,它通过固定数量的线程来执行提交的任务。在这种线程池中,线程的数量是固定的,线程池的大小在创建时被设定好,且不会发生变化。即使有大量的任务提交,线程池也只会使用有限数量的线程去处理任务,超出线程池容量的任务将被放入等待队列,直到线程池中的某个线程完成任务并空闲出来,才能继续执行新的任务。适用于任务量比较稳定的场景,可以高效管理线程资源。示意图如下:

在这里插入图片描述
使用 FixedThreadPool 创建线程池

import java.util.concurrent.*;

public class FixedThreadPoolExample {
    public static void main(String[] args) {
        // 创建一个固定大小的线程池,线程数为 3
        ExecutorService executorService = Executors.newFixedThreadPool(3);

        // 提交 5 个任务
        for (int i = 0; i < 5; i++) {
            final int taskId = i;
            executorService.submit(() -> {
                try {
                    System.out.println("Task " + taskId + " is being executed by " + Thread.currentThread().getName());
                    // 模拟任务执行
                    Thread.sleep(1000);
                    System.out.println("Task " + taskId + " completed by " + Thread.currentThread().getName());
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            });
        }

        // 关闭线程池
        executorService.shutdown();
    }
}

2、缓存型线程池

CachedThreadPool 是一种能够根据需求动态创建线程的线程池实现,其特点是当任务较少时,线程池的大小可以非常小,甚至为 0;当任务增多时,线程池可以根据需要动态创建新的线程来处理任务。线程池中的线程在任务完成后不会立即销毁,而是会被缓存一段时间。如果有新的任务提交,线程池会复用这些空闲的线程;如果任务长时间没有提交,空闲的线程会被销毁。适用于任务量不稳定的场景,可以根据任务需求动态增加线程数,线程空闲时会被回收。示意图如下:
在这里插入图片描述

使用 CachedThreadPool 创建线程池

ExecutorService executorService = Executors.newCachedThreadPool();

3、定时任务调度线程池

调度线程池可以设定一个周期,按照这个周期重复执行任务。适用于需要定时或周期性执行任务的场景。示意图如下:

在这里插入图片描述
创建 ScheduledThreadPool

import java.util.concurrent.*;

public class ScheduledThreadPoolExample {
    public static void main(String[] args) {
        // 创建一个大小为 3 的调度线程池
        ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(3);

        // 1. 延迟 2 秒执行一次任务
        scheduledExecutorService.schedule(() -> {
            System.out.println("Task with delay executed at " + System.currentTimeMillis());
        }, 2, TimeUnit.SECONDS);

        // 2. 每 3 秒执行一次任务
        scheduledExecutorService.scheduleAtFixedRate(() -> {
            System.out.println("Periodic task executed at " + System.currentTimeMillis());
        }, 1, 3, TimeUnit.SECONDS);

        // 3. 每 3 秒执行一次任务,首次任务会在延迟 1 秒后执行
        scheduledExecutorService.scheduleWithFixedDelay(() -> {
            System.out.println("Periodic task with fixed delay executed at " + System.currentTimeMillis());
        }, 1, 3, TimeUnit.SECONDS);

        // 4. 等待一段时间后关闭线程池
        try {
            Thread.sleep(10000); // 等待 10 秒钟,让任务有时间执行
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        
        scheduledExecutorService.shutdown();
    }
}

六、注意事项

  • 线程池大小设置:根据系统的硬件资源(如CPU核心数)和任务的类型,适当调整线程池的大小。线程池过大会带来资源浪费,过小则可能导致任务执行延迟。
  • 合理选择队列:使用适当的队列类型(如 LinkedBlockingQueueArrayBlockingQueue)可以优化线程池性能。
  • 线程池监控与调试:在生产环境中,监控线程池的状态(如活跃线程数、等待任务数等)对于调优非常重要。

总结

Java 线程池是处理多线程并发任务的重要工具,能够有效管理线程的生命周期,提高性能和资源利用率。通过合理配置线程池参数和任务队列,可以根据业务需求优化线程池的性能。在实际开发中,需要根据任务的特点和系统的资源状况,选择合适的线程池类型和配置。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2261535.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

如何为IntelliJ IDEA配置JVM参数

在使用IntelliJ IDEA进行Java开发时&#xff0c;合理配置JVM参数对于优化项目性能和资源管理至关重要。IntelliJ IDEA提供了两种方便的方式来设置JVM参数&#xff0c;以确保你的应用程序能够在最佳状态下运行。本文将详细介绍这两种方法&#xff1a;通过工具栏编辑配置和通过服…

【CC2530开发基础篇】继电器模块使用

一、前言 1.1 开发背景 本实验通过使用CC2530单片机控制继电器的吸合与断开&#xff0c;深入了解单片机GPIO的配置与应用。继电器作为一种常见的电气控制元件&#xff0c;广泛用于自动化系统中&#xff0c;用于控制大功率负载的开关操作。在本实验中&#xff0c;将通过GPIO口…

通过解调使用正则化相位跟踪技术进行相位解包裹

1. 绪论 光学计量学通常使用光学干涉仪来测量各种物理量。1,2 根据应用的不同&#xff0c;可以使用多种类型的干涉仪&#xff0c;但它们的共同目标是产生一个由被测物理量调制的条纹图案。使用这种光束编码程序可以检测到的物理量范围非常广&#xff1a;深度测量、应变分析、温…

数字图像处理技术期末复习

1. 已知图像的分辨率和深度&#xff0c;怎么求图像的存储空间&#xff08;位&#xff0c;字节&#xff0c;KB&#xff09;&#xff1f; 题目&#xff1a; 已知图像的分辨率和深度&#xff0c;怎么求图像的存储空间&#xff08;位&#xff0c;字节&#xff0c;KB&#xff09;&a…

Elasticsearch 架构及 Lucene 索引结构原理入门

文章目录 Elasticsearch 整体架构Lucene 索引结构Lucene 倒排索引核心原理倒排索引倒排表&#xff08;Posting List&#xff09; Elasticsearch 整体架构 一个 ES Index 在集群模式下&#xff0c;有多个Node&#xff08;节点&#xff09;组成&#xff0c;每个节点就是ES的 inst…

人脸检测的若干思考!!!

1.目前主要有人脸检测方法分类&#xff1f; 主要包含两类&#xff1a;传统人脸检测算法和基于深度学习的人脸检测算法。 传统人脸检测算法主要可以分为4类&#xff1a; 基于知识、模型、特征和外观的人脸检测方法&#xff1b; 基于深度学习的方法&#xff1a;基于级联CNN的人脸…

突破时间与空间限制的富媒体百宝箱——智能工具箱:让云上内容生产更easy

“这是你的同款日常吗&#xff1f;老是在赶deadline&#xff0c;苦练PS还未出师&#xff0c;premiere、达芬奇真的好难&#xff0c;学python脑容量确实不够~打工人太难了~~” 来试试智能工具箱吧&#xff01;即来即用&#xff0c;一键实现办公自由。图片工具、视频工具、音频工…

el-table打印PDF预览,表头错位的解决方案

文章目录 背景与需求需求分析解决方案方案一&#xff1a;vue-print-nb插件安装引入使用 方案二安装使用 方案三 总结 背景与需求 本例以vue2项目为例&#xff0c;vue3与react等同理。 有个项目需要打印的功能&#xff0c;网页使用vue2写的&#xff0c;主体内容为表格el-table&a…

【算法day16】二叉树:搜索二叉树的修剪与构建

题目引用 修剪二叉搜索树将有序数组转换为二叉搜索树把二叉搜索树转换为累加树 1. 修剪二叉搜索树 给你二叉搜索树的根节点 root &#xff0c;同时给定最小边界low 和最大边界 high。通过修剪二叉搜索树&#xff0c;使得所有节点的值在[low, high]中。修剪树 不应该 改变保留在…

java中File类

1、介绍 File类定义了一些与平台无关的方法来操作文件&#xff0c;可以通过调用File类中的方法&#xff0c;实现创建、删除、重命名文件等操作。File类的对象主要用来获取文件本身的一些信息&#xff0c;如文件所在的目录、文件长度、文件读写权限等。数据流可以将数据写入到文…

金碟中间件-AAS-V10.0安装

金蝶中间件AAS-V10.0 AAS-V10.0安装 1.解压AAS-v10.0安装包 unzip AAS-V10.zip2.更新license.xml cd /root/ApusicAS/aas# 这里要将license复制到该路径 [rootvdb1 aas]# ls bin docs jmods lib modules templates config domains …

易语言OCR证件照文字识别

一.引言 文字识别&#xff0c;也称为光学字符识别&#xff08;Optical Character Recognition, OCR&#xff09;&#xff0c;是一种将不同形式的文档&#xff08;如扫描的纸质文档、PDF文件或数字相机拍摄的图片&#xff09;中的文字转换成可编辑和可搜索的数据的技术。随着技…

Haproxy 高可用代理原理配置(Haproxy High Availability Proxy Principle Configuration)

Haproxy 高可用代理原理 简介 Haproxy是一个开源的高可用性负载均衡解决方案&#xff0c;提供基于TCP和HTTP的应用代理服务。它支持高并发连接&#xff0c;能够处理大量的请求&#xff0c;特别适合高负载站点和需要会话保持的应用场景。 主要特点 ‌高可用性‌&#xff1a;…

企业微信可信域名个人配置方法,个人添加企业微信可信IP方法5.0版本,无论是否企业认证都通用。

自动22年11月份开始更新企业微信可信域名配置方法后&#xff0c;先后阿里&#xff08;22年11月&#xff09;1.0可信域名配置方法、腾讯&#xff08;2022年12月14日&#xff09;2.0版本可信域名配置方法、百度&#xff08;2023年1月27日&#xff09;、华为&#xff08;2023年2月…

Endnote | 查看文献所在分组

软件版本&#xff1a;Endnote X8 第一种方式&#xff1a; 在文献上右键——记录摘要&#xff0c;即可在弹出页面上看到自定义和智能组的分组情况。 第二种方式&#xff1a; 在菜单栏点击文献——记录摘要&#xff0c;也可以查看分组情况。 注&#xff1a; 新版本的endnote软件…

力扣-图论-15【算法学习day.65】

前言 ###我做这类文章一个重要的目的还是给正在学习的大家提供方向和记录学习过程&#xff08;例如想要掌握基础用法&#xff0c;该刷哪些题&#xff1f;&#xff09;我的解析也不会做的非常详细&#xff0c;只会提供思路和一些关键点&#xff0c;力扣上的大佬们的题解质量是非…

练习题:一维数组

练习题 第一题 键盘录入一组数列&#xff0c;利用冒泡排序将数据由大到小排序 代码 #include <stdio.h>int arr_home01() {int arr[10];int i,j,temp;printf("请输入10个测试整数&#xff1a;\n");int len sizeof(arr) / sizeof(arr[0]);for(i 0;i < …

Kerberos身份验证

Kerberos是更现代化的身份验证协议&#xff0c;它比 NTLM 认证更安全&#xff0c;但域内某些服务仍支持 NTLM 认证。Kerberos 和 NTLM 认证一样&#xff0c;都是通过在 SSPI 接口实现的功能&#xff0c;这使得使用第三方协议&#xff08;如&#xff1a;HTTP、SMB、LDAP&#xf…

Kotlin复习

一、Kotlin类型 1.整数 2.浮点 显示转换&#xff1a; 所有数字类型都支持转换为其他类型&#xff0c;但是转换前会检测长度。 toByte(): Byte toShort(): Short toInt(): Int toLong(): Long toFloat(): Float toDouble(): Double 不同进制的数字表示方法&#xff08;为了提高…

12月17日作业

#include <myhead.h>int main(int argc, const char *argv[]) {int pipefd[2];char buff[1024] "hello world";char s[1024];if(pipe(pipefd)-1){perror("pipe");return -1;}//读端pipefd[0] 写端pipefd[1]pid_t pid fork();//创建子进程if(pid0){…