线程池(结合源码了解底层)

线程池

为什么需要线程池?

  • 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。

  • 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。

  • 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

线程池重要参数

corePoolSize : 任务队列未达到队列容量时,最大可以同时运行的线程数量

maximumPoolSize : 任务队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数

workQueue: 新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中

ThreadPoolExecutor其他常见参数 :

  • keepAliveTime:线程池中的线程数量大于 corePoolSize 的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了 keepAliveTime才会被回收销毁
  • unit : keepAliveTime 参数的时间单位
  • threadFactory :用来创建新线程
  • handler :拒绝策略
    • AbortPolicy:抛异常
    • DiscardPolicy:直接丢弃
    • DiscardOldestPolicy:丢弃任务队列中最早的未处理任务,然后将新任务加入队列;
    • CallerRunsPolicy:由提交任务的线程(通常是主线程)来执行这个任务

自定义线程池

ThreadPoolExecutor threadPool = ThreadPoolExecutor(
  																	int corePoolSize,
                                    int maximumPoolSize,
                                    long keepAliveTime,
                                    TimeUnit unit,
                                    BlockingQueue<Runnable> workQueue,
                                    ThreadFactory threadFactory,
                                    RejectedExecutionHandler handler);

ThreadPoolExecutor的某些常量定义

public class ThreadPoolExecutor extends AbstractExecutorService {
    
    private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
    private static final int COUNT_BITS = Integer.SIZE - 3;
    private static final int CAPACITY   = (1 << COUNT_BITS) - 1;

    //线程的状态
    private static final int RUNNING    = -1 << COUNT_BITS;
    private static final int SHUTDOWN   =  0 << COUNT_BITS;
    private static final int STOP       =  1 << COUNT_BITS;
    private static final int TIDYING    =  2 << COUNT_BITS;
    private static final int TERMINATED =  3 << COUNT_BITS;
  
  	private final HashSet<Worker> workers = new HashSet<Worker>();
}

主线程池控制状态 ctl 是一个原子整数(AtomicInteger),它打包了两个概念字段:

  • workerCount:记录的理论上应该存在的工作线程数,而线程池中实际活跃的线程数量pool size),是由内部 workers 集合来维护的,实际活跃线程数可能暂时与该值不同(后面提交任务会提到原因)
  • runState:表示线程池的运行状态,如运行中、关闭中等。

COUNT_BITS = Integer.SIZE - 3;线程池的状态占用Integer的高三位

CAPACITY = (1 << COUNT_BITS) - 1workerCount 的上限限制为 (2^29) - 1(大约 5 亿个线程);如果需要支持更多线程,可以将 ctl 改为 AtomicLong,同时调整相关的位移和掩码常量。

runState(运行状态):

  • RUNNING(运行中):接受新任务,并处理队列中的任务;
  • SHUTDOWN(关闭中):不再接受新任务,但继续处理队列中的任务;
  • STOP(停止):不接受新任务,也不处理队列中的任务,同时中断正在执行的任务;
  • TIDYING(整理中):所有任务已终止,workerCount 为 0,进入该状态的线程池将调用 terminated() 钩子方法进入TERMINATED状态
  • TERMINATED(已终止)terminated() 执行完毕。

状态转变的流程如下:

  • RUNNINGSHUTDOWN:调用 shutdown() 或在 finalize() 中隐式触发;
  • RUNNINGSHUTDOWNSTOP:调用 shutdownNow()
  • SHUTDOWNTIDYING:队列和线程池都为空时;
  • STOPTIDYING:线程池为空时;
  • TIDYINGTERMINATEDterminated() 方法执行完毕。

线程池提交任务的两种方式

execute()方法

只能提交 Runnable,没有返回值。

public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        
        int c = ctl.get();
  			//第一步
        if (workerCountOf(c) < corePoolSize) {
          //参数true:if true use corePoolSize as bound, else maximumPoolSize.
            if (addWorker(command, true))
                return;// 失败时更新 ctl 再进入第二步
            c = ctl.get();
        }
  			//第二步
        if (isRunning(c) && workQueue.offer(command)) {
            int recheck = ctl.get();
            if (! isRunning(recheck) && remove(command))
                reject(command);
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }//第三步
        else if (!addWorker(command, false))
            reject(command);
    }

addWorker(Runnable firstTask, boolean core):该方法用于检查当前线程池状态以及线程数量是否允许添加新的工作线程

  • firstTask:新线程应首先运行的任务(如果没有则为 null)。

  • core:布尔值,表示是否使用 corePoolSize(核心线程数)作为上限判断

addWorker方法中,先修改workerCount再修改workers,所以实际活跃线程数可能暂时workerCount不同

compareAndIncrementWorkerCount(c)//workerCount+1
...
w = new Worker(firstTask);
...
workers.add(w);//工作线程集合中加入一个线程

execute()整个流程分为三步

第一步:worker 数少于 corePoolSize,尝试创建新线程执行任务

