Java线程池的使用和最佳实践

news2025/3/10 18:35:20

第1章:引言

处理并发问题时,如果每次都新建线程,那系统的压力得有多大?这时候,线程池就像一个英雄一样出现了,它帮我们有效地管理线程,提高资源利用率,降低开销。那么,为什么说线程池这么重要呢?首先,线程池能够控制系统中执行线程的数量,这样就减少了线程创建和销毁的开销,提高了系统的响应速度。其次,通过合理的配置,线程池能够提供更好的系统稳定性,避免因为线程数量过多而导致系统崩溃。

小黑今天就带咱们一起深入了解Java中的线程池,看看它是怎么回事,怎么用,以及如何在我们的代码里发挥最大的效力。

第2章:线程池的基本原理

要想彻底搞懂线程池,咱们得先弄明白它的基本原理。线程池,顾名思义,就是存放线程的池子。但它不仅仅是简单的存放,更重要的是它对线程进行了有效的管理。在Java中,线程池通过Executor接口和其实现类ThreadPoolExecutor来提供。

说到底,线程池的核心思想就是复用已有线程。当任务来临时,线程池会尝试使用已存在的线程,而不是每次都新建。如果所有线程都在忙,线程池会根据配置决定是创建新线程,还是放到一个队列中等待。这就大大减少了线程创建和销毁的开销,提高了响应速度。

现在,咱们来看一段示例代码,理解线程池的创建和使用:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadPoolExample {
    public static void main(String[] args) {
        // 创建一个固定大小的线程池
        ExecutorService executor = Executors.newFixedThreadPool(5);

        for (int i = 0; i < 10; i++) {
            Runnable worker = new WorkerThread("" + i);
            executor.execute(worker);
        }

        executor.shutdown();
        while (!executor.isTerminated()) {
        }

        System.out.println("所有任务已完成");
    }
}

class WorkerThread implements Runnable {
    private String command;

    WorkerThread(String s) {
        this.command = s;
    }

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + " 开始. 命令 = " + command);
        processCommand();
        System.out.println(Thread.currentThread().getName() + " 结束.");
    }

    private void processCommand() {
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    @Override
    public String toString() {
        return this.command;
    }
}

在这个例子中,咱们创建了一个固定大小的线程池,并提交了10个任务。这就是线程池的魅力所在:管理和复用线程,让咱们的程序更加高效。

PS: 小黑收集整理了一份超级全面的复习面试资料包,在这偷偷分享给你~
链接:https://sourl.cn/gUV3UP 提取码:fjb3

第3章:Java中的线程池类型

Java提供了几种不同类型的线程池,每种都有它的特点和用途。这一章节,小黑要带大家了解这些类型,看看它们各自适合什么场景。

1. 固定数量线程池(FixedThreadPool)

先说说FixedThreadPool。顾名思义,这种线程池的线程数量是固定的。它适合于负载相对平稳的场景,线程数量不变意味着不会频繁地创建和销毁线程,效率比较高。

ExecutorService fixedThreadPool = Executors.newFixedThreadPool(5);
// 使用这个线程池来执行任务
2. 可缓存线程池(CachedThreadPool)

然后是CachedThreadPool,这个线程池可以根据需要创建新线程,但如果之前创建的线程可用,就会重用它们。它非常适合于任务数量动态变化的场景。

ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
// 适用于任务数动态变化的情况
3. 单线程化线程池(SingleThreadExecutor)

SingleThreadExecutor,这个线程池里只有一个线程在工作。它保证了所有任务都在同一个线程按顺序执行,这对于需要保证执行顺序的场景非常有用。

ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
// 用于需要顺序执行任务的场景
4. 定时及周期性任务线程池(ScheduledThreadPool)

最后是ScheduledThreadPool,这个线程池特别适合需要执行定时或周期性任务的场景。你可以设定任务在指定的延迟后执行,或者定期执行。

ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
// 使用这个线程池来执行定时或周期性任务

咱们看到,Java给我们提供了不同类型的线程池,每一种都有其独特的使用场景。选择正确的线程池类型,可以大大提高程序的性能和稳定性。不过记住,不同类型的线程池适用于不同的应用场景,咱们在选择时要根据实际情况来定。这样,咱们就能把线程池的潜力发挥到极致!

第4章:创建自定义线程池

