exector:ThreadPoolExecutor 类介绍 $

ThreadPoolExecutor 类是Excutor的底层实现

构造方法(4个):

复制代码
    public ThreadPoolExecutor(int corePoolSize,//线程池的核心线程数量
                              int maximumPoolSize,//线程池的最大线程数
                              long keepAliveTime,//当线程数大于核心线程数时,多余的空闲线程存活的最长时间
                              TimeUnit unit,//时间单位
                              BlockingQueue<Runnable> workQueue,//任务队列,用来储存等待执行任务的队列
                              ThreadFactory threadFactory,//线程工厂,用来创建线程,一般默认即可
                              RejectedExecutionHandler handler//拒绝策略,当提交的任务过多而不能及时处理时,我们可以定制策略来处理任务
                               ) {
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }
复制代码

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

  maxinumPoolSize:最大线程数,任务队列达到队列容量时,最大可以同时运行的线程数量变为最大线程数。 

  workQueue:当前线程数超过corePoolSize,线程被放入workQueue

  unit:时间单位

  keepAtiveTime:其余线程可存活的最长时间

  threadFactory:线程工厂,用来executer创建线程

  handler: 饱和策略(拒绝策略)

ThreadPoolExecuter饱和策略定义:
  当前同时运行的线程数量达到最大线程数,且队列已经被放满,ThreadPoolTaskExecutor定义一些策略:
  • AbortPolicy:直接抛出异常,RejectedExecutionException,拒绝新任务的处理。(spring默认)
  • CallerRunsPolicy:任务的执行由注入的线程自己执行,就是直接在调用execute方法的线程中运行(run)被拒绝的任务,如果执行程序已关闭,则会丢弃该任务。
    •   因此这种策略会降低对于新任务提交速度,影响程序的整体性能。如果您的应用程序可以承受此延迟并且你要求任何一个任务请求都要被执行的话,你可以选择这个策略。(可伸缩队列)
  • DiscardPolicy:不处理新任务,直接丢弃掉。
  • DiscardOldPolicy:丢弃最早的未处理的任务请求。
Executors 返回线程池对象的弊端如下:
  • FixedThreadPoolSingleThreadExecutor : 使用的是无界的 LinkedBlockingQueue,任务队列最大长度为 Integer.MAX_VALUE,可能堆积大量的请求,从而导致 OOM。

  • CachedThreadPool :使用的是同步队列 SynchronousQueue, 允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致 OOM。

  • ScheduledThreadPoolSingleThreadScheduledExecutor : 使用的无界的延迟阻塞队列DelayedWorkQueue,任务队列最大长度为 Integer.MAX_VALUE,可能堆积大量的请求,从而导致 OOM。

线程池常用的阻塞队列总结:

  新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。
   LinkedBlockingQueue(无界队列):FixedThreadPoolSingleThreadExector 。容量为 Integer.MAX_VALUE,由于队列永远不会被放满,因此FixedThreadPool最多只能创建核心线程数的线程。
  SynchronousQueue(同步队列) :CachedThreadPoolSynchronousQueue 没有容量,不存储元素,目的是保证对于提交的任务,如果有空闲线程,则使用空闲线程来处理;否则新建一个线程来处理任务。也就是说,CachedThreadPool 的最大线程数是 Integer.MAX_VALUE ,可以理解为线程数是可以无限扩展的,可能会创建大量线程,从而导致 OOM。
  DelayedWorkQueue(延迟阻塞队列):ScheduledThreadPoolSingleThreadScheduledExecutorDelayedWorkQueue 的内部元素并不是按照放入的时间排序,而是会按照延迟的时间长短对任务进行排序,内部采用的是“堆”的数据结构,可以保证每次出队的任务都是当前队列中执行时间最靠前的。DelayedWorkQueue 添加元素满了之后会自动扩容原来容量的 1/2,即永远不会阻塞,最大扩容可达 Integer.MAX_VALUE,所以最多只能创建核心线程数的线程。
FixedThreadPool是因为阻塞队列可以很大(最大为Integer最大值),故几乎不会触发拒绝策略;   
CachedThreadPool是因为线程池很大(最大为Integer最大值),几乎不会导致线程数量大于最大线程数,故几乎不会触发拒绝策略。
  PriorityBlockingQueue 作为线程池的任务队列- 提交的任务具备 排序能力
