15.jdk源码阅读之线程池(下)

1. 写在前面

上一篇文章,我们介绍了线程池,这篇文章我们继续介绍线程池。只不过是更加深入,我先抛出几个问题,看看大家工作中有没有遇到过:

  1. 线程池的拒绝策略应该使用哪种,实际工作中是怎么用的?
  2. 线程池的状态有哪些?
  3. 阿里java开发手册中关于线程池的建议 为什么那么建议?

2. 拒绝策略的选择

线程池的拒绝策略选择应根据具体的应用场景和需求来确定。在实际工作中,不同的应用场景对拒绝策略的要求可能有所不同。

2.1 AbortPolicy(默认策略)

直接抛出 RejectedExecutionException 异常,通知调用者任务被拒绝。
适用场景:

  • 当任务被拒绝时,需要立即通知调用者,并且调用者能够处理这个异常。
  • 适用于对任务丢失敏感的场景,需要保证任务不被默默丢弃。
ThreadPoolExecutor executor = new ThreadPoolExecutor(
    1, 1, 0L, TimeUnit.MILLISECONDS,
    new LinkedBlockingQueue<>(1),
    new ThreadPoolExecutor.AbortPolicy()
);

2.2 CallerRunsPolicy

由调用线程来运行被拒绝的任务。
适用场景:

  • 当线程池已满时,希望调用者线程自己执行任务,以减缓任务提交速度。
  • 适用于需要一定的自适应机制来处理任务高峰的场景。

2.3 DiscardPolicy

直接丢弃被拒绝的任务,不抛出异常。
适用场景:

  • 当可以接受任务丢失的场景,如日志记录、统计信息等非关键任务。
  • 适用于对任务丢失不敏感的应用,且任务量可能会瞬间激增。

2.4 DiscardOldestPolicy

丢弃队列中最老的任务,然后重新提交被拒绝的任务。
适用场景:

  • 当希望优先处理新任务,而可以接受丢弃较老任务的场景。
  • 适用于任务队列中存在优先级差异的情况,确保最新任务能够被处理。

2.5 实际工作中的使用

在实际工作中,选择哪种拒绝策略通常取决于以下几个因素:

  1. 任务的重要性:如果每个任务都非常重要,不能丢失,那么 AbortPolicy 可能是合适的选择。
  2. 系统的负载能力:如果系统需要自适应高负载,CallerRunsPolicy 可以帮助平衡负载。
  3. 任务的性质:对于一些非关键任务,如日志记录,可以使用 DiscardPolicy 或 DiscardOldestPolicy。
  4. 业务需求:根据具体业务逻辑,可能需要自定义拒绝策略来满足特定需求。

2.6 自定义拒绝策略

在某些情况下,内置的拒绝策略可能无法完全满足需求,可以通过实现 RejectedExecutionHandler 接口来自定义拒绝策略。

class CustomRejectedExecutionHandler implements RejectedExecutionHandler {
    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        // 自定义处理逻辑,如记录日志、报警等
        System.err.println("Task rejected: " + r.toString());
    }
}

ThreadPoolExecutor executor = new ThreadPoolExecutor(
    1, 1, 0L, TimeUnit.MILLISECONDS,
    new LinkedBlockingQueue<>(1),
    new CustomRejectedExecutionHandler()
);

3. 线程池使用最佳实践

3.1 线程池不允许使用 Executors 创建

3.1.1 原因

Executors 提供的几种创建线程池的方法存在一些潜在的问题,比如无界队列可能导致 OOM(OutOfMemoryError),固定大小线程池和单线程池可能导致任务堆积等。

3.1.2 建议

使用 ThreadPoolExecutor 来创建线程池,并明确各个参数的含义。

3.1.3 示例

// 推荐的创建线程池方式
ThreadPoolExecutor executor = new ThreadPoolExecutor(
    corePoolSize, // 核心线程数
    maximumPoolSize, // 最大线程数
    keepAliveTime, // 空闲线程存活时间
    TimeUnit.SECONDS, // 时间单位
    new LinkedBlockingQueue<>(queueCapacity), // 阻塞队列
    new ThreadFactory() {
        @Override
        public Thread newThread(Runnable r) {
            Thread thread = new Thread(r);
            thread.setName("custom-thread-name");
            return thread;
        }
    },
    new ThreadPoolExecutor.AbortPolicy() // 拒绝策略
);

3.2 线程池的配置要根据实际需求进行调整

3.2.1 原因

线程池的参数配置(如核心线程数、最大线程数、队列容量等)直接影响系统的性能和稳定性。

3.2.2 建议

根据具体的业务需求和系统负载情况来配置线程池的参数。

3.2.3 示例

