likes
comments
collection
share

🔥线程池都没搞懂,还敢说自己是程序员?

作者站长头像
站长
· 阅读数 24
  • 🍬 微信公众号: 公子小白coding
  • 📚 文章收录专栏: 《java基础系列》
  • 👨‍👦‍👦 技术交流群: 🐧:656101079 :公众号内即可获取
  • 🎨 所有文章第一时间都会在公众号发布,感兴趣的小伙伴快来接入我们吧~~
  • ⏰ 本篇文章共计 6729字 ,预计阅读用时18 分钟

🎯故事的开始

那是一个令人煎熬的下午,30多度的天气,我拿着我那皱巴巴的用 A4纸打印的简历,黑白水墨的简历,好像提前像我预示着不好的事情。

管他呢!先上去面试,年轻人么,不试一试怎么知道自己不行,这点底气咱还是有的。大热天的,来这一趟我容易吗?坐在电梯里,正在想着。

滴!电梯门开了,映入眼帘的两扇双开玻璃门,让我压制不住内心的紧张。“哎,你是来面试的吧!”,谁在叫我,定睛一看,那大长腿瞬间将我吸引,心里暗念,这是哪家的大水冲了龙王庙了!!“叫你呢!没听见吗?”。就这一句话,将我从万千思绪拉回,心里想着,我就是来面试的,今天可要好好表现,成败就在次一举了。嘿嘿😆。

我进入了一间房间,里面坐着个中年男人,目光上下打量一番,那头顶为数不多的毛发,感觉每一根都在宣誓着自己的领地。果然一看就是一个老程序员了。“坐下,我们开始吧!”。我端坐在这位大佬程序员对面。我两面面相觑,刚坐下来,感觉如坐针毡。

在进行了一番友好的问候以及寒暄之后(一波简单的自我介绍),他开口说:

 

平常开发中用过线程池吗?

我心里一想,这不正好问住我的强项了吗,心里暗暗窃喜。回到,“那必须的,现在还能有不会用线程池的程序员吗?你可不能小瞧我啊!”,说罢,忽然一想,完了后悔说最后一句话了,为自己刚才的装逼感到抱歉。


🎯面试的煎熬

心情:😀

下面内容大家只能看一遍,因为禁止鞭尸(说好的,不能反悔昂😭)

面试官:用过就好,那我问你一下,线程池有几大参数,分别都是什么意思,你介绍一下? 悲催的我:(⊙o⊙)…额,核心线程数,最大线程数,还有……(内心os):大哥,我能看一下手机吗,我有点忘了

 

心情:😐 面试官:看来这基础不行啊,那你说一下线程池提交任务后的工作流程吧,这个应该难不倒你吧 悲催的我:很简单啊!提交任务后,线程池会分配一个线程,去执行,执行完将线程再归还到线程池。(内心os):这种问题还能难倒我,哼😆! 面试官:没有了吗?我记得执行流程不止这些啊! 悲催的我:不会啊,我开发中使用的时候,看到的就是这个情况啊,我把任务提交给线程池,然后就能自己执行啊!

 

面试官的表情从惊恐到诧异,再到歪嘴一笑,整个细微过程被我尽收眼底,一览无余,我心里想,这面试官心理戏挺多啊,是我回答的太专业了吗!嗯嗯,一定是这样的。

心情:🤢 面试官: 这两个问题不谈了,你给我说一下,你在实际项目中的那些地方会使用线程池? 内心os: 呀!哎呀,这不正好试探到我的强项了嘛! 悲催的我:比如我简历上的那个项目,我通常在比如发送短信时候我们会用线程池去发送,提升整体的发送数量。 面试官: 没有的话,咋们今天就到这里吧,

 

心情: 😭 面试官起身拍了拍我的肩膀,我脑子里一片空白,想着,老大哥看我学艺不精,让我半夜三更去他那里补习吗?

“出来吧!”,小姐姐一声轻语,我跟着出到了外面,“今天咋们就到这里吧,回去等通知吧”,此话一出,我顿时心里凉了半截。手里的简历也在不经意间被攥出了几个窟窿。

 

随着电梯的缓缓下降,3 2 1 滴!电梯门开,不自觉的咽了口唾沫,嗓子感觉干燥难耐。炎热的气浪扑面而来,一句“回去等通知吧”在脑海里潆绕,内心失落到了极点,心里按按下定决心,给我等着,今天你让我回去等通知,明天我让你排队找我

🔥线程池都没搞懂,还敢说自己是程序员?