复制代码
PriorityBlockingQueue 是一个支持优先级无界阻塞队列,可以看作是线程安全的 PriorityQueue,两者底层都是使用小顶堆形式的二叉堆,即值最小的元素优先出队。不过,PriorityQueue 不支持阻塞操作。
要想让 PriorityBlockingQueue 实现对任务的排序,传入其中的任务必须是具备排序能力的,方式有两种:
    提交到线程池的任务实现 Comparable 接口,并重写 compareTo 方法来指定任务之间的优先级比较规则。
  创建 PriorityBlockingQueue 时传入一个 Comparator 对象来指定任务之间的排序规则(推荐)。
ThreadPoolExecutor的submit、invokeXxx、execute方法入参都是Runnable、Callable,均不具备可排序的属性。可以弄一个实现类 PriorityTask,加一些额外的属性,让它们具备排序能力。
  实现Runnable接口,作为任务和存储在优先级队列中的Comparable接口。此类包含用于存储任务优先级的Priority属性,属性值高的任务将先被执行,compareTo()方法在优先级队列中确定任务的排序。
  在Main类中,发送20个具有不同优先级的任务执行器,首个发送到执行器的任务将被第一个执行。当执行器空闲时,任务只要到达就会立即执行第一个任务。
    实现声明在Comparable接口中的compareTo()方法。它将PriorityTask对象作为参数接收,并比较当前对象和作为参数的对象的优先级。设置高优先级任务在低优先级任务之前执行:
      @Override
      public int compareTo(MyPriorityTask o) {
          return Integer.compare(o.getPriority(), this.getPriority());
      }
存在一些风险和问题,比如: 
  PriorityBlockingQueue 是无界的,可能堆积大量的请求,从而导致 OOM。可能会导致饥饿问题,即低优先级的任务长时间得不到执行。
由于需要对队列中的元素进行排序操作以及保证线程安全(并发控制采用的是可重入锁 ReentrantLock),因此会降低性能。
对于 OOM 这个问题的解决比较简单粗暴,就是继承PriorityBlockingQueue 并重写一下 offer 方法(入队)的逻辑,当插入的元素数量超过指定值就返回
false 。   
  饥饿问题这个可以通过优化设计来解决(比较麻烦),比如等待时间过长的任务会被移除并重新添加到队列中,但是优先级会被提升。   
  对于性能方面的影响,是没办法避免的,毕竟需要对任务进行排序操作。并且,对于大部分业务场景来说,这点性能影响是可以接受的。
复制代码
复制代码
队列选取:
    功能性:比如延时,排序等功能,如果用到排序功能,这个时候就需要考虑PriorityBlockingQueue队列。
    容量:如果队列有存储的要求或者是直接传递的要求,就要选择不同的阻塞队列。
      有固定容量的队列 ArrayBlockingQueue;
      默认是无限容量的队列 LinkedBlockingQueue;
      有的队列没有任何容量的,如同步队列SynchronousQueue;
      DelayQueue的容量为Integer.MAX_VALUE,不同的队列之间千差万别,我们需要根据任务数量来推算出合适的队列。 能否扩容:从扩容方面考虑 ,在开发过程中,对于一些业务我们没有办法估计他的容量,业务可能有高峰期和低谷期,开一个固定容量无法满足业务需求,这个时候需要选择动态扩容。
      所以 ArrayBlockingQueue是不可以使用的,他在创建时容量就固定了,无法扩容。PriorityBlockingQueue 即使在指定了初始容量之后,后续如果有需要,也可以自动扩容。所以 我们可以根据是否需要扩容来选取合适的队列。 内存结构:队列的内存结构分为两种,一种是数据结构的,如ArrayBlockingQueue,还有一种是链表结构的,如LinkedBlockingQueue,ArrayBlockingQueue 没有链表所需要的“节点”,空间利用率更高。 性能:比如 LinkedBlockingQueue 由于拥有两把锁,它的操作粒度更细,在并发程度高的时候,相对于只有一把锁的 ArrayBlockingQueue 性能会更好。
      另外,SynchronousQueue 性能往往优于其他实现,因为它只需要“直接传递”,而不需要存储的过程。如果我们的场景需要直接传递的话,可以优先考虑 SynchronousQueue。
复制代码

 

线程池原理分析:

   线程池会先执行corePoolSize数量的线程,这里是五个。然后这些任务有任务被执行完的话,就会去拿新的任务执行。

for(int i=0;i<10;i++){
       Runnable worker=new MyRunnable(""+i);
       executor.execute(worker);
}

线程池原理分析(面试回答):

线程池实现类 ThreadPoolExecutorExecutor 框架最核心的类。