int corePoolSize = 10;
int maximumPoolSize = 20;
long keepAliveTime = 60L;
int queueCapacity = 100;
ThreadPoolExecutor executor = new ThreadPoolExecutor(
    corePoolSize,
    maximumPoolSize,
    keepAliveTime,
    TimeUnit.SECONDS,
    new LinkedBlockingQueue<>(queueCapacity),
    new ThreadPoolExecutor.AbortPolicy()
);

3.3 线程池的拒绝策略要合理选择

3.3.1 原因

当线程池和队列都满了,必须要有合理的拒绝策略来处理新提交的任务,以避免系统崩溃。

3.3.2 建议

根据业务需求选择合适的拒绝策略,必要时可以自定义拒绝策略。

3.3.3 示例

ThreadPoolExecutor executor = new ThreadPoolExecutor(
    corePoolSize,
    maximumPoolSize,
    keepAliveTime,
    TimeUnit.SECONDS,
    new LinkedBlockingQueue<>(queueCapacity),
    new ThreadPoolExecutor.CallerRunsPolicy() // 选择CallerRunsPolicy拒绝策略
);

4. ThreadPoolExecutor

4.1 核心组件

4.1.1 主要字段

  • corePoolSize:核心线程数,即线程池中始终保持存活的线程数量。
  • maximumPoolSize:最大线程数,即线程池中允许的最大线程数量。
  • keepAliveTime:非核心线程的存活时间,当线程池中的线程数超过核心线程数时,多余的线程在空闲时间超过 keepAliveTime 后会被终止。
  • workQueue:任务队列,用于存放等待执行的任务。
  • threadFactory:线程工厂,用于创建新线程。
  • handler:拒绝策略,当任务无法被执行时会调用该策略。

4.1.2 内部类

  • Worker:ThreadPoolExecutor 中的工作线程,继承自 AbstractQueuedSynchronizer(AQS),并实现了 Runnable 接口。
  • TaskQueue:任务队列,通常是 BlockingQueue 的实现类,如 LinkedBlockingQueue、ArrayBlockingQueue 等。

4.2 任务提交与执行流程

当任务被提交到线程池时,ThreadPoolExecutor 会按照以下步骤处理:

  1. execute 方法:任务提交的入口方法。
  2. 判断当前线程池中的线程数是否小于 corePoolSize
    • 如果是,则创建一个新的 Worker 并启动。
    • 如果否,则将任务加入到任务队列 workQueue 中。
  3. 如果任务队列已满且线程数小于 maximumPoolSize
    • 创建新的 Worker 并启动
  4. 如果线程数已达到 maximumPoolSize 并且任务队列已满
    • 执行拒绝策略 handler
      以下是 execute 方法的核心代码:
public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();
    
    int c = ctl.get();
    if (workerCountOf(c) < corePoolSize) {
        if (addWorker(command, true))
            return;
        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);
}

4.3 线程池的状态控制

ThreadPoolExecutor 使用一个 AtomicInteger 类型的字段 ctl 来同时记录线程池的状态和线程数量。ctl 的高 3 位表示线程池的状态,低 29 位表示线程数量。

4.3.1 线程池状态

线程池的状态有以下几种:

  • RUNNING:接受新任务并处理队列中的任务。
  • SHUTDOWN:不接受新任务但处理队列中的任务。
  • STOP:不接受新任务,不处理队列中的任务,并中断正在执行的任务。
  • TIDYING:所有任务都已终止,workerCount 为 0,线程池将要转换为 TERMINATED 状态。
  • TERMINATED:终止状态。

4.3.2 状态转换

状态转换通过 ctl 字段的高 3 位和低 29 位的位运算来实现:

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 static int runStateOf(int c)     { return c & ~CAPACITY; }
private static int workerCountOf(int c)  { return c & CAPACITY; }
private static int ctlOf(int rs, int wc) { return rs | wc; }

系列文章

1.JDK源码阅读之环境搭建

2.JDK源码阅读之目录介绍

3.jdk源码阅读之ArrayList(上)

4.jdk源码阅读之ArrayList(下)

5.jdk源码阅读之HashMap

6.jdk源码阅读之HashMap(下)

7.jdk源码阅读之ConcurrentHashMap(上)

8.jdk源码阅读之ConcurrentHashMap(下)

9.jdk源码阅读之ThreadLocal

10.jdk源码阅读之ReentrantLock

11.jdk源码阅读之CountDownLatch

12.jdk源码阅读之CyclicBarrier

13.jdk源码阅读之Semaphore

14.jdk源码阅读之线程池(上)

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

至真源

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

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

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

打赏作者

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

抵扣说明:

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

余额充值