你成功激发我的斗志了,你知道吗?很可怕的。

三十年河东,三十年河西,莫欺少年穷

三十年后,摸了摸我头顶的头发,保住了还好,望着自己学习记录的成果,露出了满意的微笑。 下面请容我介绍,这份秘籍,不管你是 18 还是 80 ,保证你看完之后那是腰好腿好身体好,让我们看看这篇秘籍的精华。

 

🎯秘籍到手?

首先映入眼帘的是,线程池的顶头介绍

 

秘籍 · 目录

本篇文章主要包括如下几个方面

🔥目录🔥

  1. 👉秘籍 · 线程池的优势

  2. 👉秘籍 · 线程池的使用和实战

  3. 👉秘籍 · 线程池的工作原理

  4. 👉秘籍 · 线程池的其他参数

    • 任务队列

    • 线程工厂及自定义工厂

    • 拒绝策略及自定义策略

  5. 👉秘籍 · 聊一聊实战经验

 

看起来概括还挺清晰,话不多说,让我们进入正题吧。


 

秘籍 · 线程池的优势

熟悉 Java 多线程编程的同学都知道,当我们线程创建过多时,容易引发内存溢出,因此我们就有必要使用线程池的技术了。

 

总的来说,线程池优势主要有如下优势:

  • 降低资源消耗。 通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
  • 提高响应速度。 当任务到达时,任务可以不需要等到线程创建就能立即执行。
  • 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

 

🔥奥义 · 补充小贴士🔥

这章秘籍还行,一上来就给你罗列一堆的知识点,生怕我看懂是吧!😡

用人话怎么解释: 线程池没有之前,我们都是手动创建线程,在没有系统管理的情况下,假如一个项目被100个人开发都新建了一个线程,你是第101个,你接手这个项目,你傻眼不。 (内心os: 我可去他x的,谁给xx留的坑啊)

  稍微总结一下,罗列一下单独线程的坏处:

  • 不好管理 自己一个人还好,人多了谁知道啊,玩的就是击鼓传花,你懵不懵!你看这乱七八糟的线程。

> 🔥线程池都没搞懂,还敢说自己是程序员?

  • 资源利用率太低 你想,有的线程执行就是算个 1 + 1,整套流程创建,执行,销毁,它是一步不落啊!这明显付出和回报不成正比啊!

> 🔥线程池都没搞懂,还敢说自己是程序员?

  • 响应速度慢: 上面两个缺点我都忍了,可是你每次执行任务都要启动创建,太慢了,老板就指望着这片代码吃饭呢?你这是啥,你这属于是断老板财路了!你这能讨好吗?

在这里,我巧妙的利用反证法,证明了秘籍,真是个触类旁通,举一反三的小天才!!

知道了优势,我得知道怎么用吧,有了屠龙宝刀,没有出鞘的机会那也白搭啊,目光缓缓的移到秘籍的第二个标题。

 


秘籍 ·线程池的使用和实战

想要用线程池,肯定要知道怎么创建线程池吧。嗯,仔细思考有点道理。

 

线程池的使用和实战

下面介绍创建线程池方法,线程池共提供了四种构造函数供我们调用,本质最后调用的都是一个方法,所以这里只介绍最核心、参数最全的构造方法。

🔥线程池都没搞懂,还敢说自己是程序员?

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

✅ => (必须参数) ☑️ => (非必须参数)

  • corePoolSize => 核心线程数。 默认情况下,核心线程会一直存活,但是当将allowCoreThreadTimeout 设置为 true 时,核心线程也会超时回收。

  • maximumPoolSize => 线程池所能容纳的最大线程数。 当活跃线程数达到该数值后,后续的新任务将会阻塞。

  • keepAliveTime => 线程闲置超时时长。 如果超过该时长,非核心线程就会被回收。如果将 allowCoreThreadTimeout 设置为 true 时,核心线程也会超时回收。

  • unit => 指定 keepAliveTime 参数的时间单位。 常用的有:TimeUnit.MILLISECONDS(毫秒)、TimeUnit.SECONDS(秒)、TimeUnit.MINUTES(分)。

  • workQueue => 任务队列。 通过线程池的 execute() 方法提交的 Runnable 对象将存储在该参数中。其采用阻塞队列实现。

  • ☑️ threadFactory => 线程工厂。 用于指定为线程池创建新线程的方式。

  • ☑️ handler => 拒绝策略 当达到最大线程数时需要执行的饱和策略。


线程池在代码怎么使用?