接下来小黑要和大家聊聊如何创建自定义线程池。虽然Java提供了几种现成的线程池,但有时候咱们需要根据具体情况来定制自己的线程池。这就需要用到ThreadPoolExecutor类。

ThreadPoolExecutor类提供了丰富的构造器,让我们可以精细地控制线程池的行为,比如线程数量、存活时间、工作队列等。现在,小黑就来给大家展示一下如何使用这个类来创建一个符合自己需求的线程池。

首先,咱们来看看ThreadPoolExecutor构造器的参数:

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler)
  • corePoolSize:核心线程数,即即使线程是空闲的,线程池也会保持存活的线程数。
  • maximumPoolSize:线程池允许的最大线程数。
  • keepAliveTime:当线程数超过核心线程数时,多余的空闲线程的存活时间。
  • unitkeepAliveTime的时间单位。
  • workQueue:工作队列,用于存放待执行的任务。
  • threadFactory:线程工厂,用于创建线程。
  • handler:拒绝策略,当线程池和工作队列都满了,如何处理新加入的任务。

下面是一个创建自定义线程池的示例:

import java.util.concurrent.*;

public class CustomThreadPoolExample {
    public static void main(String[] args) {
        int corePoolSize = 5;
        int maxPoolSize = 10;
        long keepAliveTime = 5000;

        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                corePoolSize,
                maxPoolSize,
                keepAliveTime,
                TimeUnit.MILLISECONDS,
                new LinkedBlockingQueue<Runnable>(),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.CallerRunsPolicy()
        );

        // 提交任务到线程池
        for (int i = 0; i < 20; i++) {
            executor.execute(new Task("" + i));
        }

        executor.shutdown();
    }
}

class Task implements Runnable {
    private String name;

    public Task(String name) {
        this.name = name;
    }

    public void run() {
        System.out.println("Executing : " + name);
    }
}

在这个例子中,小黑创建了一个核心线程数为5,最大线程数为10的线程池。线程空闲时间设置为5000毫秒,使用了LinkedBlockingQueue作为工作队列,当线程池和队列都满了的时候,使用CallerRunsPolicy策略。

通过这种方式,咱们就可以根据实际需求创建一个最适合自己的线程池了。记住,合理配置线程池的参数对于提高程序的性能和稳定性至关重要。

第5章:线程池的关键配置及其最佳实践

走到这一步,咱们已经知道了如何创建线程池,那接下来小黑要和大家聊聊线程池的关键配置和一些最佳实践。这些配置和实践可以帮助咱们更好地使用线程池,提高程序的性能和稳定性。

核心线程数(corePoolSize)

核心线程数是线程池中始终保持活跃的线程数量,即使它们没有任务在执行。设置这个值时,咱们要考虑到系统资源的限制和任务的实际需求。如果设置得太高,可能会浪费系统资源;设置得太低,又可能导致处理能力不足。

最大线程数(maximumPoolSize)

最大线程数定义了线程池可以创建的最大线程数量。当工作队列满了之后,线程池会开始创建新线程,直到达到这个数值。合理的设置这个值对于防止系统过载非常重要。

空闲线程的存活时间(keepAliveTime)

当线程池中线程数量超过核心线程数时,多余的线程会在空闲一定时间后被终止,这个时间就是空闲线程的存活时间。这个配置可以帮助系统在不忙碌的时候释放资源。

工作队列(workQueue)

工作队列用于存放等待执行的任务。队列的类型对于线程池的行为有很大影响。例如,LinkedBlockingQueue通常用于固定大小的线程池,而SynchronousQueue适用于缓存线程池。

拒绝策略(RejectedExecutionHandler)

当线程池和工作队列都满了,我们必须定义一个拒绝策略来处理新加入的任务。Java提供了几种标准的拒绝策略,如AbortPolicy(抛出异常)、CallerRunsPolicy(在调用者的线程中执行任务)等。

最佳实践
  1. 正确估算线程需求:根据任务的性质(CPU密集型、IO密集型)和系统环境来合理设置核心线程数和最大线程数。
  2. 合理选择工作队列:根据任务的数量和类型选择适合的队列类型。
  3. 合理配置拒绝策略:根据业务需求选择合适的拒绝策略。
  4. 监控线程池状态:定期监控线程池的状态,包括线程数量、活跃度、任务数量等,以便及时调整配置。

通过以上这些配置和实践,咱们可以更有效地管理线程池,确保应用程序的高效稳定运行。记住,没有一成不变的规则,关键在于根据实际情况灵活调整。