1.线程池创建 ThreadPoolExecutor 七个参数

2.线程池执行任务的流程:executor.execute();

3.在 execute 方法中,多次调用 addWorker 方法。addWorker 这个方法主要用来创建新的工作线程,如果返回 true 说明创建和启动工作线程成功,否则的话返回的就是 false。

整个流程(对整个逻辑进行了简化,方便理解):

  1.如果当前运行的线程数小于核心线程数,那么就会新建一个线程来执行任务。

  2.如果当前运行的线程数等于或大于核心线程数,但是小于最大线程数,那么就把该任务放入到任务队列里等待执行。

  3.如果向任务队列投放任务失败(任务队列已经满了),但是当前运行的线程数是小于最大线程数的,就新建一个线程来执行任务。

  4.如果当前运行的线程数已经等同于最大线程数了,新建线程将会使当前运行的线程超出最大线程数,那么当前任务会被拒绝,饱和策略会调RejectedExecutionHandler.rejectedExecution()方法。

 

线程数怎么选择:

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

    比 CPU 核心数多出来的一个线程是为了防止线程偶发的缺页中断,或者其它原因导致的任务暂停而带来的影响。一旦任务暂停,CPU 就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间。

I/O 密集型任务(2N): 这种任务应用起来,系统会用大部分的时间来处理 I/O 交互,而线程在处理 I/O 的时间段内不会占用 CPU 来处理,这时就可以将 CPU 交出给其它线程使用。因此在 I/O 密集型任务的应用中,我们可以多配置一些线程,具体的计算方法是 2N。

如何判断是 CPU 密集任务还是 IO 密集任务?

  CPU 密集型简单理解就是利用 CPU 计算能力的任务比如你在内存中对大量数据进行排序

  但凡涉及到网络读取,文件读取这类都是 IO 密集型,这类任务的特点是 CPU 计算耗费时间相比于等待 IO 操作完成的时间来说很少,大部分时间都花在了等待 IO 操作完成上。


几个常见对比:

1.Runnable vs Callable

  Runnable自 Java 1.0 以来一直存在,但Callable仅在 Java 1.5 中引入。

  Runnable 接口不会返回结果或抛出检查异常,但是 Callable 接口可以。

  所以,如果任务不需要返回结果或抛出异常推荐使用 Runnable 接口,这样代码看起来会更加简洁。

  工具类 Executors 可以实现将 Runnable 对象转换成 Callable 对象。(Executors.callable(Runnable task)Executors.callable(Runnable task, Object result)

复制代码
@FunctionalInterface
public interface Runnable {
   /**
    * 被线程执行,没有返回值也无法抛出异常
    */
    public abstract void run();
@FunctionalInterface
public interface Callable<V> {
    /**
     * 计算结果,或在无法这样做时抛出异常。
     * @return 计算得出的结果
     * @throws 如果无法计算结果,则抛出异常
     */
    V call() throws Exception;
}
复制代码

2.execute() vs submit()

  ·execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否;

  ·submit()方法用于提交需要返回值的任务。线程池会返回一个 Future 类型的对象,通过这个 Future 对象可以判断任务是否执行成功,并且可以通过 Futureget()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用 get(long timeout,TimeUnit unit)方法的话,如果在 timeout 时间内任务还没有执行完,就会抛出 java.util.concurrent.TimeoutException

复制代码
Future<String> future= executorService.submit(() -> {
    try {
        Thread.sleep(5000L);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    return "abc";
});

String s = future.get();
String s = future.get(3, TimeUnit.SECONDS);
复制代码

3.shutdown()VSshutdownNow()

  ·shutdown() :关闭线程池,线程池的状态变为 SHUTDOWN。线程池不再接受新任务了,但是队列里的任务得执行完毕。

  ·shutdownNow() :关闭线程池,线程的状态变为 STOP。线程池会终止当前正在运行的任务,并停止处理排队的任务并返回正在等待执行的 List。

4.isTerminated() VS isShutdown()

  ·isShutDown 当调用 shutdown() 方法后返回为 true。

  ·isTerminated 当调用 shutdown() 方法后,并且所有提交的任务完成后返回为 true

 

几个常见内置线程池:(有空补充)

1.FixedThreadPool

2.SingleThreadPool

3.CachedThreadPool

4.ScheduledThreadPool

 https://www.cnblogs.com/jinggod/p/8489169.html

 

 

 

 

 

 

 

 

 

 

 

 

 

 

posted @   壹索007  阅读(18)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
点击右上角即可分享
微信分享提示