🔥线程池都没搞懂,还敢说自己是程序员?

没错,你会想,这么高大上的线程池,使用就如此的简单吗?还真就是,越是高端的东西,使用起来就越简单,这还真是个真理。

 


🔥奥义 · 实战场景🔥

这么点练习,怎么能让我灵活掌握线程池的使用场景啊!

我苦思冥想,在不些努力之下,想到了如下几点场景:

  • 文件批量下载上传

🔥线程池都没搞懂,还敢说自己是程序员?

🔥线程池都没搞懂,还敢说自己是程序员?

在之前单线程情况下,我们下载三个小视频少说需要三秒钟时间,用了线程池之后,

下载三个小视频只花了一秒,有了线程池之后,感觉没有百度云限速,我的电脑都装不下我的种子了。

  • 商品信息的多步查询

🔥线程池都没搞懂,还敢说自己是程序员? 🔥线程池都没搞懂,还敢说自己是程序员?

单线程情况下,我们查询商品信息需要分三步,并且耗时是逐步累加的,最终我们需要用时 4s 中查询商品的全部信息。

但是当我们用到了线程池技术之后,我们的耗时只会是三步中耗时最长的哪一个,最终我们用了 2s 中就能查询出所有的商品信息。

 

知道了这些线程池的小秘密,我们还需要了解一下线程池的运行流程,之前面试的经历还是历历在目啊!

让我们马上翻开秘籍的第三章,你以为激发我的斗志这么容易收场的吗?

🔥线程池都没搞懂,还敢说自己是程序员?


秘籍 ·线程池的工作原理

废话不多说,先来张图,大体对运行流程有个大概印象

🔥线程池都没搞懂,还敢说自己是程序员?

 

线程池的工作原理解读

在创建了线程池之后,等待提交过来的任务请求

  • 当调用execute() 方法添加一个请求任务的时候,线程池会做出如下判断:
    • 如果正在运行的线程数量 < corePoolSize,那么马上创建线程运行这个程序
    • 如果正在运行的线程数量 >= corePoolSize,那么将这个任务放入队列
    • 如果这个时候队列满了 && (线程数量 < maximumPoolSize) ,那么还是要创建非核心线程立刻执行这个任务
    • 如果队列满了 && (线程数量 >= maximumPoolSize) ,那么线程池会启动拒绝策略来执行。
  • 当一个线程完成任务的时候,它会从队列中取下一个任务来执行
  • 当一个线程无事可做超过一定时间(keepAliveTime)时:线程会判断如果当前运行的线程数 > corePoolSize,那么这个线程就会被停掉

\

🔥奥义 · 完全理解🔥

还不理解?没有关系,接下来,我用六张图来讲解线程池内部运行原理

  • 添加任务

  • 线程数量 < corePoolSize

🔥线程池都没搞懂,还敢说自己是程序员?

  • 线程数量 >= corePoolSize

🔥线程池都没搞懂,还敢说自己是程序员?

  • 队列满了 && (线程数量 < maximumPoolSize)

🔥线程池都没搞懂,还敢说自己是程序员?

  • 线程数量 >= maximumPoolSize

 

上述三个条件都不符合,执行拒绝策略

🔥线程池都没搞懂,还敢说自己是程序员?

\

  • 执行任务

.当线程池中的线程执行完任务空闲时,会尝试从workQueue中取头结点任务执行。

🔥线程池都没搞懂,还敢说自己是程序员?

\

  • 线程销毁

空闲线程的空闲时间超过keepAliveTime的线程会被销毁,保持线程池中线程数为corePoolSize

🔥线程池都没搞懂,还敢说自己是程序员?

到这里我相信大家,已经了解了至少是线程池添加任务的具体流程了,虽说不是很详尽,但对付一下面试官应该问题不大的。

 

由于本秘籍篇幅有限,想了解详细的底层原理的小伙伴,请移步《》

哦哦,原来上面线程池添加任务的流程这么简单啊,哎,就怪当时不知道线程池的那些核心参数,要是早早修炼这份秘籍就好了,不多说了,说多了都是泪啊。看到这里的小伙伴们,以后被问到线程池,就不会像我一样芭比Q了!

上面我们知道的七大线程池参数,我们接触到了其中四个,分别是corePoolSize,maxMumPoolSize,keepAliveTime,unit。还有三个我们好像只知其意,不知其里啊。这不是我们的风格啊。

秘籍 ·线程池的其他参数

任务队列(workQueue)(必须参数)