第二步:任务尝试入队后,需要二次检查线程池状态

  • workQueue.offer(command):任务成功入队。

  • 然后需要二次检查 ctl 状态(recheck)

    • 如果状态已不是 RUNNING,说明 shutdown()shutdownNow() 被调用了,就要回滚任务(remove)并拒绝。
    • 否则,如果当前没有工作线程(可能任务入队时线程池空了),那就得启动一个线程去处理队列任务(addWorker(null, false))。

第三步:入队失败时,尝试再新建一个线程;否则拒绝任务

可结合下面这个图来理解

在这里插入图片描述

submit()方法

适用于 RunnableCallable 任务,依赖execute()执行任务

public Future<?> submit(Runnable task) {
    if (task == null) throw new NullPointerException();
    RunnableFuture<Void> ftask = newTaskFor(task, null);
    execute(ftask);
    return ftask;
}
 
public <T> Future<T> submit(Runnable task, T result) {
    if (task == null) throw new NullPointerException();
    RunnableFuture<T> ftask = newTaskFor(task, result);
    execute(ftask);
    return ftask;
}

public <T> Future<T> submit(Callable<T> task) {
    if (task == null) throw new NullPointerException();
    RunnableFuture<T> ftask = newTaskFor(task);
    execute(ftask);
    return ftask;
}
protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) {
      return new FutureTask<T>(runnable, value);
}

protected <T> RunnableFuture<T> newTaskFor(Callable<T> callable) {
        return new FutureTask<T>(callable);
}

submit它会将传进来的任务封装成RunnableFuture,然后将Future返回出去,调用者可以通过get方法获取返回结果

Worker线程管理

Worker线程

线程池为了掌握线程的状态并维护线程的生命周期,设计了线程池内的工作线程Worker

private final class Worker extends AbstractQueuedSynchronizer implements Runnable{

    final Thread thread;           // 真正运行任务的线程对象

    Runnable firstTask;           // 该 Worker 启动时要执行的第一个任务(构造时传入)

    volatile long completedTasks; // 该 Worker 运行的任务数量(用于统计)
}

firstTask用它来保存传入的第一个任务,这个任务可以有也可以为null。如果这个值是非空的,那么线程就会在启动初期立即执行这个任务,也就对应核心线程创建时的情况;如果这个值是null,那么就需要创建一个线程去执行任务列表(workQueue)中的任务,也就是非核心线程的创建

Worker执行任务的模型如下图所示:
在这里插入图片描述

Worker线程增加

private boolean addWorker(Runnable firstTask, boolean core) {
		......
		w = new Worker(firstTask);
  	final Thread t = w.thread;
		......
    workers.add(w);    
  	...
  	workerAdded = true;
  	........
  	if (workerAdded) {
        t.start();
        workerStarted = true;
    }
    .......
}

该方法的功能就是增加一个线程,该方法不考虑线程池是在哪个阶段增加的该线程,这个分配线程的策略是在上个步骤完成的,该步骤仅仅完成增加线程,并使它运行,最后返回是否成功这个结果。

启动worker的过程为:

Worker 构造函数中,会创建线程对象 thread

Worker(Runnable firstTask) {
    this.firstTask = firstTask;
    this.thread = getThreadFactory().newThread(this); // this 实现了 Runnable
}

这里 this 就是 Worker 实例。

因为 Worker 实现了 Runnable 接口,所以当执行 t.start() 时,底层会调用:

Worker.run()

而 Worker 的 run() 方法中,会进一步调用线程池的主循环方法:

public void run() {
    runWorker(this);
}

最终由 runWorker(Worker w) 来不断从队列中获取任务并执行。

Worker线程执行任务

worker调用了runWorker方法来执行任务,通过getTask()从工作队列中取任务

while (task != null || (task = getTask()) != null) 

在这里插入图片描述

Worker线程回收

线程池使用一张Hash表去持有线程的引用,这样可以通过添加引用、移除引用这样的操作来控制线程的生命周期。线程池中线程的销毁依赖JVM自动的回收,线程池做的工作是根据当前线程池的状态维护一定数量的线程引用,防止这部分线程被JVM回收,当线程池决定哪些线程需要回收时,只需要将其引用消除即可。

private final HashSet<Worker> workers = new HashSet<Worker>();

这个时候重要的就是如何判断线程是否在运行

Worker被创建出来后,就会不断地进行轮询,然后通过getTask()获取任务去执行,核心线程可以无限等待获取任务,非核心线程要限时获取任务。当Worker无法获取到任务,也就是获取的任务为空时,循环会结束,Worker会主动消除自身在线程池内的引用。

线程回收的工作是在processWorkerExit方法完成的。

