线程池简单源码思路手撕实现
注意这里的代码只是线程池的简单逻辑实现,没考虑多线程加锁什么的
java
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;
public class myThreadPool {
public BlockingQueue<Runnable> blockingQueue;
private int corePoolSize;
private int maxPoolSize;
private int timeout;
private TimeUnit timeUnit;
private RejectHandle rejectHandle;
private List<Thread> coreList = new ArrayList<>();
private List<Thread> supportList = new ArrayList<>();
public myThreadPool(int corePoolSize, int maxPoolSize, int timeout, TimeUnit timeUnit, BlockingQueue<Runnable> blockingQueue, RejectHandle rejectHandle) {
this.corePoolSize = corePoolSize;
this.maxPoolSize = maxPoolSize;
this.timeout = timeout;
this.timeUnit = timeUnit;
this.blockingQueue = blockingQueue;
this.rejectHandle = rejectHandle;
}
void execute(Runnable command) {
if (coreList.size() < corePoolSize) {
CoreThread thread = new CoreThread(command);
coreList.add(thread);
thread.start();
return;
}
if (blockingQueue.offer(command)) {
return;
}
if (coreList.size() + supportList.size() < maxPoolSize) {
SupportThread thread = new SupportThread(command);
supportList.add(thread);
thread.start();
return;
}
if (!blockingQueue.offer(command)) {
rejectHandle.reject(command, this);
}
}
class CoreThread extends Thread {
private final Runnable firstTask;
CoreThread(Runnable firstTask) {
this.firstTask = firstTask;
}
@Override
public void run() {
firstTask.run();
while (true) {
try {
Runnable command = blockingQueue.take();
command.run();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
class SupportThread extends Thread {
private final Runnable firstTask;
SupportThread(Runnable firstTask) {
this.firstTask = firstTask;
}
@Override
public void run() {
firstTask.run();
while (true) {
try {
Runnable command = blockingQueue.poll(timeout, timeUnit);
if (command == null) {
break;
}
command.run();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println(Thread.currentThread().getName()+"线程死掉了");
supportList.remove(Thread.currentThread());
}
}
}
首先是重要的线程池参数:
核心线程数:定义不会被回收的核心线程数量
最大线程数:定义除了核心线程以外的临时线程,临时线程超过时间还没任务就会被销毁
阻塞队列:选择用什么阻塞队列,使用阻塞队列是防止在核心/辅助线程再去拿任务的时候一直while对cpu空转的损害
拒绝策略:在超过最大线程就会采取对应的拒绝策略-比如报错舍弃/静默丢弃/舍弃旧任务(或者也可以考虑持久化到mysql保存起来)
时间:这个是辅助线程没任务的存活时间。
时间单位:这个是辅助线程没任务的存活时间的单位。
线程工厂:用来决定线程的命名,创建线程的方式,管理线程优先级之类的。这里只是简单demo所以没有实现。
java
void execute(Runnable command) {
if (coreList.size() < corePoolSize) {
CoreThread thread = new CoreThread(command);
coreList.add(thread);
thread.start();
return;
}
if (blockingQueue.offer(command)) {
return;
}
if (coreList.size() + supportList.size() < maxPoolSize) {
SupportThread thread = new SupportThread(command);
supportList.add(thread);
thread.start();
return;
}
if (!blockingQueue.offer(command)) {
rejectHandle.reject(command, this);
}
}
这个是重要的线程池接受任务的过程:
核心线程没满就用核心线程,满了就先放到阻塞队列,阻塞队列满了就创建临时辅助线程,辅助线程也满了就考虑拒绝策略。
java
class CoreThread extends Thread {
private final Runnable firstTask;
CoreThread(Runnable firstTask) {
this.firstTask = firstTask;
}
@Override
public void run() {
firstTask.run();
while (true) {
try {
Runnable command = blockingQueue.take();
command.run();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
class SupportThread extends Thread {
private final Runnable firstTask;
SupportThread(Runnable firstTask) {
this.firstTask = firstTask;
}
@Override
public void run() {
firstTask.run();
while (true) {
try {
Runnable command = blockingQueue.poll(timeout, timeUnit);
if (command == null) {
break;
}
command.run();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println(Thread.currentThread().getName()+"线程死掉了");
supportList.remove(Thread.currentThread());
}
}
}
这里有firsttask的任务主要是在线程刚创建就来执行,减少再去阻塞队列拿的麻烦,核心线程和辅助线程从阻塞队列拿任务的方法分别是take()和poll()主要是后者限制时间,辅助线程执行完就会被销毁这里是发生在run()执行完。而take()永久等待出不来就不会被销毁。
然后看一下拒绝策略
java
package tech.insight;
/**
* @author gongxuanzhangmelt@gmail.com
**/
public interface RejectHandle {
void reject(Runnable rejectCommand, MyThreadPool threadPool);
}
java
package tech.insight;
/**
* @author gongxuanzhangmelt@gmail.com
**/
public class ThrowRejectHandle implements RejectHandle {
@Override
public void reject(Runnable rejectCommand, MyThreadPool threadPool) {
throw new RuntimeException("阻塞队列满了!");
}
}
java
package tech.insight;
/**
* @author gongxuanzhangmelt@gmail.com
**/
public class DiscardRejectHandle implements RejectHandle {
@Override
public void reject(Runnable rejectCommand, MyThreadPool threadPool) {
threadPool.blockingQueue.poll();
threadPool.execute(rejectCommand);
}
}
只实现了报错丢弃和丢弃旧任务加入新任务。
java
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.TimeUnit;
public class Main {
public static void main(String[] args) {
myThreadPool myThreadPool = new myThreadPool(2, 10, 1, TimeUnit.SECONDS, new ArrayBlockingQueue<>(2), new ThrowRejectHandle());
for (int i = 0; i < 10; i++) {
final int fi=i;
myThreadPool.execute(()->{
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName()+" "+fi);
});
}
}
}
main里初始化线程池,以及添加10个任务,以上就是线程池的简单实现。
接下来我记录一些自己看的扩展
关于参数该怎么设置?比如核心线程数和最大线程数和其他的?
线程数
首先看八股怎么说:
cpu密集型:核心线程数=cpu核心数加减1,最大线程数=cpu核心数加减1
io密集型:核心线程数=cpu核心数*2,最大线程数=CPU核心数 × 4 或更高
然后还有具体的场景:
比如在电商场景:流量波动极大。平时可能没什么人,一旦活动开启,流量瞬间暴涨。可以极端的把核心线程数设置为0(还是留一点但是占比小),全是临时线程没有任务就回收。
还有记录日志场景:这种一般没什么变化,就全部设置为核心线程数就好,最大线程数等于核心线程数。
阻塞队列
阻塞队列选有界的如 ArrayBlockingQueue 或 LinkedBlockingQueue(n),因为无界队列存在内存溢出风险,还有**SynchronousQueue:** 之前提到的电商核心线程为 0 时常用。它不存任务,直接"手递手"交给线程。
关于存活时间
电商/高并发场景: 建议设置得稍长一些(如 60s)。因为流量往往是波动性的,频繁地销毁和重新创建线程是非常消耗 CPU 的。
后台低频任务: 建议设置得短一些(如 1s 或 10s)。任务处理完赶紧释放资源。
默认参考: Java 自带的 CachedThreadPool 默认是 60s,这是一个非常合理的平衡点点。
可以理解为核心线程少就长一点,多就短一点。
拒绝策略
当线程池满了(最大线程已开,队列也塞满了),新来的任务怎么办?Java 提供了四种标准策略:
- AbortPolicy(默认): 直接抛出
RejectedExecutionException。- 适用: 关键业务,必须让调用方知道任务失败了。
- CallerRunsPolicy(调用者运行): 谁提交的任务,谁自己去执行。
- 适用: 最重要的兜底方案。它会减慢生产者的速度(因为生产者去干活了,没空发任务了),起到一种天然的"降级"和"限流"作用。
- DiscardPolicy: 直接丢掉任务,不报任何错。
- 适用: 比如打印无关紧要的日志、监控埋点,丢了就丢了。
- DiscardOldestPolicy: 丢弃队列里排队最久(最老)的任务,把当前任务塞进去。
- 适用: 具有"时效性"的任务,比如实时行情数据,旧的数据没意义了。
根绝业务性质来选择拒绝策略。
还有美团的动态线程池是怎么搞的?
先讲讲为什么有这个需求?
所谓的"美团动态线程池",核心本质就是:配置中心(如 Nacos/Apollo) + JDK 线程池原生 API + 监控告警。
它不是创造了一种新的线程池技术,而是一套管理和运维线程池的架构方案。这个概念最早源于美团技术团队发表的一篇经典文章《Java线程池实现原理及其在美团业务中的实践》,后来成为了业界标准。
传统痛点: 线程池参数(核心数、最大数、队列长度)通常写死在代码或配置文件里。一旦上线,发现流量太大导致队列积压,或者参数设置不合理导致 CPU 飙升,必须改代码 -> 重新打包 -> 重启服务。这在"双11"这种分秒必争的时刻是致命的。
动态线程池能做到什么:
热更新: 像开关灯一样,运营/开发在后台修改参数,服务端的线程池瞬间生效,无需重启。
全景监控: 实时看到线程池在干嘛(队列堆了多少、当前活跃线程多少)。
具体是怎么实现的?
它的实现并不复杂,主要分为三步:管理、监听、修改。
第一步:管理(注册中心)
也就是你要把系统里所有的线程池都管理起来。通常会做一个 ThreadPoolManager,用一个 Map 把所有线程池存起来,每个线程池有个名字(比如 order-pool, log-pool)。
第二步:监听(配置中心)
这是关键。利用 Nacos、Apollo、Etcd 等配置中心的能力。
- 你在 Nacos 上修改配置:
order-pool.coreSize = 20。 - 应用程序里的监听器(Listener)捕捉到配置变化。(只要改了主动推送)
第三步:修改(JDK API - 很多人不知道的秘密)
这是最核心的。很多人以为线程池创建了就不能改,其实 JDK 的 ThreadPoolExecutor 原生就提供了 public 的 set 方法。
这里还有一个点,默认的阻塞队列是final的想要修改长度只能自己实现阻塞队列并去掉final。
还要配合定时任务去监控读取运行时指标保证安全,也要监控队列使用率比如超过80%报警。
省流:
"其实参数设置没有绝对的标准,核心准则是:即不要让 CPU 闲置(吞吐量太低),也不要让队列积压导致 OOM(内存溢出)。
所以,动态线程池 才是最终的解决方案。因为线上流量是未知的,我们无法在写代码时就预测出最完美的参数,只有具备了动态调整 和可视化监控的能力,才能让系统在面对突发流量时立于不败之地。"