如何设计一个线程池

其实设计线程池本质上就是问你现有的线程池的原理,只是让你换个思路回答

设计一个线程池应该从以下几个方面入手:

  • 线程池有哪些状态,如何维护这个状态

  • 重要的成员变量有哪些?

  • 任务执行机制是什么?

  • 拒绝策略有哪些?

  • 线程怎么封装?用什么来存储线程?

线程池有哪些状态,如何维护这个状态?

线程的状态分为:

  1. RUNNING:这是最正常的状态,接受新的任务,处理等待队列中的任务。
  2. SHUTDOWN:不接受新的任务提交,但是会继续处理等待队列中的任务。
  3. STOP:不接受新的任务提交,不再处理等待队列中的任务,中断正在执行任务的线程。
  4. TIDYING:所有的任务都销毁了,workCount 为 0,线程池的状态在转换为 TIDYING 状态时,会执行钩子方法 terminated()。
  5. TERMINATED:terminated() 方法结束后,线程池的状态就会变成这个。

img

如何维护?

我们可以使用AtomicInteger类型的变量来维护两个值:运行状态(runState)和线程数量 (workerCount)。高3位保存runState,低29位保存workerCount。用一个变量去存储两个值,可避免在做相关决策时,出现不一致的情况,不必为了维护两者的一致,而占用锁资源。

重要的成员变量有哪些?

  • `corePoolSize :核心线程数,定义了最小可以同时运行的线程数。

  • maximumPoolSize :线程池中允许存在的最大工作线程数。

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

任务执行机制是怎么样的?

这部分完成的工作是:检查现在线程池的运行状态、运行线程数、运行策略,决定接下来执行的流程,是直接申请线程执行,或是缓冲到队列中执行,亦或是直接拒绝该任务。其执行过程如下:

  1. 首先检测线程池运行状态,如果不是RUNNING,则直接拒绝,线程池要保证在RUNNING的状态下执行任务。
  2. 如果workerCount < corePoolSize,则创建并启动一个线程来执行新提交的任务。
  3. 如果workerCount >= corePoolSize,且线程池内的阻塞队列未满,则将任务添加到该阻塞队列中。
  4. 如果workerCount >= corePoolSize && workerCount < maximumPoolSize,且线程池内的阻塞队列已满,则创建并启动一个线程来执行新提交的任务。
  5. 如果workerCount >= maximumPoolSize,并且线程池内的阻塞队列已满, 则根据拒绝策略来处理该任务, 默认的处理方式是直接抛异常。

图4 任务调度流程

拒绝策略有哪些?

如果当工作队列已满并且同时运行的线程数达到最大工作线程数时,定义一些策略:

  • AbortPolicy(默认):抛出异常来拒绝新任务的处理。

  • `CallerRunsPolicy:用调用者所在的线程来执行任务。但是这种策略会降低对于新任务提交速度,影响程序的整体性能。

  • DiscardPolicy:不处理新任务,直接丢弃掉。

  • DiscardOldestPolicy:丢弃最早的未处理的任务。

如何封装线程?

首先封装的线程类(我们可以称为Worker)要执行任务,因此要包含一个Thread线程,通过这个Thread线程来执行任务。Worker还应该持有任务,我们可以用Runnable变量(firstTask)来保存传入的第一个任务,这个任务可以有也可以为null。如果这个值是非空的,那么线程就会在启动初期立即执行这个任务,也就对应核心线程创建时的情况;如果这个值是null,那么就需要创建一个线程去执行任务列表(workQueue)中的任务,也就是非核心线程的创建。

实际的Worker线程

private final class Worker extends AbstractQueuedSynchronizer implements Runnable{
    final Thread thread;//Worker持有的线程
    Runnable firstTask;//初始化的任务,可以为null
}

Worker执行任务的模型如下图所示:

图7 Worker执行任务

线程池需要管理线程的生命周期,需要在线程长时间不运行的时候进行回收。线程池使用一张Hash表去持有线程的引用,这样可以通过添加引用、移除引用这样的操作来控制线程的生命周期。这个时候重要的就是如何判断线程是否在运行。

Worker可以通过继承AQS,使用AQS来实现独占锁这个功能。不使用可重入锁ReentrantLock,而是使用AQS,为的就是实现不可重入的特性去反应线程现在的执行状态。

1.lock方法一旦获取了独占锁,表示当前线程正在执行任务中。

2.如果正在执行任务,则不应该中断线程。

3.如果该线程现在不是独占锁的状态,也就是空闲的状态,说明它没有在处理任务,这时可以对该线程进行中断。

4.线程池在执行shutdown方法时,我们需要判断线程池中的线程是否是空闲状态;如果线程是空闲状态则可以安全回收。

参考链接:

Java线程池实现原理及其在美团业务中的实践

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

一定会去到彩虹海的麦当

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

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

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

打赏作者

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

抵扣说明:

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

余额充值