try {
    while (task != null || (task = getTask()) != null) {
    .......
    } finally {
    processWorkerExit(w, completedAbruptly);
}

processWorkerExit中将worker引用移出wokers

另外,Worker是通过继承AQS来实现独占锁这个功能,线程池在执行shutdown方法或tryTerminate方法时会调用interruptIdleWorkers方法来中断空闲的线程,interruptIdleWorkers方法会使用tryLock方法来判断线程池中的线程是否是空闲状态;如果线程是空闲状态则可以安全回收。
在这里插入图片描述

线程池拒绝策略的使用场景

  • AbortPolicy(默认策略):适合重要性高且时效性高的场景,必须确保关键任务(如支付场景、订单提交)正常执行,拒绝后需要快速失败并通知调用方处理异常
  • CallerRunsPolicy(调用者执行策略):适合重要性高 + 时效性低,允许任务延迟执行,但不能丢弃(如异步日志写入、非核心业务通知)
  • DiscardPolicy(直接丢弃策略):适合重要性低且时效性低的场景,非关键任务(如统计数据采集),允许少量丢失
  • DiscardOldestPolicy(丢弃最旧任务策略):适合重要性低且时效性高的场景,新任务比旧任务更重要(如实时状态更新、动态配置刷新),保留最新数据。

线程池的几种创建方法

  • 固定大小线程池:使用 newFixedThreadPool(),线程池大小固定,适用于处理稳定的任务数量。

    ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);  
    
  • 可缓存线程池:使用 newCachedThreadPool(),适用于处理大量短时间的异步任务。newCachedThreadPool() 创建一个大小不固定的线程池,根据需要创建新的线程,空闲线程超过 60 秒则会被终止。

    ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
    
  • 单线程线程池:使用 newSingleThreadExecutor(),适用于顺序执行任务的场景。

    ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
    
  • 定时任务线程池:使用 newScheduledThreadPool(),适用于需要定时或周期性任务的场景。

    ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(2);
    
  • 自定义线程池:通过 ThreadPoolExecutor 灵活控制线程池的参数,适用于需要对线程池进行精细化管理的场景。

他们之间的关系可以看下面这张图

在这里插入图片描述

线程池的关闭方式

shutdown:使用这个方法之后,无法提交新的任务进来,线程池会继续工作,将手头的任务执行完再停止

executor.shutdown();

shutdownNow:这种停止方式就比较粗暴了,立即中断所有正在执行的线程,返回尚未开始执行的任务列表(List<Runnable>),通常用于异常退出、紧急停机

List<Runnable> notExecutedTasks = executor.shutdownNow();

线程池的大小如何设定?

CPU 密集型任务(N+1): 这种任务消耗的主要是 CPU 资源,可以将线程数设置为 N(CPU 核心数)+1。

I/O 密集型任务(2N)CPU 核心数的两倍

1.1 CPU 密集型任务

  • 定义:CPU 密集型任务主要消耗的是 CPU 资源,比如数学计算、图像处理等。
  • 推荐线程数:通常情况下,线程数应该设置为 CPU 核心数 + 1 或 CPU 核心数,这样可以让 CPU 保持高负载,同时减少上下文切换的开销。
    • 公式:线程池大小 = CPU 核心数 + 1
    • 其中 +1 是为了应对偶尔的 I/O 操作或任务挂起情况,这样即使一个线程暂时被挂起,CPU 依然有线程可以执行其他任务。

1.2 I/O 密集型任务

  • 定义:I/O 密集型任务通常会涉及大量的 I/O 操作,比如网络请求、文件读写等,这些任务会导致线程在等待 I/O 操作完成时空闲。
  • 推荐线程数:线程池的大小可以设置为 (CPU 核心数 × 2) 或 (CPU 核心数 × 2) ~ (CPU 核心数 × 4)。I/O 密集型任务在执行过程中会有大量的等待时间,因此可以创建更多的线程来并发处理任务。
    • 公式:线程池大小 = CPU 核心数 × (1 + 任务等待时间与任务计算时间之比)
    • 该公式通过考虑任务的计算时间和等待时间的比例来调整线程池的大小。

笔者也认为不用太纠结这个固定的方式,可以灵活应对

这篇文章写的还可以:https://mp.weixin.qq.com/s/IdJhR7ANn3iCno1CVsF4WA

思考

为什么先要用corePoolSize核心线程,然后当核心线程处理不过来时将异步任务先放到workQueue中,而不是直接开maximumPoolSize的线程数继续处理应急任务呢?

假设任务处理不过来之后,直接创建maximumPoolSize个线程处理任务,会存在以下问题:

  • 线程数暴增导致资源耗尽:如果一波高并发任务突然涌入,线程池马上创建大量线程,会导致内存压力急剧增加,出现 频繁 GC,甚至 OOM(OutOfMemoryError)。工作队列(如 LinkedBlockingQueue)可以暂时缓存任务,让系统更平稳处理流量波动。它提供了一种“削峰填谷”的机制。
  • 非核心线程(core 以外的线程)需要一定时间才会被回收(受 keepAliveTime 控制)。在此期间,系统依然维持大量空闲线程,占用资源。

最大线程数通常用于应对短时间的突发负载,他是一种应急措施。

参考链接:
https://tech.meituan.com/2020/04/02/java-pooling-pratice-in-meituan.html
https://mp.weixin.qq.com/s/JHgRDz7qGAiqpHq5ToL85Q

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

liubo666_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值