在并发修仙界很多新手以为创建线程像呼吸一样简单错随意 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 里捞之前存下来的任务丢回线程池继续处理。第二种思路是自定义阻塞队列。我们可以重写队列的take或poll方法。当工作线程来队列里取任务时如果发现队列里的任务量低于某个阈值我们就主动去外部存储里把之前持久化的任务拉取出来补充到队列里。
Java 线程池修仙传:从“无限收徒”到“灵力枯竭”的血泪教训
在并发修仙界很多新手以为创建线程像呼吸一样简单错随意 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 里捞之前存下来的任务丢回线程池继续处理。第二种思路是自定义阻塞队列。我们可以重写队列的take或poll方法。当工作线程来队列里取任务时如果发现队列里的任务量低于某个阈值我们就主动去外部存储里把之前持久化的任务拉取出来补充到队列里。