用线程池,注意避坑

ops/2024/12/14 6:26:41/

前言

线程池是 Java 中处理多线程的强大工具,但它不仅仅是“直接用就完事”的工具。

很多小伙伴在用线程池时,因为配置不当或忽略细节,踩过许多坑。

今天跟大家一起聊聊线程池中容易踩的 10 个坑,以及如何避免这些坑,希望对你会有所帮助。

1. 直接使用 Executors 创建线程池

许多初学者在创建线程池时,直接使用 ​​Executors​​ 提供的快捷方法:

ExecutorService executor = Executors.newFixedThreadPool(10);

​问题在哪?

  • 无界队列newFixedThreadPool 使用的队列是 LinkedBlockingQueue,它是无界队列,任务堆积可能会导致内存溢出。
  • 线程无限增长newCachedThreadPool 会无限创建线程,在任务量激增时可能耗尽系统资源。

​示例:内存溢出的风险

ExecutorService executor = Executors.newFixedThreadPool(2);
for (int i = 0; i < 1000000; i++) {executor.submit(() -> {try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}});
}

任务数远大于线程数,导致任务无限堆积在队列中,最终可能导致 ​​OutOfMemoryError​​。

​解决办法

使用 ​​ThreadPoolExecutor​​,并明确指定参数:

ThreadPoolExecutor executor = new ThreadPoolExecutor(2,4,60L,TimeUnit.SECONDS,new ArrayBlockingQueue<>(100), // 有界队列new ThreadPoolExecutor.AbortPolicy() // 拒绝策略
);

2. 错误配置线程数

很多人随意配置线程池参数,比如核心线程数 10,最大线程数 100,看起来没问题,但这可能导致性能问题或资源浪费。

​示例:错误配置导致的线程过载

