22 | Executor与线程池:如何创建正确的线程池?
王宝令
该思维导图由 AI 生成,仅供参考
虽然在 Java 语言中创建线程看上去就像创建一个对象一样简单,只需要 new Thread() 就可以了,但实际上创建线程远不是创建一个对象那么简单。创建对象,仅仅是在 JVM 的堆里分配一块内存而已;而创建一个线程,却需要调用操作系统内核的 API,然后操作系统要为线程分配一系列的资源,这个成本就很高了,所以线程是一个重量级的对象,应该避免频繁创建和销毁。
那如何避免呢?应对方案估计你已经知道了,那就是线程池。
线程池的需求是如此普遍,所以 Java SDK 并发包自然也少不了它。但是很多人在初次接触并发包里线程池相关的工具类时,多少会都有点蒙,不知道该从哪里入手,我觉得根本原因在于线程池和一般意义上的池化资源是不同的。一般意义上的池化资源,都是下面这样,当你需要资源的时候就调用 acquire() 方法来申请资源,用完之后就调用 release() 释放资源。若你带着这个固有模型来看并发包里线程池相关的工具类时,会很遗憾地发现它们完全匹配不上,Java 提供的线程池里面压根就没有申请线程和释放线程的方法。
线程池是一种生产者 - 消费者模式
为什么线程池没有采用一般意义上池化资源的设计方法呢?如果线程池采用一般意义上池化资源的设计方法,应该是下面示例代码这样。你可以来思考一下,假设我们获取到一个空闲线程 T1,然后该如何使用 T1 呢?你期望的可能是这样:通过调用 T1 的 execute() 方法,传入一个 Runnable 对象来执行具体业务逻辑,就像通过构造函数 Thread(Runnable target) 创建线程一样。可惜的是,你翻遍 Thread 对象的所有方法,都不存在类似 execute(Runnable target) 这样的公共方法。
公开
同步至部落
取消
完成
0/2000
荧光笔
直线
曲线
笔记
复制
AI
- 深入了解
- 翻译
- 解释
- 总结
线程池在Java并发编程中扮演着重要角色,有效地管理线程对于提高性能至关重要。本文通过生动的比喻和具体的示例,深入浅出地介绍了线程池的设计原理和使用注意事项。文章首先强调了线程池的重要性,指出频繁创建和销毁线程会带来高昂的成本,因此引入了线程池的概念。通过生产者-消费者模式解释了线程池的工作原理,以及如何使用Java中的线程池。此外,文章详细介绍了ThreadPoolExecutor的构造函数参数含义,以及使用线程池时需要注意的问题,如避免使用无界队列、谨慎选择拒绝策略、以及异常处理等。同时,文章还提到了不建议使用Executors的原因,并建议使用有界队列。总的来说,本文对于想要深入了解线程池的读者来说,是一篇非常有价值的文章。
仅可试看部分内容,如需阅读全部内容,请付费购买文章所属专栏
《Java 并发编程实战》,新⼈⾸单¥59
《Java 并发编程实战》,新⼈⾸单¥59
立即购买
© 版权归极客邦科技所有,未经许可不得传播售卖。 页面已增加防盗追踪,如有侵权极客邦将依法追究其法律责任。
登录 后留言
全部留言(78)
- 最新
- 精选
- 南琛一梦回答下Lrwin和张天屹同学的问题:当线程池中无可用线程,且阻塞队列已满,那么此时就会触发拒绝策略。对于采用何种策略,具体要看执行的任务重要程度。如果是一些不重要任务,可以选择直接丢弃。但是如果为重要任务,可以采用降级处理,例如将任务信息插入数据库或者消息队列,启用一个专门用作补偿的线程池去进行补偿。所谓降级就是在服务无法正常提供功能的情况下,采取的补救措施。具体采用何种降级手段,这也是要看具体场景。技术的世界里没有一尘不变的方案。另外,看到很多同学都提到让老师多讲讲源码,其实我觉得真没必要,老师目前的思路起到提纲契领的作用,让我们有大的思路,有全局观,具体细节我觉得大家私下去研究更合适。小弟不才,可以加微信(SevenBlue)一起讨论。
作者回复: 👍我觉得那些源码用的时候看一下就可以了,现在都是用开源项目,天天都得看源码,看源码能局部最优而已
2019-04-226131 - 随风🐿老师,有个问题一直不是很明确,①一个项目中如果多个业务需要用到线程池,是定义一个公共的线程池比较好,还是按照业务定义各自不同的线程池?②如果定义一个公共的线程池那里面的线程数的理论值应该是按照老师前面章节讲的去计算吗?还是按照如果有多少个业务就分别去计算他们各自创建线程池线程数的加和?③如果不同的业务各自定义不同的线程池,那线程数的理论值也是按照前面的去计算吗?
作者回复: 建议不同类别的业务用不同的线程池,至于线程池的数量,各自计算各自的,然后去做压测。虽然你的系统有多个线程池,但是并不是所有的线程池里的线程都是忙碌的,你只需要针对有性能瓶颈的业务优化就可以了。
2019-04-29894 - 木卫六guava的ThreadFactoryBuilder.setNameFormat可以指定一个前缀,使用%d表示序号; 或者自己实现ThreadFactory并制定给线程池,在实现的ThreadFactory中设定计数和调用Thread.setName
作者回复: 👍
2019-04-18244 - 张天屹老师你好,使用有界队列虽然避免了OOM 但是如果请求量太大,我又不想丢弃和异常的情况下一般怎么实践呢。我对降级这一块没经验,我能直观想到的就是存放在缓存,如果缓存内存也不够了就只能持久化了
作者回复: 可以放数据库,放mq,redis,本地文件都可以,具体要看实际需求
2019-04-18533 - 曾轼麟public class ReNameThreadFactory implements ThreadFactory { /** * 线程池编号(static修饰)(容器里面所有线程池的数量) */ private static final AtomicInteger POOLNUMBER = new AtomicInteger(1); /** * 线程编号(当前线程池线程的数量) */ private final AtomicInteger threadNumber = new AtomicInteger(1); /** * 线程组 */ private final ThreadGroup group; /** * 业务名称前缀 */ private final String namePrefix; /** * 重写线程名称(获取线程池编号,线程编号,线程组) * * @param prefix 你需要指定的业务名称 */ public ReNameThreadFactory(@NonNull String prefix) { SecurityManager s = System.getSecurityManager(); group = (s != null) ? s.getThreadGroup() : Thread.currentThread().getThreadGroup(); //组装线程前缀 namePrefix = prefix + "-poolNumber:" + POOLNUMBER.getAndIncrement() + "-threadNumber:"; } @Override public Thread newThread(Runnable r) { Thread t = new Thread(group, r, //方便dump的时候排查(重写线程名称) namePrefix + threadNumber.getAndIncrement(), 0); if (t.isDaemon()) { t.setDaemon(false); } if (t.getPriority() != Thread.NORM_PRIORITY) { t.setPriority(Thread.NORM_PRIORITY); } return t; } }
作者回复: 👍
2019-04-21227 - Red Cape请问老师,有界队列的长度怎么确定呢
作者回复: 看场景,拍脑门
2019-04-22625 - 海鸿1.利用guava的ThreadFactoryBuilder 2.自己实现ThreadFactory
作者回复: 👍
2019-04-1821 - Uncle Drew老师请教一下,如果线上系统宕机了,线程池中的阻塞队列怎么处理才能保证任务不丢失
作者回复: 可以把所有任务先存入数据库,处理完一条就在数据库里删除一条。单纯依赖单机的内存是无法解决的。
2019-12-06420 - yang老师,有一个问题想问一下: 如果corePoolSize为10,maxinumPoolSize为20,而此时线程池中有15个线程在运行,过了一段时间后,其中有3个线程处于等待状态的时间超过keepAliveTime指定的时间,则结束这3个线程,此时线程池中则还有12个线程正在运行;若有六个线程处于等待状态的时间超过keepAliveTime指定的时间,则只会结束5个线程,此时线程池中则还有10个线程,即核心线程数。 是这样吗?
作者回复: 是的
2019-04-2215 - 君哥聊技术我们项目中用了guava的new ThreadFactoryBuilder().setNameFormat() 老师,请教个问题,在工程中,线程池的定义一般是在全局还是局部呢?如果全局的话,是不用shutdown吗?不关闭线程池有没有问题呢?
作者回复: 一般都全局,如果需要优雅退出就需要shutdown。不关闭,会有coresize个线程一直回收不了。
2019-04-1814
收起评论