• 1.6 线程池原理与实战


    1.6 线程池原理与实战

    Java线程的创建非常昂贵,需要JVM和OS(操作系统)配合完成大量的工作:

    (1)必须为线程堆栈分配和初始化大量内存块,其中包含至少1MB的栈内存。

    (2)需要进行系统调用,以便在OS(操作系统)中创建和注册本地线程。

    Java高并发应用频繁创建和销毁线程的操作是非常低效的,而且是不被编程规范所允许的。如何降低Java线程的创建成本?必须使用到线程池。线程池主要解决了以下两个问题:

    (1)提升性能:线程池能独立负责线程的创建、维护和分配。在执行大量异步任务时,可以不需要自己创建线程,而是将任务交给线程池去调度。线程池能尽可能使用空闲的线程去执行异步任务,最大限度地对已经创建的线程进行复用,使得性能提升明显。

    (2)线程管理:每个Java线程池会保持一些基本的线程统计信息,例如完成的任务数量、空闲时间等,以便对线程进行有效管理,使得能对所接收到的异步任务进行高效调度。

    说明

    在主要大厂的编程规范中,不允许在应用中自行显式地创建线程,线程必须通过线程池提供。由于创建和销毁线程需要时间以及系统资源开销,使用线程池的好处是减少这些开销,解决资源不足的问题。

    1.6.1 JUC的线程池架构

    在JUC中有关线程池的类与接口的架构图大致如图1-15所示。

    image-20220802170152616

    说明

    JUC就是java.util.concurrent工具包的简称,该工具包是从JDK 1.5开始加入JDK的,是用于完成高并发、处理多线程的一个工具包。

    1.Executor

    Executor是Java异步目标任务的“执行者”接口,其目标是执行目标任务。“执行者”Executor提供了execute()接口来执行已提交的Runnable执行目标实例。Executor作为执行者的角色,其目的是提供一种将“任务提交者”与“任务执行者”分离开来的机制。它只包含一个函数式方法:


         void execute(Runnable command)
    
    • 1

    2.ExecutorService

    ExecutorService继承于Executor。它是Java异步目标任务的“执行者服务接”口,对外提供异步任务的接收服务。ExecutorService提供了“接收异步任务并转交给执行者”的方法,如submit系列方法、invoke系列方法等,具体如下:


         //向线程池提交单个异步任务
          Future submit(Callable task);
         //向线程池提交批量异步任务
          List> invokeAll(Collection> tasks)
                 throws InterruptedException;
    
    • 1
    • 2
    • 3
    • 4
    • 5

    3.AbstractExecutorService

    AbstractExecutorService是一个抽象类,它实现了ExecutorService接口。AbstractExecutorService存在的目的是为ExecutorService中的接口提供默认实现。

    4.ThreadPoolExecutor

    ThreadPoolExecutor就是大名鼎鼎的“线程池”实现类,它继承于AbstractExecutorService抽象类。

    ThreadPoolExecutor是JUC线程池的核心实现类。线程的创建和终止需要很大的开销,线程池中预先提供了指定数量的可重用线程,所以使用线程池会节省系统资源,并且每个线程池都维护了一些基础的数据统计,方便线程的管理和监控。

    5.ScheduledExecutorService

    ScheduledExecutorService是一个接口,它继承于ExecutorService。它是一个可以完成“延时”和“周期性”任务的调度线程池接口,其功能和Timer/TimerTask类似。

    6.ScheduledThreadPoolExecutor

    ScheduledThreadPoolExecutor继承于ThreadPoolExecutor,它提供了ScheduledExecutorService线程池接口中“延时执行”和“周期执行”等抽象调度方法的具体实现。

    ScheduledThreadPoolExecutor类似于Timer,但是在高并发程序中,ScheduledThreadPoolExecutor的性能要优于Timer。

    7.Executors

    Executors是一个静态工厂类,它通过静态工厂方法返回ExecutorService、ScheduledExecutorService等线程池示例对象,这些静态工厂方法可以理解为一些快捷的创建线程池的方法。

    1.6.2 Executors的4种快捷创建线程池的方法

    Java通过Executors工厂类提供了4种快捷创建线程池的方法,具体如表1-1所示。

    表1-1 Executors工厂类提供的4种快捷创建线程池的方法

    image-20220803112405147

    1.newSingleThreadExecutor创建“单线程化线程池”

    该方法用于创建一个“单线程化线程池”,也就是只有一个线程的线程池,所创建的线程池用唯一的工作线程来执行任务,使用此方法创建的线程池能保证所有任务按照指定顺序(如FIFO)执行。

    (1)单线程化的线程池中的任务是按照提交的次序顺序执行的。

    (2)池中的唯一线程的存活时间是无限的。

    (3)当池中的唯一线程正繁忙时,新提交的任务实例会进入内部的阻塞队列中,并且其阻塞队列是无界的。

    总体来说,单线程化的线程池所适用的场景是:任务按照提交次序,一个任务一个任务地逐个执行的场景。

    以上用例在最后调用shutdown()方法来关闭线程池。执行shutdown()方法后,线程池状态变为SHUTDOWN,此时线程池将拒绝新任务,不能再往线程池中添加新任务,否则会抛出RejectedExecutionException异常。此时,线程池不会立刻退出,直到添加到线程池中的任务都已经处理完成才会退出。还有一个与shutdown()类似的方法,叫作shutdownNow(),执行shutdownNow()方法后,线程池状态会立刻变成STOP,并试图停止所有正在执行的线程,并且不再处理还在阻塞队列中等待的任务,会返回那些未执行的任务。

    2.newFixedThreadPool创建“固定数量的线程池”

    该方法用于创建一个“固定数量的线程池”,其唯一的参数用于设置池中线程的“固定数量”。

    “固定数量的线程池”的特点大致如下:

    (1)如果线程数没有达到“固定数量”,每次提交一个任务线程池内就创建一个新线程,直到线程达到线程池固定的数量。

    (2)线程池的大小一旦达到“固定数量”就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。

    (3)在接收异步任务的执行目标实例时,如果池中的所有线程均在繁忙状态,新任务会进入阻塞队列中(无界的阻塞队列)。

    “固定数量的线程池”的适用场景:需要任务长期执行的场景。“固定数量的线程池”的线程数能够比较稳定地保证一个数,能够避免频繁回收线程和创建线程,故适用于处理CPU密集型的任务,在CPU被工作线程长时间占用的情况下,能确保尽可能少地分配线程。

    “固定数量的线程池”的弊端:内部使用无界队列来存放排队任务,当大量任务超过线程池最大容量需要处理时,队列无限增大,使服务器资源迅速耗尽。

    3.newCachedThreadPool创建“可缓存线程池”

    该方法用于创建一个“可缓存线程池”,如果线程池内的某些线程无事可干成为空闲线程,“可缓存线程池”可灵活回收这些空闲线程。

    “可缓存线程池”的特点大致如下:

    (1)在接收新的异步任务target执行目标实例时,如果池内所有线程繁忙,此线程池就会添加新线程来处理任务。

    (2)此线程池不会对线程池大小进行限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。

    (3)如果部分线程空闲,也就是存量线程的数量超过了处理任务数量,就会回收空闲(60秒不执行任务)线程。

    “可缓存线程池”的适用场景:需要快速处理突发性强、耗时较短的任务场景,如Netty的NIO处理场景、REST API接口的瞬时削峰场景。“可缓存线程池”的线程数量不固定,只要有空闲线程就会被回收;接收到的新异步任务执行目标,查看是否有线程处于空闲状态,如果没有就直接创建新的线程。

    “可缓存线程池”的弊端:线程池没有最大线程数量限制,如果大量的异步任务执行目标实例同时提交,可能会因创建线程过多而导致资源耗尽。

    4.newScheduledThreadPool创建“可调度线程池”

    该方法用于创建一个“可调度线程池”,即一个提供“延时”和“周期性”任务调度功能的ScheduledExecutorService类型的线程池。Executors提供了多个创建“可调度线程池”的工厂方法,部分如下:

     //方法一:创建一个可调度线程池,池内仅含有一个线程
     public static ScheduledExecutorService newSingleThreadScheduledExecutor();
     
     //方法二:创建一个可调度线程池,池内含有N个线程,N的值为输入参数corePoolSize
     public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) ;
    
    • 1
    • 2
    • 3
    • 4
    • 5

    newSingleThreadScheduledExecutor工厂方法所创建的仅含有一个线程的可调度线程池适用于调度串行化任务,也就是一个任务一个任务地串行化调度执行。

    newScheduledThreadPool工厂方法可以创建一个执行“延时”和“周期性”任务的可调度线程池,所创建的线程池为ScheduleExecutorService类型的实例。ScheduleExecutorService接口中有多个重要的接收被调目标任务的方法,其中scheduleAtFixedRate和scheduleWithFixedDelay使用得比较多。

    ScheduleExecutorService中接收被调目标任务的方法之一scheduleAtFixedRate的定义如下:

     public ScheduledFuture scheduleAtFixedRate(
             Runnable command,      //异步任务target执行目标实例
             long initialDelay,     //首次执行延时
             long period,           //两次开始执行最小间隔时间
             TimeUnit unit                  //所设置的时间的计时单位,如TimeUnit.SECONDS常量
             );
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    ScheduleExecutorService中接收被调目标任务的方法之二scheduleWithFixedDelay的定义如下:

     public ScheduledFuture scheduleWithFixedDelay(
         Runnable command,  //异步任务target执行目标实例
         long initialDelay,         //首次执行延时
         long delay,          //前一次执行结束到下一次执行开始的间隔时间(间隔执行延迟时间)
         TimeUnit unit        //所设置的时间的计时单位,如TimeUnit.SECONDS常量
     );
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    当被调任务的执行时间大于指定的间隔时间时,ScheduleExecutorService并不会创建一个新的线程去并发执行这个任务,而是等待前一次调度执行完毕。

    “可调度线程池”的适用场景:周期性地执行任务的场景。Spring Boot中的任务调度器,底层借助了JUC的ScheduleExecutorService“可调度线程池”实现,并且可以通过@Configuration配置类型的Bean。

    以上为Executors中4个主要的快捷创建线程池的方法。为何JUC要提供工厂方法呢?原因是使用ThreadPoolExecutor、ScheduledThreadPoolExecutor构造器创建普通线程池、可调度线程池比较复杂,这些构造器会涉及大量的复杂参数。尽管Executors的工厂方法使用方便,但是在生产场景中被很多企业(尤其是大厂)的开发规范所禁用。

    1.6.3 线程池的标准创建方式

    大部分企业的开发规范都会禁止使用快捷线程池(具体原因稍后介绍),要求通过标准构造器ThreadPoolExecutor去构造工作线程池。Executors工厂类中创建线程池的快捷工厂方法实际上是调用ThreadPoolExecutor(定时任务使用ScheduledThreadPoolExecutor)线程池的构造方法完成的。ThreadPoolExecutor构造方法有多个重载版本,其中一个比较重要的构造器如下:


         // 使用标准构造器构造一个普通的线程池
         public ThreadPoolExecutor(
           int corePoolSize,            // 核心线程数,即使线程空闲(Idle),也不会回收
           int maximumPoolSize,                 // 线程数的上限
           long keepAliveTime, TimeUnit unit,   // 线程最大空闲(Idle)时长 
           BlockingQueue workQueue,     // 任务的排队队列
           ThreadFactory threadFactory,                         // 新线程的产生方式
           RejectedExecutionHandler handler)    // 拒绝策略
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    很无奈,构造一个线程池竟然有7个参数,但是确实需要这么多参数。接下来对这些参数进行具体介绍。

    1.核心和最大线程数量

    参数corePoolSize用于设置核心(Core)线程池数量,参数maximumPoolSize用于设置最大线程数量。线程池执行器将会根据corePoolSize和maximumPoolSize自动维护线程池中的工作线程,大致规则为:

    (1)当在线程池接收到新任务,并且当前工作线程数少于corePoolSize时,即使其他工作线程处于空闲状态,也会创建一个新线程来处理该请求,直到线程数达到corePoolSize。

    (2)如果当前工作线程数多于corePoolSize数量,但小于maximumPoolSize数量,那么仅当任务排队队列已满时才会创建新线程。通过设置corePoolSize和maximumPoolSize相同,可以创建一个固定大小的线程池。

    (3)当maximumPoolSize被设置为无界值(如Integer.MAX_VALUE)时,线程池可以接收任意数量的并发任务。

    (4)corePoolSize和maximumPoolSize不仅能在线程池构造时设置,也可以调用setCorePoolSize()和setMaximumPoolSize()两个方法进行动态更改。

    2.BlockingQueue

    BlockingQueue(阻塞队列)的实例用于暂时接收到的异步任务,如果线程池的核心线程都在忙,那么所接收到的目标任务缓存在阻塞队列中。

    3.keepAliveTime

    线程构造器的keepAliveTime(空闲线程存活时间)参数用于设置池内线程最大Idle(空闲)时长(或者说保活时长),如果超过这个时间,默认情况下Idle、非Core线程会被回收。

    如果池在使用过程中提交任务的频率变高,也可以调用方法setKeepAliveTime(long,TimeUnit)进行线程存活时间的动态调整,可以将时长延长。如果需要防止Idle线程被终止,可以将Idle时间设置为无限大,具体如下:


         setKeepAliveTime(Long.MAX_VALUE,TimeUnit.NANOSECONDS);
    
    • 1

    默认情况下,Idle超时策略仅适用于存在超过corePoolSize线程的情况。但若调用了allowCoreThreadTimeOut(boolean)方法,并且传入了参数true,则keepAliveTime参数所设置的Idle超时策略也将被应用于核心线程。

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

    向线程池提交任务的两种方式大致如下:

    方式一:调用execute()方法,例如:


         //Executor 接口中的方法
         void execute(Runnable command);
    
    • 1
    • 2

    方式二:调用submit()方法,例如:


         //ExecutorService 接口中的方法
          Future submit(Callable task); 
          Future submit(Runnable task, T result);
         Future submit(Runnable task);
    
    • 1
    • 2
    • 3
    • 4

    以上的submit()和execute()两类方法的区别在哪里呢?大致有以下三点:

    (1)二者所接收的参数不一样

    Execute()方法只能接收Runnable类型的参数,而submit()方法可以接收Callable、Runnable两种类型的参数。Callable类型的任务是可以返回执行结果的,而Runnable类型的任务不可以返回执行结果。

    Callable是JDK 1.5加入的执行目标接口,作为Runnable的一种补充,允许有返回值,允许抛出异常。Runnable和Callable的主要区别为:Callable允许有返回值,Runnable不允许有返回值;Runnable不允许抛出异常,Callable允许抛出异常。

    (2)submit()提交任务后会有返回值,而execute()没有

    execute()方法主要用于启动任务的执行,而任务的执行结果和可能的异常调用者并不关心。submit()方法也用于启动任务的执行,但是启动之后会返回Future对象,代表一个异步执行实例,可以通过该异步执行实例去获取结果。

    (3)submit()方便Exception处理

    execute()方法在启动任务执行后,任务执行过程中可能发生的异常调用者并不关心。而通过submit()方法返回的Future对象(异步执行实例),可以进行异步执行过程中的异常捕获。

    1.通过submit()返回的Future对象获取结果

    submit()方法自身并不会传递结果,而是返回一个Future异步执行实例,处理过程的结果被包装到Future实例中,调用者可以通过Future.get()方法获取异步执行的结果。


    2.通过submit()返回的Future对象捕获异常

    submit()方法自身并不会传递异常,处理过程中的异常都被包装到Future实例中,调用者在调用Future.get()方法获取执行结果时,可以捕获异步执行过程中抛出的受检异常和运行时异常,并进行对应的业务处理。

    在ThreadPoolExecutor类的实现中,内部核心的任务提交方法是execute()方法,虽然用户程序通过submit()也可以提交任务,但是实际上submit()方法中最终调用的还是execute()方法。

    1.6.5 线程池的任务调度流程

    线程池的任务调度流程(包含接收新任务和执行下一个任务)大致如下:

    (1)如果当前工作线程数量小于核心线程数量,执行器总是优先创建一个任务线程,而不是从线程队列中获取一个空闲线程。

    (2)如果线程池中总的任务数量大于核心线程池数量,新接收的任务将被加入阻塞队列中,一直到阻塞队列已满。在核心线程池数量已经用完、阻塞队列没有满的场景下,线程池不会为新任务创建一个新线程。

    (3)当完成一个任务的执行时,执行器总是优先从阻塞队列中获取下一个任务,并开始执行,一直到阻塞队列为空,其中所有的缓存任务被取光。

    (4)在核心线程池数量已经用完、阻塞队列也已经满了的场景下,如果线程池接收到新的任务,将会为新任务创建一个线程(非核心线程),并且立即开始执行新任务。

    (5)在核心线程都用完、阻塞队列已满的情况下,一直会创建新线程去执行新任务,直到池内的线程总数超出maximumPoolSize。如果线程池的线程总数超过maximumPoolSize,线程池就会拒绝接收任务,当新任务过来时,会为新任务执行拒绝策略。

    总体的线程池的任务调度流程如图1-16所示。

    image-20220803140345247

    在创建线程池时,如果线程池的参数(如核心线程数量、最大线程数量、BlockingQueue等)配置得不合理,就会出现任务不能被正常调度的问题。

    (1)核心和最大线程数量、BlockingQueue队列等参数如果配置得不合理,可能会造成异步任务得不到预期的并发执行,造成严重的排队等待现象。

    (2)线程池的调度器创建线程的一条重要的规则是:在corePoolSize已满之后,还需要等阻塞队列已满,才会去创建新的线程。

    1.6.6 ThreadFactory(线程工厂)

    ThreadFactory是Java线程工厂接口,这是一个非常简单的接口,具体如下:


         package java.util.concurrent;
         public interface ThreadFactory {
             //唯一的方法:创建一个新线程
             Thread newThread(Runnable target);
         }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    在调用ThreadFactory的唯一方法newThread()创建新线程时,可以更改所创建的新线程的名称、线程组、优先级、守护进程状态等。如果newThread()的返回值为null,表示线程工厂未能成功创建线程,线程池可能无法执行任何任务。

    使用Executors创建新的线程池时,也可以基于ThreadFactory(线程工厂)创建,在创建新线程池时可以指定将要使用的ThreadFactory实例。只不过,如果没有指定的话,就会使用Executors.defaultThreadFactory默认实例。使用默认的线程工厂实例所创建的线程全部位于同一个ThreadGroup(线程组)中,具有相同的NORM_PRIORITY(优先级为5),而且都是非守护进程状态。

    说明

    这里提到了两个工厂类,比较容易混淆,故做出说明。Executors为线程池工厂类,用于快捷创建线程池(Thread Pool);ThreadFactory为线程工厂类,用于创建线程(Thread)。

    基于自定义的ThreadFactory实例创建线程池,首先需要实现一个ThreadFactory类,实现其唯一的抽象方法newThread(Runnable)。

    1.6.7 任务阻塞队列

    Java中的阻塞队列(BlockingQueue)与普通队列相比有一个重要的特点:在阻塞队列为空时会阻塞当前线程的元素获取操作。具体来说,在一个线程从一个空的阻塞队列中获取元素时线程会被阻塞,直到阻塞队列中有了元素;当队列中有元素后,被阻塞的线程会自动被唤醒(唤醒过程不需要用户程序干预)。

    Java线程池使用BlockingQueue实例暂时接收到的异步任务,BlockingQueue是JUC包的一个超级接口,比较常用的实现类有:

    (1)ArrayBlockingQueue:是一个数组实现的有界阻塞队列(有界队列),队列中的元素按FIFO排序。ArrayBlockingQueue在创建时必须设置大小,接收的任务超出corePoolSize数量时,任务被缓存到该阻塞队列中,任务缓存的数量只能为创建时设置的大小,若该阻塞队列已满,则会为新的任务创建线程,直到线程池中的线程总数大于maximumPoolSize。

    (2)LinkedBlockingQueue:是一个基于链表实现的阻塞队列,按FIFO排序任务,可以设置容量(有界队列),不设置容量则默认使用Integer.Max_VALUE作为容量(无界队列)。该队列的吞吐量高于ArrayBlockingQueue。

    如果不设置LinkedBlockingQueue的容量(无界队列),当接收的任务数量超出corePoolSize时,则新任务可以被无限制地缓存到该阻塞队列中,直到资源耗尽。有两个快捷创建线程池的工厂方法Executors.newSingleThreadExecutor和Executors.newFixedThreadPool使用了这个队列,并且都没有设置容量(无界队列)。

    (3)PriorityBlockingQueue:是具有优先级的无界队列。

    (4)DelayQueue:这是一个无界阻塞延迟队列,底层基于PriorityBlockingQueue实现,队列中每个元素都有过期时间,当从队列获取元素(元素出队)时,只有已经过期的元素才会出队,队列头部的元素是过期最快的元素。快捷工厂方法Executors.newScheduledThreadPool所创建的线程池使用此队列。

    (5)SynchronousQueue:(同步队列)是一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程的调用移除操作,否则插入操作一直处于阻塞状态,其吞吐量通常高于LinkedBlockingQueue。快捷工厂方法Executors.newCachedThreadPool所创建的线程池使用此队列。与前面的队列相比,这个队列比较特殊,它不会保存提交的任务,而是直接新建一个线程来执行新来的任务。

    1.6.8 调度器的钩子方法

    ThreadPoolExecutor线程池调度器为每个任务执行前后都提供了钩子方法。ThreadPoolExecutor类提供了三个钩子方法(空方法),这三个钩子方法一般用作被子类重写,具体如下:


         //任务执行之前的钩子方法(前钩子)
         protected void beforeExecute(Thread t, Runnable r)   { }
         //任务执行之后的钩子方法(后钩子)
         protected void afterExecute(Runnable r, Throwable t) { }
         //线程池终止时的钩子方法(停止钩子)
         protected void terminated() { }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    (1)beforeExecute:异步任务执行之前的钩子方法

    线程池工作线程在异步执行目标实例(如Runnable实例)前调用此钩子方法。此方法仍然由执行任务的工作线程调用。默认实现不执行任何操作,但可以在子类中对其进行自定义。

    此方法由执行目标实例的工作线程调用,可用于重新初始化ThreadLocal线程本地变量实例、更新日志记录、开始计时统计、更新上下文变量等。

    (2)afterExecute:异步任务执行之后的钩子方法

    线程池工作线程在异步执行目标实例后调用此钩子方法。此方法仍然由执行任务的工作线程调用。此钩子方法的默认实现不执行任何操作,可以在调度器子类中对其进行自定义。

    此方法由执行目标实例的工作线程调用,可用于清除ThreadLocal线程本地变量、更新日志记录、收集统计信息、更新上下文变量等。

    (3)terminated:线程池终止时的钩子方法

    terminated钩子方法在Executor终止时调用,默认实现不执行任何操作。

    说明

    beforeExecute和afterExecute两个方法在每个任务执行前后被调用,如果钩子(回调方法)引发异常,内部工作线程可能失败并突然终止。

    为线程池定制钩子方法的示例,具体代码如下:


         package com.crazymakercircle.multithread.basic.create3;
         // 省略import
         public class CreateThreadPoolDemo
         {
             @org.junit.Test
             public void testHooks()
             {
                 ExecutorService pool = new ThreadPoolExecutor(2, //coreSize
                             4, //最大线程数
                            60,//空闲保活时长
                         TimeUnit.SECONDS, 
                            new LinkedBlockingQueue<>(2)) //等待队列
                 {
                  //继承:调度器终止钩子
                     @Override
                     protected void terminated() 
                     {
                         Print.tco("调度器已经终止!");
                     }
         
                    //继承:执行前钩子
                    @Override
                     protected void beforeExecute(Thread t, Runnable target)
                     {
                         Print.tco( target +"前钩被执行");
                         //记录开始执行时间
                         startTime.set(System.currentTimeMillis());
                                              super.beforeExecute(t, target);
                     }
         
                    //继承:执行后钩子
                     @Override
                     protected void afterExecute(Runnable target, Throwable t)
                     {
                         super.afterExecute(target, t);
                         //计算执行时长
                         long time = (System.currentTimeMillis() - startTime.get()) ;
                         Print.tco( target + " 后钩被执行, 任务执行时长(ms):" + time);
                         //清空本地变量
                         startTime.remove();
                     }
                 };
         
                 for (int i = 1; i <= 5; i++)
                 {
                     pool.execute(new TargetTask());
                 }
                 //等待10秒
                 sleepSeconds(10);
                 Print.tco("关闭线程池");
                 pool.shutdown();
             }
             // 省略其他
         }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54

    运行以上示例代码,输出的结果如下:


    [pool-1-thread-3]:TargetTask{task-5}前钩被执行
    [pool-1-thread-1]:TargetTask{task-1}前钩被执行
    [pool-1-thread-2]:TargetTask{task-2}前钩被执行
    [pool-1-thread-2]:任务:task-2 doing
    [pool-1-thread-1]:任务:task-1 doing
    [pool-1-thread-3]:任务:task-5 doing
    [pool-1-thread-3]:task-5 运行结束.
    [pool-1-thread-2]:task-2 运行结束.
    ...
    [pool-1-thread-3]:TargetTask{task-4} 后钩被执行, 任务执行时长(ms):515
    [main]:关闭线程池
    [pool-1-thread-3]:调度器已经终止!
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    示例代码在beforeExecute(前钩子)方法中通过startTime线程局部变量暂存了异步目标任务(如Runnable实例)的开始执行时间(起始时间),在afterExecute(后钩子)方法中通过startTime线程局部变量获取了之前暂存的起始时间,然后计算与系统当前时间(结束时间)之间的时间差,从而得出异步目标任务的执行时长。

    .6.9 线程池的拒绝策略

    在线程池的任务缓存队列为有界队列(有容量限制的队列)的时候,如果队列满了,提交任务到线程池的时候就会被拒绝。总体来说,任务被拒绝有两种情况:

    (1)线程池已经被关闭。

    (2)工作队列已满且maximumPoolSize已满。

    无论以上哪种情况任务被拒绝,线程池都会调用RejectedExecutionHandler实例的rejectedExecution方法。RejectedExecutionHandler是拒绝策略的接口,JUC为该接口提供了以下几种实现:

    ·AbortPolicy:拒绝策略。

    ·DiscardPolicy:抛弃策略。

    ·DiscardOldestPolicy:抛弃最老任务策略。

    ·CallerRunsPolicy:调用者执行策略。

    ·自定义策略。

    JUC线程池拒绝策略的接口与类之间的关系图如图1-17所示。

    image-20220804140410838

    (1)AbortPolicy

    使用该策略时,如果线程池队列满了,新任务就会被拒绝,并且抛出RejectedExecutionException异常。该策略是线程池默认的拒绝策略。

    (2)DiscardPolicy

    该策略是AbortPolicy的Silent(安静)版本,如果线程池队列满了,新任务就会直接被丢掉,并且不会有任何异常抛出。

    (3)DiscardOldestPolicy

    抛弃最老任务策略,也就是说如果队列满了,就会将最早进入队列的任务抛弃,从队列中腾出空间,再尝试加入队列。因为队列是队尾进队头出,队头元素是最老的,所以每次都是移除队头元素后再尝试入队。

    (4)CallerRunsPolicy

    调用者执行策略。在新任务被添加到线程池时,如果添加失败,那么提交任务线程会自己去执行该任务,不会使用线程池中的线程去执行新任务。

    在以上4种内置策略中,线程池默认的拒绝策略为AbortPolicy,如果提交的任务被拒绝,线程池就会抛出RejectedExecutionException异常,该异常是非受检异常(运行时异常),很容易忘记捕获。如果关心任务被拒绝的事件,需要在提交任务时捕获RejectedExecutionException异常。

    (5)自定义策略

    如果以上拒绝策略都不符合需求,那么可自定义一个拒绝策略,实现RejectedExecutionHandler接口的rejectedExecution方法即可。

    下面给出一个自定义拒绝策略的例子,代码如下:


         package com.crazymakercircle.multithread.basic.create3;
         // 省略import
         public class CreateThreadPoolDemo
         {
             //一个简单的线程工厂
             static public class SimpleThreadFactory implements ThreadFactory
             {
             //为了节约篇幅,省略重复内容
         }
         
             //自定义拒绝策略
             public static class CustomIgnorePolicy
                              implements RejectedExecutionHandler
             {
                 public void rejectedExecution(Runnable r, ThreadPoolExecutor e)
                 {
                     // 可做日志记录等
                     Print.tco(r + " rejected; " +
                              " - getTaskCount: " + e.getTaskCount());
                 }
             }
         
             @org.junit.Test
             public void testCustomIgnorePolicy()
             {
                 int corePoolSize = 2;          //核心线程数
                 int maximumPoolSize = 4;       //最大线程数
                 long keepAliveTime = 10;
                 TimeUnit unit = TimeUnit.SECONDS;
                 //最大排队任务数
                 BlockingQueue workQueue = new ArrayBlockingQueue<>(2);
                 //线程工厂
                 ThreadFactory threadFactory = new SimpleThreadFactory();
                 //拒绝和异常处理策略
                 RejectedExecutionHandler policy = new CustomIgnorePolicy();
                 ThreadPoolExecutor pool = new ThreadPoolExecutor(
                         corePoolSize,
                         maximumPoolSize,
                         keepAliveTime, unit,
                         workQueue,
                         threadFactory,
                         policy);
         
                 // 预启动所有核心线程
                 pool.prestartAllCoreThreads();
                 for (int i = 1; i <= 10; i++)
                 {
                     pool.execute(new TargetTask());
                 }
                 //等待10秒
                 sleepSeconds(10);
                 Print.tco("关闭线程池");
                 pool.shutdown();
             }
             // 省略其他
         }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56

    运行以上代码,大致结果如下:

         [main]:创建一个线程,名称为:simpleThread-1
         [main]:创建一个线程,名称为:simpleThread-2
         [main]:创建一个线程,名称为:simpleThread-3
         [simpleThread-1]:任务:task-1 doing
         [simpleThread-2]:任务:task-2 doing
         [main]:创建一个线程,名称为:simpleThread-4
         [simpleThread-3]:任务:task-3 doing
         [simpleThread-4]:任务:task-6 doing
         [main]:TargetTask{task-7} rejected;  - getTaskCount: 6
         [main]:TargetTask{task-8} rejected;  - getTaskCount: 6
         [main]:TargetTask{task-9} rejected;  - getTaskCount: 6
         [main]:TargetTask{task-10} rejected;  - getTaskCount: 6
         [simpleThread-1]:task-1 运行结束.
         [simpleThread-2]:task-2 运行结束.
         [simpleThread-1]:任务:task-4 doing
         [simpleThread-2]:任务:task-5 doing
         [simpleThread-2]:task-5 运行结束.
         [simpleThread-4]:task-6 运行结束.
         [simpleThread-3]:task-3 运行结束.
         [simpleThread-1]:task-4 运行结束.
         [main]:关闭线程池
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    1.6.10 线程池的优雅关闭

    一般情况下,线程池启动后建议手动关闭。在介绍线程池的优雅关闭之前,我们先了解一下线程池的状态。线程池总共存在5种状态,定义在ThreadPoolExecutor类中,具体代码如下:


         package java.util.concurrent;
         // 省略import
         public class ThreadPoolExecutor extends AbstractExecutorService {
              // runState is stored in the high-order bits
             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;
             // 省略其他
         }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    线程池的5种状态具体如下:

    (1)RUNNING:线程池创建之后的初始状态,这种状态下可以执行任务。

    (2)SHUTDOWN:该状态下线程池不再接受新任务,但是会将工作队列中的任务执行完毕。

    (3)STOP:该状态下线程池不再接受新任务,也不会处理工作队列中的剩余任务,并且将会中断所有工作线程。

    (4)TIDYING:该状态下所有任务都已终止或者处理完成,将会执行terminated()钩子方法。

    (5)TERMINATED:执行完terminated()钩子方法之后的状态。

    线程池的状态转换规则为:

    (1)线程池创建之后状态为RUNNING。

    (2)执行线程池的shutdown()实例方法,会使线程池状态从RUNNING转变为SHUTDOWN。

    (3)执行线程池的shutdownNow()实例方法,会使线程池状态从RUNNING转变为STOP。

    (4)当线程池处于SHUTDOWN状态时,执行其shutdownNow()方法会将其状态转变为STOP。

    (5)等待线程池的所有工作线程停止,工作队列清空之后,线程池状态会从STOP转变为TIDYING。

    (6)执行完terminated()钩子方法之后,线程池状态从TIDYING转变为TERMINATED。

    线程池的状态转换规则如图1-18所示。

    优雅地关闭线程池主要涉及的方法有3个:

    (1)shutdown:是JUC提供的一个有序关闭线程池的方法,此方法会等待当前工作队列中的剩余任务全部执行完成之后,才会执行关闭,但是此方法被调用之后线程池的状态转为SHUTDOWN,线程池不会再接收新的任务。

    image-20220804140741358

    (2)shutdownNow:是JUC提供的一个立即关闭线程池的方法,此方法会打断正在执行的工作线程,并且会清空当前工作队列中的剩余任务,返回的是尚未执行的任务。

    (3)awaitTermination:等待线程池完成关闭。在调用线程池的shutdown()与shutdownNow()方法时,当前线程会立即返回,不会一直等待直到线程池完成关闭。如果需要等到线程池关闭完成,可以调用awaitTermination()方法。

    1.shutdown()方法的原理

    shutdown()方法的源码大致如下:


            public void shutdown()
             {
                 final ReentrantLock mainLock = this.mainLock;
                 mainLock.lock();
                 try
                 {
                     // 检查权限
                     checkShutdownAccess();
                     // 设置线程池状态
                     advanceRunState(SHUTDOWN);
                     // 中断空闲线程
                     interruptIdleWorkers();
                     // 钩子函数,主要用于清理一些资源
                     onShutdown();
                 } finally
                 {
                     mainLock.unlock();
                 }
                 tryTerminate();
             }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    Shutdown()方法首先加锁,其次检查调用者是否用于执行线程池关闭的Java Security权限。接着shutdown()方法会将线程池状态变为SHUTDOWN,在这之后线程池不再接受提交的新任务。此时如果还继续往线程池提交任务,将会使用线程池拒绝策略响应,默认的拒绝策略将会使用ThreadPoolExecutor.AbortPolicy,接收新任务时会抛出RejectedExecutionException异常。

    2.shutdownNow()方法的原理

    shutdownNow()方法的源码大致如下:

       public List<Runnable> shutdownNow()
       {
             List<Runnable> tasks;
             final ReentrantLock mainLock = this.mainLock;
             mainLock.lock();
             try
             {
                 // 检查状态
                 checkShutdownAccess();
                 // 将线程池状态变为 STOP
                 advanceRunState(STOP);
                 // 中断所有线程,包括工作线程以及空闲线程
                 interruptWorkers();
                 // 丢弃工作队列中的剩余任务
                 tasks = drainQueue();
             } finally
             {
                 mainLock.unlock();
             }
             tryTerminate();
             return tasks;
       }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    shutdownNow()方法将会把线程池状态设置为STOP,然后中断所有线程(包括工作线程以及空闲线程),最后清空工作队列,取出工作队列所有未完成的任务返回给调用者。与有序的shutdown()方法相比,shutdownNow()方法比较粗暴,直接中断工作线程。不过这里需要注意的是,中断线程并不代表线程立刻结束,只是通过工作线程的interrupt()实例方法设置了中断状态,这里需要用户程序主动配合线程进行中断操作。

    3.awaitTermination()方法的使用

    调用了线程池的shutdown()与shutdownNow()方法之后,用户程序都不会主动等待线程池关闭完成,如果需要等待线程池关闭完成,需要调用awaitTermination()进行主动等待。调用方法大致如下:

     threadPool.shutdown();
     try {
         //一直等待,直到线程池完成关闭
         while (!threadPool.awaitTermination(60,TimeUnit.SECONDS)){
             System.out.println("线程池任务还未执行结束");
         }
     } catch (InterruptedException e) {
         e.printStackTrace();
     }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    如果线程池完成关闭,awaitTermination()方法将会返回true,否则当等待时间超过指定时间后将会返回false。如果需要调用awaitTermination(),建议不是永久等待,而是设置一定重试次数。下面的代码参考了阿里巴巴著名的分布式框架Dubbo框架中线程池关闭源码中的部分代码:

           if(!threadPool.isTerminated())
         {
             try
             {
                 for (int i = 0; i < 1000; i++) //循环关闭1000次,每次等待10毫秒
                 {
                     if (threadPool.awaitTermination(10, TimeUnit.MILLISECONDS))
                     {
                         break;
                     }
                     threadPool.shutdownNow();
                 }
             } catch (InterruptedException e)
             {
                 System.err.println(e.getMessage());
             } catch (Throwable e)
             {
                 System.err.println(e.getMessage());
             }
         }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    4.优雅地关闭线程池

    大家可以结合shutdown()、shutdownNow()、awaitTermination()三个方法优雅地关闭一个线程池,大致分为以下几步:

    (1)执行shutdown()方法,拒绝新任务的提交,并等待所有任务有序地执行完毕。

    (2)执行awaitTermination(long timeout,TimeUnit unit)方法,指定超时时间,判断是否已经关闭所有任务,线程池关闭完成。

    (3)如果awaitTermination()方法返回false,或者被中断,就调用shutDownNow()方法立即关闭线程池所有任务。

    (4)补充执行awaitTermination(long timeout,TimeUnit unit)方法,判断线程池是否关闭完成。如果超时,就可以进入循环关闭,循环一定的次数(如1000次),不断关闭线程池,直到其关闭或者循环结束。

    5.注册JVM钩子函数自动关闭线程池

    如果使用了线程池,可以在JVM中注册一个钩子函数,在JVM进程关闭之前,由钩子函数自动将线程池优雅地关闭,以确保资源正常释放。

    下面的例子使用JVM钩子函数关闭了一个定义在随书源码的ThreadUtil辅助类中用于执行定时、顺序任务的线程池,具体代码如下:


         package com.crazymakercircle.util;
         // 省略import
         public class ThreadUtil
         {
         
             //懒汉式单例创建线程池:用于执行定时、顺序任务
             static class SeqOrScheduledTargetThreadPoolLazyHolder
             {
                 //线程池:用于定时任务、顺序排队执行任务
                 static final ScheduledThreadPoolExecutor EXECUTOR =
                                 new ScheduledThreadPoolExecutor( 1,
                                 new CustomThreadFactory("seq"));
         
                 static
                 {
                     //注册JVM关闭时的钩子函数
                     Runtime.getRuntime().addShutdownHook(
                                          new ShutdownHookThread("定时和顺序任务线程池",
                                          new Callable<Void>()
                             {
                                 @Override
                                 public Void call() throws Exception
                                 {
                                     //优雅地关闭线程池
                                     shutdownThreadPoolGracefully(EXECUTOR);
                                     return null;
                                 }
                             }));
                 }
             }
             // 省略不相干代码
         }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    1.6.11 Executors快捷创建线程池的潜在问题

    在很多公司(如阿里、华为等)的编程规范中,非常明确地禁止使用Executors快捷创建线程池,为什么呢?这里从源码讲起,介绍使用Executors工厂方法快捷创建线程池将会面临的潜在问题。

    1.使用Executors创建“固定数量的线程池”的潜在问题

    使用newFixedThreadPool工厂方法创建“固定数量的线程池”的源码如下:


             public static ExecutorService newFixedThreadPool(int nThreads)
             {
                 return new ThreadPoolExecutor(
                         nThreads,                                              // 核心线程数
                         nThreads,                                              // 最大线程数
                         0L,                                                    // 线程最大空闲(Idle)时长
                         TimeUnit.MILLISECONDS,         // 时间单位:毫秒
                         new LinkedBlockingQueue()      //任务的排队队列,无界队列
                 );
             }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    newFixedThreadPool工厂方法返回一个ThreadPoolExecutor实例,该线程池实例的corePoolSize数量为参数nThread,其maximumPoolSize数量也为参数nThread,其workQueue属性的值为LinkedBlockingQueue()无界阻塞队列。

    使用Executors创建“固定数量的线程池”的潜在问题主要存在于其workQueue上,其值为LinkedBlockingQueue(无界阻塞队列)。如果任务提交速度持续大于任务处理速度,就会造成队列中大量的任务等待。如果队列很大,很有可能导致JVM出现OOM(Out Of Memory)异常,即内存资源耗尽。

    2.使用Executors创建“单线程化线程池”的潜在问题

    使用newSingleThreadExecutor工厂方法创建“单线程化线程池”的源码如下:


             public static ExecutorService newSingleThreadExecutor()
             {
                 return new FinalizableDelegatedExecutorService
                         (new ThreadPoolExecutor(
                                 1,                                                     // 核心线程数
                                 1,                                                     // 最大线程数
                                 0L,                                                    // 线程最大空闲(Idle)时长
                                 TimeUnit.MILLISECONDS,         //时间单位:毫秒
                                 new LinkedBlockingQueue()      //无界队列
                         ));
             }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    以上代码首先通过调用工厂方法newFixedThreadPool(1)创建一个数量为1的“固定大小的线程池”,然后使用FinalizableDelegatedExecutorService对该“固定大小的线程池”进行包装,这一层包装的作用是防止线程池的corePoolSize被动态地修改。

    .使用Executors创建“可缓存线程池”的潜在问题

    使用newCachedThreadPool工厂方法创建“可缓存线程池”的源码如下:


             public static ExecutorService newCachedThreadPool()
             {
                 return new ThreadPoolExecutor(
                         0,                                                             // 核心线程数
                         Integer.MAX_VALUE,                             // 最大线程数
                         60L,                                                           // 线程最大空闲(Idle)时长
                         TimeUnit.MILLISECONDS,                         // 时间单位:毫秒
                         new SynchronousQueue() // 任务的排队队列,无界队列
                 );
             }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    以上代码通过调用ThreadPoolExecutor标准构造器创建一个核心线程数为0、最大线程数不设限制的线程池。所以,理论上“可缓存线程池”可以拥有无数个工作线程,即线程数量几乎无限制。“可缓存线程池”的workQueue为SynchronousQueue同步队列,这个队列类似于一个接力棒,入队出队必须同时传递,正因为“可缓存线程池”可以无限制地创建线程,不会有任务等待,所以才使用SynchronousQueue。

    当“可缓存线程池”有新任务到来时,新任务会被插入SynchronousQueue实例中,由于SynchronousQueue是同步队列,因此会在池中寻找可用线程来执行,若有可用线程则执行,若没有可用线程,则线程池会创建一个线程来执行该任务。

    SynchronousQueue是一个比较特殊的阻塞队列实现类,SynchronousQueue没有容量,每一个插入操作都要等待对应的删除操作,反之每个删除操作都要等待对应的插入操作。也就是说,如果使用SynchronousQueue,提交的任务不会被真实地保存,而是将新任务交给空闲线程执行,如果没有空闲线程,就创建线程,如果线程数都已经大于最大线程数,就执行拒绝策略。使用这种队列需要将maximumPoolSize设置得非常大,从而使得新任务不会被拒绝。

    使用Executors创建的“可缓存线程池”的潜在问题存在于其最大线程数量不设限上。由于其maximumPoolSize的值为Integer.MAX_VALUE(非常大),可以认为可以无限创建线程,如果任务提交较多,就会造成大量的线程被启动,很有可能造成OOM异常,甚至导致CPU线程资源耗尽。

    4.使用Executors创建“可调度线程池”的潜在问题

    使用newScheduledThreadPool工厂方法创建“可调度线程池”的源码如下:

         public static ScheduledExecutorService newScheduledThreadPool(
                                         int corePoolSize)
         {
             return new ScheduledThreadPoolExecutor(corePoolSize);
         }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    Executors的newScheduledThreadPool工厂方法调用了ScheduledThreadPoolExecutor实现类的构造器,而ScheduledThreadPoolExecutor继承了ThreadPoolExecutor的普通线程池类,在其构造器内部进一步调用了该父类的构造器,具体的代码如下:

       public ScheduledThreadPoolExecutor(int corePoolSize)
         {
             super(corePoolSize,                    // 核心线程数
                     Integer.MAX_VALUE,     // 最大线程数
                     0,                                     // 线程最大空闲(Idle)时长
                     NANOSECONDS,//时间单位
                     new DelayedWorkQueue()  //任务的排队队列
             );
         }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    以上代码创建了一个ThreadPoolExecutor实例,其corePoolSize为传递来的参数,maximumPoolSize为Integer.MAX_VALUE,表示线程数不设上限,其workQueue为一个DelayedWorkQueue实例,这是一个按到期时间升序排序的阻塞队列。

    使用Executors创建的“可缓存线程池”的潜在问题存在于其最大线程数量不设限上。由于其线程数量不设限,如果到期任务太多,就会导致CPU的线程资源耗尽。

    以上内容分别梳理了Executors四个工厂方法所创建的线程池将面临的潜在问题。总结起来,使用Executors创建线程池主要的弊端如下:

    (1)FixedThreadPool和SingleThreadPool

    这两个工厂方法所创建的线程池,工作队列(任务排队的队列)的长度都为Integer.MAX_VALUE,可能会堆积大量的任务,从而导致OOM(即耗尽内存资源)。

    (2)CachedThreadPool和ScheduledThreadPool

    这两个工厂方法所创建的线程池允许创建的线程数量为Integer.MAX_VALUE,可能会导致创建大量的线程,从而导致OOM。

    虽然Executors工厂类提供了构造线程池的便捷方法,但是对于服务器程序而言,大家应该杜绝使用这些便捷方法,而是直接使用线程池ThreadPoolExecutor的构造器,从而有效避免由于使用无界队列可能导致的内存资源耗尽,或者由于对线程个数不做限制而导致的CPU资源耗尽等问题。

                     Integer.MAX_VALUE,     // 最大线程数
                     0,                                     // 线程最大空闲(Idle)时长
                     NANOSECONDS,//时间单位
                     new DelayedWorkQueue()  //任务的排队队列
             );
         }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    以上代码创建了一个ThreadPoolExecutor实例,其corePoolSize为传递来的参数,maximumPoolSize为Integer.MAX_VALUE,表示线程数不设上限,其workQueue为一个DelayedWorkQueue实例,这是一个按到期时间升序排序的阻塞队列。

    使用Executors创建的“可缓存线程池”的潜在问题存在于其最大线程数量不设限上。由于其线程数量不设限,如果到期任务太多,就会导致CPU的线程资源耗尽。

    以上内容分别梳理了Executors四个工厂方法所创建的线程池将面临的潜在问题。总结起来,使用Executors创建线程池主要的弊端如下:

    (1)FixedThreadPool和SingleThreadPool

    这两个工厂方法所创建的线程池,工作队列(任务排队的队列)的长度都为Integer.MAX_VALUE,可能会堆积大量的任务,从而导致OOM(即耗尽内存资源)。

    (2)CachedThreadPool和ScheduledThreadPool

    这两个工厂方法所创建的线程池允许创建的线程数量为Integer.MAX_VALUE,可能会导致创建大量的线程,从而导致OOM。

    虽然Executors工厂类提供了构造线程池的便捷方法,但是对于服务器程序而言,大家应该杜绝使用这些便捷方法,而是直接使用线程池ThreadPoolExecutor的构造器,从而有效避免由于使用无界队列可能导致的内存资源耗尽,或者由于对线程个数不做限制而导致的CPU资源耗尽等问题。

    所以,大厂的编程规范都不允许使用Executors创建线程池,而是要求使用标准构造器ThreadPoolExecutor创建线程池。

  • 相关阅读:
    Springboot 多模块(A依赖B)集成mybatis , mybatis.mapper-locations 配置多个mapper路径配置
    latex生成的pdf上传到ieee express检查格式 fail
    第十章 单调栈 part02 503.下一个更大元素II 42. 接雨水
    EfficientNet:通过模型效率彻底改变深度学习
    Ax=y,Ax=0以及非线性方程组的最小二乘解
    Python3操作MongoDb7最新版创建文档及CRUD基本操作
    Linux环境安装一:jdk的安装及将jar包项目发布在服务器上
    windbg-windows调试工具来抓端游crash dump
    c语言中的字符函数
    周四见|物流人的一周资讯
  • 原文地址:https://blog.csdn.net/KongZhongNiao/article/details/126159189