第6章:线程池的常见问题及解决策略

本章和大家探讨一下线程池可能遇到的一些常见问题以及解决这些问题的策略。理解这些问题及其解决方法对于确保线程池稳定高效地运行至关重要。

线程池的常见问题
  1. 线程饥饿死锁:当线程池中的线程都在等待其他任务完成,而这些任务也需要线程池中的线程来执行时,就会发生线程饥饿死锁。

  2. 资源耗尽:如果线程池的最大线程数设置得过高,可能会耗尽系统资源,导致性能下降,甚至崩溃。

  3. 任务拒绝:当线程池满了且工作队列也满时,新提交的任务会被拒绝。

  4. 线程泄露:在某些情况下,线程可能因为未能正确处理异常而永远卡在某个状态,导致线程泄露。

解决策略
  1. 避免线程饥饿死锁:合理配置核心线程数和最大线程数,确保有足够的线程来处理任务。另一种策略是使用不同的线程池来处理不同类型的任务。

  2. 资源管理:合理设置最大线程数和工作队列的大小,以避免资源耗尽。监控系统的性能指标,如CPU和内存使用率,可以帮助及时调整线程池配置。

  3. 合理的拒绝策略:选择合适的拒绝策略,如CallerRunsPolicy,可以让提交任务的线程自己执行该任务,从而降低对线程池的压力。

  4. 异常处理:确保任务执行过程中的异常被妥善处理,避免线程因异常而无法继续执行其他任务。

让我们来看一个简单的示例,展示如何处理任务执行中的异常:

public class SafeTask implements Runnable {
    @Override
    public void run() {
        try {
            // 执行任务的逻辑
        } catch (Exception e) {
            // 处理异常
        }
    }
}

// 使用线程池执行任务
ExecutorService executor = Executors.newFixedThreadPool(5);
executor.execute(new SafeTask());

在这个例子中,SafeTask类中的run方法内部处理了可能发生的异常,这样即使在执行任务时出现异常,线程也不会因此退出,而是可以继续执行其他任务。

理解并解决这些常见问题,将有助于我们更好地利用线程池,保持应用程序的稳定和高效。

第7章:实际案例分析

接下来小黑要和大家分享一些实际的线程池使用案例。通过这些案例,咱们可以更好地理解线程池在实际项目中是如何发挥作用的。

案例一:Web服务器处理请求

想象一下,咱们有一个Web服务器,它需要处理成百上千的并发请求。如果为每个请求创建一个新线程,系统很快就会因为线程过多而崩溃。这时,线程池就派上用场了。

// 创建一个固定大小的线程池
ExecutorService pool = Executors.newFixedThreadPool(100);

// 模拟处理请求
for (int i = 0; i < 1000; i++) {
    pool.execute(new HttpHandler());
}

// HttpHandler类处理实际的请求
class HttpHandler implements Runnable {
    public void run() {
        // 处理HTTP请求的逻辑
    }
}

在这个案例中,线程池限制了同时处理的请求数量,保证了系统的稳定性。

案例二:数据处理和分析

假设小黑现在有一个任务是处理大量数据并进行分析。这些数据处理任务是独立的,可以并行执行以提高效率。

// 创建一个可缓存的线程池
ExecutorService pool = Executors.newCachedThreadPool();

// 模拟数据处理任务
for (Data data : dataList) {
    pool.execute(new DataProcessor(data));
}

// DataProcessor类处理数据
class DataProcessor implements Runnable {
    private Data data;

    DataProcessor(Data data) {
        this.data = data;
    }

    public void run() {
        // 数据处理逻辑
    }
}

在这个案例中,可缓存的线程池可以根据需要创建新线程,从而提高了数据处理的效率。

通过这些案例,咱们可以看到,线程池在不同场景下如何有效地提高系统性能,同时保证稳定性和可靠性。记住,理论知识很重要,但将知识应用到实际问题中才能真正理解和掌握它。

第8章:总结

线程池是Java并发编程中非常强大的工具,它能有效地管理线程,提高资源利用率,增强程序的响应速度。但同时,合理配置和使用线程池也非常关键,这关系到程序的性能和稳定性。咱们在使用线程池时,要考虑到核心线程数、最大线程数、工作队列、线程存活时间以及拒绝策略等多个方面。


