在并发修仙界,很多新手以为创建线程像呼吸一样简单?错!随意 new Thread 就像无限制招收外门弟子,早晚导致宗门(服务器)资源耗尽而崩塌。真正的强者懂得利用线程池这座“护宗大阵”——核心线程是内门长老,队列是待办任务堂。但小心!若不懂拒绝策略这道“天劫防线”,一旦任务积压如山,你的系统就会当场走火入魔,抛出 RejectedExecutionException!本文带你参透 ThreadPoolExecutor 的七层境界

为什么要用线程池?别直接 new Thread 了!

刚开始学习多线程的时候,可能有许多人有疑问:

既然要并发执行任务,那我直接 new Thread() 不就行了吗?

看起来没什么问题。但是如果真的这么干,很快你就会发现:

  • CPU飙升
  • 内存暴涨
  • 系统响应变慢
  • 严重时甚至直接 OOM

那么问题来了:为什么线程不能随便创建?

        底层原因其实很扎心:Java 的线程是 1:1 对应操作系统内核线程的。你每创建一个线程,都需要通过系统调用切换到内核态,还要分配堆和栈空间;销毁时又要切回内核态去回收。这一套动作下来,CPU 资源消耗极大。

        所以我们自然而然想到,有没有一种技术,可以提前创建好一些线程放在容器里面,等到使用的时候取出来,用完放回容器不销毁,每次复用线程,不频繁的创建和销毁呢?这个就是我们常说的线程池。他是一种管理和复用线程的机制。


线程池到底是个啥?

通俗点说,线程池就像一个“人才市场”。它提前养了一批熟练工(线程),有活儿(异步任务)来了,直接派空闲的人去干,干完不辞退,回池子里继续待命。它的核心作用就两点:控制并发数 和 实现线程复用

JDK 给我们提供了一些快捷工具类(Executors),但我得提醒大家,千万别在生产环境乱用,里面全是坑:

  • ScheduledThreadPool(定时线程池):适合定期执行任务。但坑在于,大量任务提交时,它会疯狂创建成百上千的线程,分分钟把系统资源耗尽导致 OOM。
  • CachedThreadPool(可缓存线程池):没有核心线程,全是临时工。任务一来就建线程,高并发下同样容易 OOM。
  • FixedThreadPool(固定线程池):全是核心线程,异常了会自动重建。但它的任务队列是无界的!如果处理速度赶不上提交速度,任务全堆在内存里,照样 OOM。
  • SingleThreadExecutor(单线程池):永远只有一个线程,保证任务按顺序执行。缺点同上,队列无界,有 OOM 风险。

线程池有哪些参数,具体是如何是如何工作的?

很多文章在写线程池的时候,可能就是直接说参数是什么,执行流程什么。但是下面我们换一个角度,以问题驱动,来带大家刨析线程池的七大核心参数和工作流程。

        一般情况下,线程池里的线程数也就几十个。但如果突然来了特别多的任务,现有的线程处理不过来了怎么办?这时候,我们通常会引入一个阻塞队列,把那些还没轮到执行的任务先存起来。

        不过阻塞队列的容量毕竟是有限的,我们不可能无限制地往里塞任务。当队列满了,就说明当前的任务量确实太大,现有的线程不够用了。这时候,线程池就会去创建一些‘临时线程’(也就是非核心线程)来帮忙处理这些任务。当然,等这波高峰过去,临时线程空闲下来,达到了我们设定的空闲存活时间,它们就会被自动销毁掉。

        那如果情况更极端呢?比如阻塞队列已经满了,而且这些线程也全都在忙着干活,这时候如果又新来了任务,该怎么办?这时候,就会触发我们提前定义好的‘拒绝策略’,把这些实在处理不了的新任务给拒绝掉。

        另外,对于线程的创建,可以配置一个线程工厂。我们可以根据具体的业务场景给线程和线程池命名, 这样的话业务出问题, 就能定位到对应的线程池和线程.

所以综上线程池的参数和执行流程就比较明确了。

七大参数:核心线程数, 最大线程数, 临时线程的空闲时间, 时间单位, 阻塞队列, 拒绝策略, 线程工厂。

执行流程:

        提交一个任务时,如果当前线程数未达到核心线程数,就直接创建新的核心线程来执行;如果核心线程已满,就把任务加入等待队列;如果队列也满了,再尝试创建非核心线程;如果连最大线程数也达到了,就按照配置好的拒绝策略处理这个新任务。


在最后抛出一个问题:

线程池如何避免任务丢失?

问题:

  • 有人建议使用无界队列来避免拒绝任务。但这其实是个坑,因为服务器的内存是有限的,如果任务一直堆积,最后肯定会触发 OOM(内存溢出),导致系统崩溃。

  • 当然使用CallerRunsPolicy拒绝策略确实可以避免丢弃任务, 但是让业务线程执行任务,会导致线程被拖慢,无法及时处理前端发的请求. 所以这个拒绝策略只能适用于低并发还不想丢弃任务的场景

解决方案:

如果我们在高并发下,既想要系统跑得快,又坚决不能丢任务,那其实可以借鉴操作系统里“虚拟内存”和“请求分页”的思想。内存不够用的时候,操作系统会把数据持久化到磁盘里,等需要了再加载进来。

对应到线程池,我们可以做任务的持久化。具体落地主要有两种思路:

第一种是自定义拒绝策略。

  • 当线程池满了触发拒绝时,我们不丢弃,而是把这个任务存到 MySQL、Redis 或者 MQ 里。

  • 同时,重写线程池的 afterExecute 方法。每次执行完一个任务,我们可以去检查一下阻塞队列,如果队列比较空,就去数据库或 MQ 里捞之前存下来的任务,丢回线程池继续处理。

第二种思路是自定义阻塞队列。

  • 我们可以重写队列的 takepoll 方法。当工作线程来队列里取任务时,如果发现队列里的任务量低于某个阈值,我们就主动去外部存储里把之前持久化的任务拉取出来,补充到队列里。

更多推荐