任务队列,在 JAVA 中是通过 阻塞队列实现的,在 JAVA 中需要实现BlockingQueue 接口,我们也可以自定义阻塞队列。下面我们介绍几种,在创建线程池中常用的阻塞队列。

  • ArrayBlockingQueue(⭐⭐⭐)一个由数组结构组成的有界阻塞队列
  • LinkedBlockingQueue(⭐⭐) 一个由链表结构组成的有界阻塞队列,在未指明容量时,容量默认为 Integer.MAX_VALUE
  • 其他不常用阻塞队列(⭐)
    • PriorityBlockingQueue: 一个支持优先级排序的无界阻塞队列
    • **DelayQueue:**类似于PriorityBlockingQueue,是二叉堆实现的无界优先级阻塞队列
    • SynchronousQueue: 一个不存储元素的阻塞队列,消费者线程调用 take() 方法的时候就会发生阻塞
    • LinkedBlockingDeque: 使用双向队列实现的有界双端阻塞队列\

这篇秘籍看的出来,仅仅只是简单介绍了一下,阻塞队列的简单介绍,想要和我一样,想要了解 阻塞队列的详细原理的,请少侠移步我的另一篇秘籍详解《阻塞队列xxxx》

看完阻塞队列之后,下面一行赫然写着一下线程池的另一个不为人知的 ThreadFactory 线程工厂。 名字很好理解,就是创建线程的工厂,线程池中的线程都是由指定的线程工厂创建的。

 

线程工厂(threadFactory)(可选参数)

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

🔥线程池都没搞懂,还敢说自己是程序员?

仔细分析代码中的 newThread 方法,当新的任务进来线程池需要创建新的线程执行时,会进入里面的逻辑。 首先根据 线程工厂初始化的 线程组、线程名(线程名字前缀 + 原子自增int)执行任务(Runnable) 来新建一个线程。判断当前线程是不是守护线程,是的话,改为正常线程。设置线程优先级为正常优先级

 

为什么要这么做呢?

因为,当项目规模逐渐扩展,各系统中线程池也不断增多,当发生线程执行问题时,通过自定义线程工厂创建的线程设置有意义的线程名称可快速追踪异常原因高效、快速的定位问题。

 

🔥奥义 · 实际举例🔥

没有具体案例怎么能够理解呢!!在开发中我们也会遇到需要自定义线程工厂的需求。

话不多说,我们直接开始

🔥线程池都没搞懂,还敢说自己是程序员?

我们在开发中运用线程池时,常常想要,让线程池有自己对应的业务前缀,比如 [服务名][下载数据]-thread-1 线程名字,我们能很轻易的看出当前线程所属服务和业务,一旦出了问题想要排查,查日志也是相当方便的,定位起来很快。

等等,如果我没记错的话,现在我们看了有 6 个了,还剩下一个 handler 是什么意思呢?

拒绝策略(handler)(可选参数)

在上面一些案例时,我们已经知道。当添加任务时,线程池中核心线程被占满,阻塞队列也满了,也达到了最大线程量,那线程池就会去执行,对应设置的拒绝策略。

拒绝策略实现接口 RejectedExecutionHandler 的 rejectedExecution方法。

不过线程池已经默认定义了四种拒绝策略

🔥线程池都没搞懂,还敢说自己是程序员?

  • AbortPolicy(默认) :丢弃任务并抛出 RejectedExecutionException 异常。
  • CallerRunsPolicy:由调用线程处理该任务。
  • DiscardPolicy丢弃任务,但是不抛出异常。可以配合这种模式进行自定义的处理方式。
  • DiscardOldestPolicy:丢弃队列最早的未处理任务,然后重新尝试执行任务。\

看起来四个拒绝策略没想象中那么复杂,以后在开发中就能根据不同的场景定义线程池时候,不仅能用默认的四种拒绝策略,在知道了他的原理的情况下,还能自定义拒绝策略

🔥奥义 · 实际举例🔥

使用自定义拒绝策略

虽然,JDK给我们提供了一些默认的拒绝策略,但我们可以根据项目需求的需要,或者是用户体验的需要,定制拒绝策略,完成特殊需求。我们在日常开发需求中经常有线程拒绝我们需要加监控的诉求,而我们可以自定义拒绝策略,去加监控,打日志和通知等等,一系列我们想干的操作。

废话多说上图一看便知。

🔥线程池都没搞懂,还敢说自己是程序员?

毕竟,被拒绝,能干很多很酷的事情的对吧。我不也是被拒绝才到这里了吗!!