面对寒冬,我们更需团结!小黑收集整理了一份超级强大的复习面试资料包,也强烈建议你加入我们的Java后端报团取暖群,一起复习,共享各种学习资源,互助成长。无论是新手还是老手,这里都有你的位置。在这里,我们共同应对职场挑战,分享经验,提升技能,闲聊副业,共同抵御不确定性,携手走向更稳定的职业未来。让我们在Java的路上,不再孤单!进群方式以及资料,点击如下链接即可获取!

链接:https://sourl.cn/gUV3UP 提取码:fjb3

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

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

相关文章

还搞不懂什么是参数,超参数吗?三分钟快速了解参数与超参数的概念和区别!!!

文章目录 前言一、参数是什么&#xff1f;二、超参数是什么三&#xff0c;常使用的超参数有哪些 前言 参数是模型中可被学习和调整的参数&#xff0c;通过训练数据进行学习和优化&#xff1b; 而超参数则是手动设置的参数&#xff0c;用于控制模型的行为和性能&#xff0c;超…

探秘Python FastAPI、Sanic、Tornado 与Golang Gin性能之战!

更多资料获取 &#x1f4da; 个人网站&#xff1a;ipengtao.com Python和Golang作为两种流行的编程语言&#xff0c;都拥有强大的异步框架&#xff0c;为开发者提供了在构建高性能应用时的选择。在Python阵营中&#xff0c;FastAPI、Sanic、Tornado等框架因其异步特性和高效的…

viple模拟器使用(四):unity模拟器中实现两距离局部最优迷宫算法

名字解读 两距离&#xff1a;指的是左侧距离和右侧距离 局部最优&#xff1a;对当前状态来说最好的选择&#xff0c;至于整体能不能达到最优&#xff0c;是无法确定的。 从节点1到节点5&#xff0c;一共有3条路 第1条路线&#xff1a;1→2→4→5&#xff0c;对应的花销是&…

LeetCode刷题---反转链表

个人主页&#xff1a;元清加油_【C】,【C语言】,【数据结构与算法】-CSDN博客 个人专栏&#xff1a;http://t.csdnimg.cn/ZxuNL http://t.csdnimg.cn/c9twt 前言&#xff1a;这个专栏主要讲述递归递归、搜索与回溯算法&#xff0c;所以下面题目主要也是这些算法做的 我讲述…

Linux 基础认识

文章目录 前言Linux历史window历史Linux地位发行版本 前言 建议只看概述 Linux历史 概述&#xff1a; 由一个研究生受Minix操作系统启发编写的&#xff0c;因为功能实用&#xff0c;代码开源被世界人接收和开发 &#xff0c;最终正式发布 。 详情&#xff1a; 1991年10月5日…

JavaSE学习路线及经验所谈

前言 一.学习框架二.学习经验 相信很多小白刚开始学习Java时&#xff0c;都是靠自己在网上搜集资料&#xff0c;并没有明确规划&#xff0c;不知道要学习什么内容&#xff0c;也不知道学习的重点是什么&#xff0c;那么这篇文章会给你一个大致的指引&#xff0c;当然也可以作为…

Apache Doris 详细教程(二)

5、doris的查询语法 5.1、doris查询语法整体结构 SELECT [ALL | DISTINCT | DISTINCTROW ] -- 对查询字段的结果是否需要去重&#xff0c;还是全部保留等参数 select_expr [, select_expr ...] -- select的查询字段 [FROM table_references [PARTITION…

Project 1: The Game of Hog(CS61A)

&#xff08;第一阶段&#xff09;问题 5a&#xff08;3 分&#xff09; 实现该函数&#xff0c;该函数模拟了完整的 Hog 游戏。球员 交替轮流掷骰子&#xff0c;直到其中一名玩家达到分数。playgoal 您现在可以忽略 Feral Hogs 规则和论点; 您将在问题 5b 中实现它。feral_h…

微信小程序:调用 摄像头、选择照片或视频 都没反应 / wx.chooseImage 选择上传图片无反应

一、问题描述 微信小程序 调用 摄像头、选中的照片或视频&#xff0c;都没反应 wx.chooseImage 选择上传图片无反应 二、问题解决 2.1、设置 登录后台关联&#xff0c;点“设置” 2.2、服务内容声明 服务内容声明&#xff0c;用户隐私保护指引&#xff0c;更新 2.3、添加…

React立即更新DOM