ThreadPoolExecutor executor = new ThreadPoolExecutor(10, // 核心线程数100, // 最大线程数60L,TimeUnit.SECONDS,new ArrayBlockingQueue<>(10)
);for (int i = 0; i < 1000; i++) {executor.submit(() -> {try {Thread.sleep(5000); // 模拟耗时任务} catch (InterruptedException e) {e.printStackTrace();}});
}

这种配置在任务激增时,会创建大量线程,系统资源被耗尽。

​正确配置方式

根据任务类型选择合理的线程数:

  • CPU 密集型:线程数建议设置为 CPU 核心数 + 1
  • IO 密集型:线程数建议设置为 2 * CPU 核心数

示例:

int cpuCores = Runtime.getRuntime().availableProcessors();
ThreadPoolExecutor executor = new ThreadPoolExecutor(cpuCores + 1,cpuCores + 1,60L,TimeUnit.SECONDS,new ArrayBlockingQueue<>(50)
);

3. 忽略任务队列的选择

任务队列直接影响线程池的行为。如果选错队列类型,会带来很多隐患。

​常见队列的坑

  • 无界队列:任务无限堆积。
  • 有界队列:队列满了会触发拒绝策略。
  • 优先级队列:容易导致高优先级任务频繁抢占低优先级任务。

​示例:任务堆积导致问题

ThreadPoolExecutor executor = new ThreadPoolExecutor(2,4,60L,TimeUnit.SECONDS,new LinkedBlockingQueue<>()
);for (int i = 0; i < 100000; i++) {executor.submit(() -> System.out.println(Thread.currentThread().getName()));
}

改进方法:用有界队列,避免任务无限堆积。

new ArrayBlockingQueue<>(100);

4. 忘记关闭线程池

有些小伙伴用完线程池后,忘记调用 ​​shutdown()​​,导致程序无法正常退出。

​示例:线程池未关闭

ExecutorService executor = Executors.newFixedThreadPool(5);
executor.submit(() -> System.out.println("任务执行中..."));
// 线程池未关闭,程序一直运行

​正确关闭方式

executor.shutdown();
try {if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {executor.shutdownNow();}
} catch (InterruptedException e) {executor.shutdownNow();
}

5. 忽略拒绝策略

当任务队列满时,线程池会触发拒绝策略,很多人不知道默认策略(​​AbortPolicy​​)会直接抛异常。

​示例:任务被拒绝

ThreadPoolExecutor executor = new ThreadPoolExecutor(1,1,60L,TimeUnit.SECONDS,new ArrayBlockingQueue<>(2),new ThreadPoolExecutor.AbortPolicy() // 默认策略
);for (int i = 0; i < 10; i++) {executor.submit(() -> System.out.println("任务"));
}

执行到第四个任务时会抛出 ​​RejectedExecutionException​​。

​改进:选择合适的策略

  • ​CallerRunsPolicy​​:提交任务的线程自己执行。
  • ​DiscardPolicy​​:直接丢弃新任务。
  • ​DiscardOldestPolicy​​:丢弃最老的任务。

6. 任务中未处理异常

线程池中的任务抛出异常时,线程池不会直接抛出,导致很多问题被忽略。

​示例:异常被忽略

executor.submit(() -> {throw new RuntimeException("任务异常");
});

​解决方法

  1. 捕获任务内部异常:
executor.submit(() -> {try {throw new RuntimeException("任务异常");} catch (Exception e) {System.err.println("捕获异常:" + e.getMessage());}
});
  1. 自定义线程工厂:
ThreadFactory factory = r -> {Thread t = new Thread(r);t.setUncaughtExceptionHandler((thread, e) -> {System.err.println("线程异常:" + e.getMessage());});return t;
};

7. 阻塞任务占用线程池

如果线程池中的任务是阻塞的(如文件读写、网络请求),核心线程会被占满,影响性能。

​示例:阻塞任务拖垮线程池

executor.submit(() -> {Thread.sleep(10000); // 模拟阻塞任务
});

​改进方法

  • 减少任务的阻塞时间。
  • 增加核心线程数。
  • 使用异步非阻塞方式(如 NIO)。

8. 滥用线程池

线程池不是万能的,某些场景直接使用 ​​new Thread()​​ 更简单。

​示例:过度使用线程池

一个简单的短期任务:

ExecutorService executor = Executors.newSingleThreadExecutor();
executor.submit(() -> System.out.println("执行任务"));
executor.shutdown();

这种情况下,用线程池反而复杂。

​改进方式

new Thread(() -> System.out.println("执行任务")).start();

9. 未监控线程池状态

很多人用线程池后,不监控其状态,导致任务堆积、线程耗尽的问题被忽略。

​示例:监控线程池状态

System.out.println("核心线程数:" + executor.getCorePoolSize());
System.out.println("队列大小:" + executor.getQueue().size());
System.out.println("已完成任务数:" + executor.getCompletedTaskCount());

结合监控工具(如 JMX、Prometheus),实现实时监控。

10. 动态调整线程池参数

有些人在线程池设计时忽略了参数调整的必要性,导致后期性能优化困难。

​示例:动态调整核心线程数

executor.setCorePoolSize(20);
executor.setMaximumPoolSize(50);

实时调整线程池参数,能适应业务的动态变化。

总结

线程池是强大的工具,但如果我们日常工作中用得不好也非常容易踩坑。

这篇文章通过实际代码示例,我们可以清楚看到线程池的问题所在及改进方法。

希望这些内容能帮你避免踩坑,写出高质量的线程池代码!

线程池用得好,效率杠杠的;用得不好,程序天天崩!


http://www.ppmy.cn/ops/141739.html

相关文章

【C++初阶】第8课—标准模板库STL(string_2)

文章目录 1. string类对象遍历操作1.1 标准库中的成员函数begin( )和end( )1.2 标准库中的成员函数rbegin( )和rend( )1.3 C11引入的4个标准库中的成员函数 2. string类对象的访问2.1 operator[ ]运算符重载访问字符串字符2.2 公有成员函数at访问字符2.3 公有成员函数back()和f…

基于 Python 的机器学习模型部署到 Flask Web 应用:从训练到部署的完整指南

目录 引言 技术栈 步骤一&#xff1a;数据预处理 步骤二&#xff1a;训练机器学习模型 步骤三&#xff1a;创建 Flask Web 应用 步骤四&#xff1a;测试 Web 应用 步骤五&#xff1a;模型的保存与加载 保存模型 加载模型并在 Flask 中使用 步骤六&#xff1a;Web 应用…

UE5 C+、C++、C# 构造方法区别示例

我们对比一下UE C、C 、C#的构造方法&#xff1a; 1. UE4 C例子&#xff1a; // 声明和构造合并在一起static ConstructorHelpers::FObjectFinder<UTexture2D> CrosshairTexObj(TEXT("/Game/Path"));// 使用加载的资源UTexture2D* Texture CrosshairTexObj.…

yarn 安装问题

Couldn’t find package “regenerator-runtime” on the “npm” registry. Error: Couldn’t find package “watch-size” on the “npm” regist 标题Error: Couldn’t find package “babel-helper-vue-jsx-merge-props” on the “npm” registry. Error: Couldn’t f…

React 第十六节 useCallback 使用详解注意事项

useCallback 概述 1、useCallback 是在React 中多次渲染缓存函数的 Hook&#xff0c;返回一个函数的 memoized的值&#xff1b; 2、如果多次传入的依赖项不变&#xff0c;那么多次定义的时候&#xff0c;返回的值是相同的,防止频繁触发更新&#xff1b; 3、多应用在 父组件为函…

C++面试:HTTP1.0/1.1,HTTP2.0,HTPP3.0的区别

1.你对HTTP1.0/1.1&#xff0c;HTTP2.0&#xff0c;HTPP3.0有什么了解&#xff1f; 答&#xff1a;HTTP1.0&#xff1a; ①属于无连接式&#xff0c;每次发送HTTP请求都需要建立TCP连接。 ②会造成发送时的对头阻塞&#xff0c;当上一个请求没有应答&#xff0c;当前的请求就会…

大数据常用的算法--常用的分类算法

概述 分类算法是根据数据特征来预测数据的类别。 分类算法是一种监督学习&#xff08;Supervised Learning&#xff09;方法&#xff0c;它需要一个已知的类别标签的训练数据集&#xff0c;通过学习这个数据集来预测新的数据点的类别。例如&#xff0c;在电子邮件过滤系统中&am…

PostgreSQL JSON/JSONB 查询与操作指南

PostgreSQL 提供了强大的 JSON 和 JSONB 数据类型及相关操作&#xff0c;适用于存储和查询半结构化数据。本文将详细介绍其常用操作。 1. 基础操作 1.1 JSON 属性访问 ->: 返回 JSON 对象中的值&#xff0c;结果为 JSON 格式。 SELECT {"a": {"b": 1…