多线程 线程池ThreadPoolExecutor

多线程 线程池ThreadPoolExecutor

线程池是java提供的线程管理容器,它允许开发者定义多个线程,并开启一些额外线程,提供一个自定义的任务队列和队列满时对新任务的处理策略。

使用

 ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(
     10,// 核心线程数,这些线程不会被淘汰
     15,// 最大线程数 = 核心线程数+额外线程数
     10,// 额外线程数超时淘汰时间
     TimeUnit.SECONDS,//额外线程数超时淘汰时间单位
     new ArrayBlockingQueue<>(10),//任务队列
     new ThreadPoolExecutor.AbortPolicy()//拒绝策略
 );
// 提交任务,lambda表达式
poolExecutor.submit(()->{/*业务代码*/});
...
// 提交任务,多线程实体
Runnable runnable = new RunnableDemo();
poolExecutor.submit(runnable);
...
    
// 拒绝接受任务,执行完所有池内以及任务队列中的全部任务
service.shutdown();
// 在超时时间内等待任务执行完成,常用于if条件;在线程池中工作完全结束后执行其他业务
bool is_finish=poolExecutor.awaitTermination(100, TimeUnit.HOURS)

// 拒绝接受任务且立即干掉所有正在执行的任务,不推荐使用
service.shutdownNow();

线程池工作流程

以上述代码为例,初始化的线程池的任务队列和是空的,线程池中仅存在有核心线程,线程也无任务执行,第一次提交任务给线程池,因为核心线程无一个有任务正在执行,所以直接随机调用一个核心线程执行提交的任务。

接着任务变多,核心线程数已经不够用了,若此时已提交 10个任务,它们都在被核心线程执行且均未执行完毕,那么第11个任务提交后,线程池开启额外线程数执行,注意哈!此时第11个任务并未被放入任务队列中;随着任务越来越多,某一时刻已经提交到第16个任务,且前15个任务均未被执行完毕;此时第16个任务就被安排进入任务队列,等待第一个执行完任务的线程池取用它;

某一时刻,在前15个任务没有执行完毕的前提下,你又提交了9个任务,算上上次提交的第16个任务,已经提交了10个不能被立即执行的任务了,它们均被放入任务队列中,已知任务队列最大可容纳10个任务,当第26个任务被提交时,此时队列已经容纳不了这个任务了,所以这个任务应该被怎么处理呢?这就取决于你给出的拒绝策略了。

任务队列

ArrayBlockingQueue(基础)

  • 基于数组的有界阻塞队列。它按照先进先出(FIFO)的原则存储和提供元素。
  • 队列长度固定,可通过构造函数指定。
  • 当队列满时,put操作将阻塞,直到队列中的元素被消费。

LinkedBlockingQueue(基础)

  • 基于链表结构的阻塞队列,可以设置容量上限,如果不设置则默认为Integer.MAX_VALUE,即几乎无限。
  • 它同样遵循FIFO原则。
  • 适合于需要一个大容量队列的场景,或者需要一个无限队列来缓冲任务。

PriorityBlockingQueue(高阶)

  • 一个无界的优先级队列,使用堆数据结构实现。
  • 元素按照自然排序或比较器决定的顺序进行排序。
  • 当队列满时,put操作不会阻塞,因为它是无界的,但是它会抛出IllegalArgumentException如果队列无法接受更多的元素(例如,因为元素不可比较)。

SynchronousQueue(高阶)

  • 一个不存储元素的阻塞队列,每个put操作必须等待一个take操作,反之亦然。
  • 它常被用作线程间传递数据的通道,而不是一个真正的队列。
  • 特别适用于希望直接将任务传递给线程,而无需缓冲的情况。

DelayQueue

  • 一个持有Delayed元素的无界阻塞队列。
  • 元素只有在其延迟到期后才能被消费者线程提取。
  • 通常用于实现定时任务或延迟任务的场景。

拒绝策略

AbortPolicy

  • 默认的拒绝策略
  • 当线程池无法接受更多任务时,AbortPolicy会抛出RejectedExecutionException异常,这会中断提交任务的线程。这种策略对于调试很有帮助,因为它可以立即通知你线程池已经超载,生产环境中勿用。

⭐ ​CallerRunsPolicy

  • 当线程池无法接受更多任务时,不将其放入队列或创建新的线程,而是提交任务的线程直接执行此任务。相当于回到一个没有线程池的情况,即调用execute方法的线程自己执行自己提交的任务。这种策略可能会降低系统的响应速度,因为它会占用原本用于提交任务的线程。

DiscardPolicy

  • 默默地丢弃无法处理的任务,不抛出任何异常
  • 这种策略对于统计计算的任务丢失会导致灾难性后果。

DiscardOldestPolicy

  • 当线程池无法接受更多任务时,丢弃任务队列中最早的任务,然后尝试再次提交当前任务。这种策略试图保护最近的任务,牺牲最早的任务,它可能在处理实时或最新数据时有用,但如果任务依赖于队列中的顺序,这种策略可能会导致问题。

⚠️ 注意事项

  • 核心线程数不能大于最大线程数

  • 最大线程数不能小于核心线程数,且建议最大线程数+队列长度≥提交的任务数

  • 仅接受实现了Runnable接口的任务

  • 额外线程数超时淘汰时间&额外线程数超时淘汰时间单位:根据实际运行情况设置,最好运行几次多线程任务后计算出一个平衡值

  • 任务队列长度最好和最大线程数相等,不然很容易出以下的BUG

    Exception in thread "main" java.util.concurrent.RejectedExecutionException: Task java.util.concurrent.FutureTask@7c30a502 rejected from java.util.concurrent.ThreadPoolExecutor@5594a1b5[Running, pool size = 100, active threads = 100, queued tasks = 10, completed tasks = 0]
    	at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2063)
    	at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:830)
    	at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1379)
    	at java.util.concurrent.AbstractExecutorService.submit(AbstractExecutorService.java:112)
    
  • 建议使用for循环提交任务时给一个sleep睡眠时间,让主线程让出时间片,让线程池抢占使用,不然一下子提交的数量超过最大线程数+任务队列,会报错!

    for (int i = 0; i < 200; i++) {
        Thread.sleep(10);
        poolExecutor.submit(/*业务*/);
    }
    
posted @   勤匠  阅读(13)  评论(0编辑  收藏  举报
编辑推荐:
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示