正常情况下&#xff0c;react会等待set完毕后再进行页面渲染&#xff0c;所以在set时无法拿到更新后的dom import { useRef, useState } from "react"export default () > {const div useRef(null)const [count, setCount] useState(0)const btnClick () >…

手写实现一个动态代理框架

手写实现一个动态代理框架 什么是代理模式什么是动态代理动态代理中的编译、类加载与对象实例化手写实现一个动态代理框架实现细节DynamicProxyHandlerProxy生成代码写入代码到磁盘文件调用编译器进行编译调用类加载器进行类加载反射实例化删除前面生成的java文件和class文件 C…

LeetCode - 100. 相同的树 (C语言,二叉树,配图,简单)

利用分治思想&#xff0c;将大问题化解成为小问题&#xff0c;我们只需要比较&#xff1a; 1. 根节点的同时为空/不为空&#xff0c;不为空时值相等。 2. 左子树是否一样。 3. 右子树是否一样。 可以看出&#xff0c;这道题非常简单&#xff0c;但是为什么我们还要将它呢&#…

Linux 上的容器技术

容器实现封闭的环境主要要靠两种技术&#xff0c;一种是看起来是隔离的技术&#xff0c;称为 namespace&#xff08;命名空间&#xff09;。在每个 namespace 中的应用看到的&#xff0c;都是不同的 IP 地址、用户空间、进程 ID 等。另一种是用起来是隔离的技术&#xff0c;称为…

【系统运维】Centos部署Haproxy+Keepalived+RabbitMQ高可用集群

1.RabbitMQ高可用集群方案 &#xff08;1&#xff09;RabbitMQ搭建集群的作用&#xff1a;提高可用性、可靠性和处理能力&#xff0c;确保系统提供高效的消息传递服务 高可用性&#xff1a;通过集群&#xff0c;即使其中一个节点发生故障&#xff0c;其他节点仍然可以继续提供…

Elasticsearch:什么是向量数据库?

向量数据库定义 向量数据库是将信息存储为向量的数据库&#xff0c;向量是数据对象的数值表示&#xff0c;也称为向量嵌入。 它利用这些向量嵌入的强大功能来对非结构化数据和半结构化数据&#xff08;例如图像、文本或传感器数据&#xff09;的海量数据集进行索引和搜索。 向…

简明指南:使用Kotlin和Fuel库构建JD.com爬虫

概述 爬虫&#xff0c;作为一种自动化从网络上抓取数据的程序&#xff0c;广泛应用于数据分析、信息提取以及竞争对手监控等领域。不同的实现方式和编程语言都能构建出高效的爬虫工具。在本文中&#xff0c;我们将深入介绍如何充分利用Kotlin和Fuel库&#xff0c;构建一个简单…

Autosar COM通信PDU

文章目录 Autosar 中各个PDU所在示意图PDU的分类PDU 和 SDU 的关系I-PDUN-PDUL-PDU相关协议其他参考 Autosar 中各个PDU所在示意图 PDU的分类 在Autosar 中&#xff0c;主要有 I-PDU、N-PDU和 L-PDU 三种。 L-PDU&#xff1a;Data Link Layer PDU&#xff0c;数据链路层PDUN-…

Qt/QML编程学习之心得:如何添加资源文件到QML工程(十一)

Qt作为一种GUI界面编辑工具&#xff0c;在嵌入式编程中也大受欢迎&#xff0c;而进一步QML出现了&#xff0c;QML我理解也是一种资源文件&#xff0c;因为像其他资源文件一样添加进工程的。那么一个图片如何增加进资源文件呢&#xff1f;这个的确很基础&#xff0c;就是把资源文…

JavaEE 多线程

JavaEE 多线程 文章目录 JavaEE 多线程引子多线程1. 特性2. Thread类2.1 概念2.2 Thread的常见构造方法2.3 Thread的几个常见属性2.4 启动一个线程2.5 中断一个线程2.6 等待一个线程2.7 获取当前线程引用2.8 休眠当前线程 3. 线程状态 引子 当进入多线程这一块内容时&#xff…

Redis中分布式锁的使用

在分布式系统中&#xff0c;如果使用JVM中的同步锁在高并发的场景下仍然会产生线程安全问题。首先我们来查看在多个服务器时为什么会产生线程安全问题&#xff0c;有这样一个案例&#xff0c;有一件商品购买规则为一个用户只能购买一次&#xff0c;如果使用同步锁锁住用户id&am…