以上就是我对线程池所有参数做的总结和相关扩展,下面让我们聊一下实战经验

🔥线程池都没搞懂,还敢说自己是程序员?\

秘籍 ·聊一聊实战经验

说完了线程池各种各样的小知识,我们需要问我们自己几个问题,也就是说,掌握这些问题,你才能真正用好线程池!!!

(最重要)线程池的核心线程数参数该设置多大呢?

现在网上大部分答案都是单纯暴力的一上来就直接根据业务场景来区分出 IO 密集型 和 CPU密集型

🔥奥义 · 小贴士🔥

  • CPU密集型 也叫计算密集型,指的是系统的硬盘、内存性能相对CPU要好很多,此时,系统运作大部分的状况是CPU Loading 100%,CPU要读/写I/O(硬盘/内存),I/O在很短的时间就可以完成,而CPU还有许多运算要处理,CPU Loading 很高。 比如,我们需要做很复杂的计算,数据合成等比较耗费CPU计算资源的操作,就属于CPU密集型。

  • IO密集型 IO密集型指的是系统的CPU性能相对硬盘、内存要好很多,此时,系统运作,大部分的状况是CPU在等I/O (硬盘/内存) 的读/写操作,此时CPU Loading并不高。 比如,我们下载网络文件,我们需要不断等待网络流给我们返回数据,受限于网络带宽,并不受限于 CPU 性能。

 

然后根据定义直接了当的给出两条规范:

  • CPU密集型:corePoolSize = CPU核数 + 1

  • IO密集型:corePoolSize = CPU核数 * 2

也不是说这样子不好,但是有点过于理想化了,针对单一线程池还可以,但是因为我们现在服务体量大的情况下,不可能只有一个线程池,而是会根据业务类型分出很多线程池。这样就不能达到我们想优化的目的。

这里提出的解决方案就是,配置大小不能写死,而是变成一种动态配置,这样就可以实时根据 CPU 和 IO 情况动态调整大小,优化效果会好很多

这里推荐大家阅读由美团推出的文章:xxxxx

 

线程池的阻塞队列该如何选择呢?

强制禁止:不能选择的队列 LinkedBlockingQueue

为什么呢,他底层是一个链表,而且定义时,并不会指定大小限制(默认值 Interger 的最大值,得有21亿多)这就有可能导致,因为一些不可知晓的因素(代码·bug、死循环),会导致内存占用过高,造成OOM 服务崩溃!!

 

一般场景ArrayBlockingQueue 使我们最常用的阻塞队列,我们可以初始化大小,这个大小的限制,我们根据我们的业务场景来定义。其他场景目前,我还没见到,不过一般来说上面的阻塞队列应该能应付 90%以上的场景了!!

一定使用构造方法创建线程池

相信你发现,哎,怎么没有介绍Executors 类,看其他博主都会介绍啊,我在这里明确的告诉你最好不要用。

  举个例子,这个类中有个创建固定线程池的方法 Executors.newFixedThreadPool() 只需要传入核心线程大小就可。但是点进去底层会发现,它的阻塞队列是 LinkedBlockingQueue 是我们明确禁止使用的 阻塞队列

  所以,这个类使用起来是很方便,原因是他帮我们隐藏了很多参数细节,我们并不能做好把控,所以为了开发(保住饭碗),我们一定要使用 ThreadPoolExcetor 创建线程池,虽然很累,但是一切至少都在我们掌控之中。

使用自定义线程工厂

为什么要这么做呢?

是因为,当项目规模逐渐扩展,各系统中线程池也不断增多,当发生线程执行问题时,通过自定义线程工厂创建的线程设置有意义的线程名称可快速追踪异常原因,高效、快速的定位问题。

使用自定义拒绝策略

虽然,JDK给我们提供了一些默认的拒绝策略,但我们可以根据项目需求的需要,或者是用户体验的需要,定制拒绝策略,完成特殊需求。

线程池划分隔离

不同业务、执行效率不同的分不同线程池,避免因某些异常导致整个线程池利用率下降或直接不可用,进而影响整个系统或其它系统的正常运行。

 

🎯修炼完成

经过上面这么多的讲解、案例和对知识的思考,相信大家已经初步掌握了线程池的使用方法和细节,以及对原理运行流程的掌握,

如果你觉得本文对你有一定的启发,引起了你的思考。

点赞、转发、收藏,下次你就能很快的找到我喽!

🔥线程池都没搞懂,还敢说自己是程序员?