ThreadPoolExecutor

xiaoxiao2021-02-28  37

原文地址:http://www.tianshouzhi.com/api/tutorials/mutithread/310

ThreadPoolExecutor 类是JDK提供的ExecutorService接口的默认实现。提供一个可扩展的线程池实现。ExecutorService在Executor接口的基础上,添加了一些可以管理Executor自身生命周期以及任务的生命周期的方法,每个 ThreadPoolExecutor 还维护着一些基本的统计数据,如完成的任务数。 

ThreadPoolExecutor构造方法

构造方法 说明 ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue)  用给定的初始参数和默认的线程工厂及被拒绝的执行处理程序创建新的 ThreadPoolExecutor。  ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, RejectedExecutionHandler handler)   用给定的初始参数和默认的线程工厂创建新的 ThreadPoolExecutor。  ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory)  用给定的初始参数和默认被拒绝的执行处理程序创建新的 ThreadPoolExecutor。  ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)  用给定的初始参数创建新的 ThreadPoolExecutor。 

参数说明:

corePoolSize和maximumPoolSize大小 

    ThreadPoolExecutor 将根据 corePoolSize和 maximumPoolSize设置的边界自动调整线程池大小。当新任务在方法 execute(java.lang.Runnable) 中提交时,如果运行的线程少于 corePoolSize,则创建新线程来处理请求,即使其他辅助线程是空闲的。如果运行的线程多于 corePoolSize 而少于 maximumPoolSize,则仅当队列满时才创建新线程。如果设置的 corePoolSize 和 maximumPoolSize 相同,则创建了固定大小的线程池。如果将 maximumPoolSize 设置为基本的无界值(如 Integer.MAX_VALUE),则允许池适应任意数量的并发任务。在大多数情况下,核心和最大池大小仅基于构造来设置,不过也可以使用 setCorePoolSize(int) 和 setMaximumPoolSize(int) 进行动态更改。 

keepAliveTime与timeUnit

如果池中当前有多于 corePoolSize 的线程,则这些多出的线程在空闲时间超过 keepAliveTime 时将会终止。这提供了当池处于非活动状态时减少资源消耗的方法。如果池后来变得更为活动,则可以创建新的线程。也可以使用方法 setKeepAliveTime(long, java.util.concurrent.TimeUnit) 动态地更改此参数。使用 Long.MAX_VALUE TimeUnit.NANOSECONDS 的值在关闭前有效地从以前的终止状态禁用空闲线程。默认情况下,保持活动策略只在有多于 corePoolSizeThreads 的线程时应用。但是只要 keepAliveTime 值非 0,allowCoreThreadTimeOut(boolean) 方法也可将此超时策略应用于核心线程。 

workQueue

所有 BlockingQueue 都可用于传输和保持提交的任务。可以使用此队列与池大小进行交互: 

如果运行的线程少于 corePoolSize,则 Executor 始终首选添加新的线程,而不进行排队。 

如果运行的线程等于或多于 corePoolSize,则 Executor 始终首选将请求加入队列,而不添加新的线程。 

如果无法将请求加入队列,则创建新的线程,除非创建此线程超出 maximumPoolSize,在这种情况下,任务将被拒绝。 

排队有三种通用策略: 

    1、直接提交。工作队列的默认选项是 SynchronousQueue,它将任务直接提交给线程而不保持它们。在此,如果不存在可用于立即运行任务的线程,则试图把任务加入队列将失败,因此会构造一个新的线程。此策略可以避免在处理可能具有内部依赖性的请求集时出现锁。直接提交通常要求无界 maximumPoolSizes 以避免拒绝新提交的任务。当命令以超过队列所能处理的平均数连续到达时,此策略允许无界线程具有增长的可能性。 

    2、无界队列。使用无界队列(例如,不具有预定义容量的 LinkedBlockingQueue)将导致在所有 corePoolSize 线程都忙时新任务在队列中等待。这样,创建的线程就不会超过 corePoolSize。(因此,maximumPoolSize 的值也就无效了。)当每个任务完全独立于其他任务,即任务执行互不影响时,适合于使用无界队列;例如,在 Web 页服务器中。这种排队可用于处理瞬态突发请求,当命令以超过队列所能处理的平均数连续到达时,此策略允许无界线程具有增长的可能性。 

    3、有界队列。当使用有限的 maximumPoolSizes 时,有界队列(如 ArrayBlockingQueue)有助于防止资源耗尽,但是可能较难调整和控制。队列大小和最大池大小可能需要相互折衷:使用大型队列和小型池可以最大限度地降低 CPU 使用率、操作系统资源和上下文切换开销,但是可能导致人工降低吞吐量。如果任务频繁阻塞(例如,如果它们是 I/O 边界),则系统可能为超过您许可的更多线程安排时间。使用小型队列通常要求较大的池大小,CPU 使用率较高,但是可能遇到不可接受的调度开销,这样也会降低吞吐量。

threadFactory

使用 ThreadFactory 创建新线程。如果没有另外说明,则在同一个 ThreadGroup 中一律使用 Executors.defaultThreadFactory() 创建线程,并且这些线程具有相同的 NORM_PRIORITY 优先级和非守护进程状态。通过提供不同的 ThreadFactory,可以改变线程的名称、线程组、优先级、守护进程状态,等等。如果从 newThread 返回 null 时 ThreadFactory 未能创建线程,则执行程序将继续运行,但不能执行任何任务。

RejectedExecutionHandler

当 Executor 已经关闭,并且 Executor 将有限边界用于最大线程和工作队列容量,且已经饱和时,在方法 execute(java.lang.Runnable) 中提交的新任务将被拒绝。在以上两种情况下,execute 方法都将调用其 RejectedExecutionHandler 的 RejectedExecutionHandler.rejectedExecution(java.lang.Runnable, java.util.concurrent.ThreadPoolExecutor) 方法。下面提供了四种预定义的处理程序策略: 

在默认的 ThreadPoolExecutor.AbortPolicy 中,处理程序遭到拒绝将抛出运行时 RejectedExecutionException。 

在 ThreadPoolExecutor.CallerRunsPolicy 中,线程调用运行该任务的 execute 本身。此策略提供简单的反馈控制机制,能够减缓新任务的提交速度。 

在 ThreadPoolExecutor.DiscardPolicy 中,不能执行的任务将被删除。 

在 ThreadPoolExecutor.DiscardOldestPolicy 中,如果执行程序尚未关闭,则位于工作队列头部的任务将被删除,然后重试执行程序(如果再次失败,则重复此过程)。 

定义和使用其他种类的 RejectedExecutionHandler 类也是可能的,但这样做需要非常小心,尤其是当策略仅用于特定容量或排队策略时。 

以下通过代码案例,用于演示ThreadPoolExecutor的各个特性

public class ThreadPoolExecutorTest {    //测试corePoolSize和MaximumPoolSize随着任务提交量的变化,以及keepAliveTime与TimeUnit    @Test    public void threadPoolExecutorTest() throws InterruptedException {        int corePoolSize = 2;        int maximumPoolSize = 5;        int keepAliveTime = 5;        TimeUnit seconds = TimeUnit.SECONDS;        BlockingQueue workQueue = new SynchronousQueue();        int taskCount = 5;        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(corePoolSize, maximumPoolSize,                         keepAliveTime, seconds, workQueue);        doTest(keepAliveTime, taskCount, threadPoolExecutor);    }     private void doTest(int keepAliveTime, int taskCount, ThreadPoolExecutor threadPoolExecutor)     throws InterruptedException {        threadPoolExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardOldestPolicy());        System.out.println("-------threadPoolExecutor刚刚创建----------");        printPoolSize(threadPoolExecutor);         for (int i = 1; i <= taskCount; i++) {            threadPoolExecutor.execute(new Task(threadPoolExecutor,i));            System.out.print("--------已提交任务"+ i +"个任务--------");            printPoolSize(threadPoolExecutor);        }         //等到所有的任务都执行完        TimeUnit.SECONDS.sleep(11);//休眠10秒        System.out.println("---------所有的任务都执行完--------");        printPoolSize(threadPoolExecutor);         //此时maximumPoolSize>corePoolSize,当前时间再休眠keepAliveTime时间,测试多出corePoolSize的线程是否能自动销毁        System.out.println("---------休眠keepAliveTime,测试maximumPoolSize>corePoolSize的部分能否自动回收--------");        TimeUnit.SECONDS.sleep(keepAliveTime);        printPoolSize(threadPoolExecutor);    }     private void printPoolSize(ThreadPoolExecutor threadPoolExecutor){        int corePoolSize = threadPoolExecutor.getCorePoolSize();        int maximumPoolSize = threadPoolExecutor.getMaximumPoolSize();        int poolSize = threadPoolExecutor.getPoolSize();        System.out.println("核心线程池大小:"+corePoolSize+",最大线程池大小:"+maximumPoolSize+",当前线程池大小:"+poolSize);    }  class Task implements Runnable{     private ThreadPoolExecutor threadPoolExecutor;     private int taskId;      public Task(ThreadPoolExecutor threadPoolExecutor,final int taskId) {         this.threadPoolExecutor = threadPoolExecutor;         this.taskId = taskId;     }      @Override     public void run() {         try {             TimeUnit.SECONDS.sleep(10);//休眠10秒             System.out.print("第"+taskId+"个任务执行完:");             printPoolSize(threadPoolExecutor);         } catch (InterruptedException e) {             e.printStackTrace();         }     } }}

 代码输出:

-------threadPoolExecutor刚刚创建----------

核心线程池大小:2,最大线程池大小:5,当前线程池大小:0

--------已提交任务1个任务--------核心线程池大小:2,最大线程池大小:5,当前线程池大小:1

--------已提交任务2个任务--------核心线程池大小:2,最大线程池大小:5,当前线程池大小:2

--------已提交任务3个任务--------核心线程池大小:2,最大线程池大小:5,当前线程池大小:3

--------已提交任务4个任务--------核心线程池大小:2,最大线程池大小:5,当前线程池大小:4

--------已提交任务5个任务--------核心线程池大小:2,最大线程池大小:5,当前线程池大小:5

第1个任务执行完:核心线程池大小:2,最大线程池大小:5,当前线程池大小:5

第2个任务执行完:核心线程池大小:2,最大线程池大小:5,当前线程池大小:5

第3个任务执行完:核心线程池大小:2,最大线程池大小:5,当前线程池大小:5

第4个任务执行完:核心线程池大小:2,最大线程池大小:5,当前线程池大小:5

第5个任务执行完:核心线程池大小:2,最大线程池大小:5,当前线程池大小:5

---------所有的任务都执行完--------

核心线程池大小:2,最大线程池大小:5,当前线程池大小:5

---------休眠keepAliveTime,测试maximumPoolSize>corePoolSize的部分能否自动回收--------

核心线程池大小:2,最大线程池大小:5,当前线程池大小:2

说明:

corePoolSize与maxPoolSize

上述测试代码说明,executor刚刚创建的时候,是不会初始化所有线程的。只有等到有任务进来的时候,才会创建线程对象。

在这个案例中,每次提交一个任务,创建一个线程。当达到maxPoolSize的时候,线程池不会继续增大。

keepAliveTime与timeUnit

当所有任务都执行完成的时候,maxPoolSize为5,而因为corePoolSize为2,所以过了keepAliveTime之后,会自动缩小corePoolSize。

需要注意的是,在按个案例中,线程池达到corePoolSize之后,立刻继续增长,这是因为我们使用的是SynchronousQueue的原因。这个队列的特定点,只要一有任务进来,就立马要找一个空闲的线程来运行这个任务,如果没有空闲的线程,就会抛出异常。

立即提交的情况下,taskCount>maxPoolSize的情况

读者可以尝试将改大当前提交的任务数量,例如将taskCount改为6:

@Testpublic void threadPoolExecutorTest() throws InterruptedException {    ...    int taskCount = 6;//将taskCount改为6,超出maxPoolSize设置的5    ...}

再次运行单元测试,就会看到类似以下的异常:

java.util.concurrent.RejectedExecutionException: Task com.tianshouzhi.thread.executors.ThreadPoolExecutorTest$Task@a294a6 rejected from java.util.concurrent.ThreadPoolExecutor@b37bc[Running, pool size = 5, active threads = 5, queued tasks = 0, completed tasks = 0]

at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2047)

有界队列的情况下,taskCount>maxPoolSize+taskQueueSize的情况

而如果将taskCount设置为10,将BlockingQueue设置为一个有界的LinkedBlockingDeque,例如

@Testpublic void threadPoolExecutorTest() throws InterruptedException {    ...    int taskCount = 10;//将taskCount改为10,超出maxPoolSize设置的5    BlockingQueue workQueue = new LinkedBlockingDeque<>(4);//队列的大小设置为4    ...}

我们会看到类似以下的输出:

-------threadPoolExecutor刚刚创建----------

核心线程池大小:2,最大线程池大小:5,当前线程池大小:0

--------已提交任务1个任务--------核心线程池大小:2,最大线程池大小:5,当前线程池大小:1

--------已提交任务2个任务--------核心线程池大小:2,最大线程池大小:5,当前线程池大小:2

--------已提交任务3个任务--------核心线程池大小:2,最大线程池大小:5,当前线程池大小:2

--------已提交任务4个任务--------核心线程池大小:2,最大线程池大小:5,当前线程池大小:2

--------已提交任务5个任务--------核心线程池大小:2,最大线程池大小:5,当前线程池大小:2

--------已提交任务6个任务--------核心线程池大小:2,最大线程池大小:5,当前线程池大小:2

--------已提交任务7个任务--------核心线程池大小:2,最大线程池大小:5,当前线程池大小:3

--------已提交任务8个任务--------核心线程池大小:2,最大线程池大小:5,当前线程池大小:4

--------已提交任务9个任务--------核心线程池大小:2,最大线程池大小:5,当前线程池大小:5

java.util.concurrent.RejectedExecutionException: Task com.tianshouzhi.thread.executors.ThreadPoolExecutorTest$Task@533e64 rejected from java.util.concurrent.ThreadPoolExecutor@a294a6[Running, pool size = 5, active threads = 5, queued tasks = 4, completed tasks = 0]

at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2047)

在这里,可以看到,线程池大小增加到2之后,连续4次提交的任务,都没有继续扩大。一直到第7次提交任务的时候,线程池才开始增大,这是因为,使用了有界LinkedBlockingDeque(这里设置为4)的时候,默认情况下,线程池增大到corePoolSize(这里设置的是2)之后,就不再继续变大。

只有等到LinkedBlockingDeque已经全部填满,并且超出的时候,才会继续增加线程,而到9就次提交任务的时候,线程池已经达到了maxPoolSize为5。此时线程池不能继续增大。当继续往线程池中,添加任务时,就抛出了异常。

这意味,在默认的AbortPolicy策略下,使用有界任务队列的时候,线程池可以同时执行的最大任务数量为:maxPoolSize+queueSize,而在我们的案例中,这个值为maxPoolSize(5)+queueSize(4)=9,所以超出这个限制的时候抛出了异常。

无界队列的情况下

如果构建 LinkedBlockingDeque的时候,没有指定大小,即使用了无界的任务队列

BlockingQueue workQueue = new LinkedBlockingDeque<>();

那么输出可能如下所示:

-------threadPoolExecutor刚刚创建----------

核心线程池大小:2,最大线程池大小:5,当前线程池大小:0

--------已提交任务1个任务--------核心线程池大小:2,最大线程池大小:5,当前线程池大小:1

--------已提交任务2个任务--------核心线程池大小:2,最大线程池大小:5,当前线程池大小:2

--------已提交任务3个任务--------核心线程池大小:2,最大线程池大小:5,当前线程池大小:2

--------已提交任务4个任务--------核心线程池大小:2,最大线程池大小:5,当前线程池大小:2

--------已提交任务5个任务--------核心线程池大小:2,最大线程池大小:5,当前线程池大小:2

--------已提交任务6个任务--------核心线程池大小:2,最大线程池大小:5,当前线程池大小:2

--------已提交任务7个任务--------核心线程池大小:2,最大线程池大小:5,当前线程池大小:2

--------已提交任务8个任务--------核心线程池大小:2,最大线程池大小:5,当前线程池大小:2

--------已提交任务9个任务--------核心线程池大小:2,最大线程池大小:5,当前线程池大小:2

--------已提交任务10个任务--------核心线程池大小:2,最大线程池大小:5,当前线程池大小:2

某个任务执行完:核心线程池大小:2,最大线程池大小:5,当前线程池大小:2

某个任务执行完:核心线程池大小:2,最大线程池大小:5,当前线程池大小:2

---------所有的任务都执行完--------

核心线程池大小:2,最大线程池大小:5,当前线程池大小:2

---------休眠keepAliveTime,测试maximumPoolSize>corePoolSize的部分能否自动回收--------

核心线程池大小:2,最大线程池大小:5,当前线程池大小:2

可以发现当前线程池的大小一直是corePoolSize:2,maxPoolSize失效了。

上述案例中,提交的任务超出maxPoolSize+queueSize的时候,会抛出异常。我们可以使用预定的RejectedExecutionHandler策略来处理这个异常

需要注意的是,只有是在直接提交,和有界队列的情况下,这种异常处理策略才是有效的,对于无界队列,永远不会抛出这个异常。

ThreadFactory演示

线程池中的线程默认都是根据ThreadFactory创建,如果在构建ThreadPoolExecutor的时候,没有指定ThreadFactory,默认就会使用 Executors.defaultThreadFactory()获取ThreadFactory实例。ThreadFactory只定义了一个抽象方法,用于返回新的线程:

public interface ThreadFactory {    Thread newThread(Runnable r);    }

我们可以完全可以创建一个自己的ThreadFactory,以下实现参考了Executors.defaultThreadFactory()。

public class NamedThreadFactory implements ThreadFactory {    private static AtomicInteger poolId;    private static ThreadGroup threadGroup;    private AtomicInteger threadId;    private static String threadNamePrefix="NamedThreadPool";     public NamedThreadFactory() {        poolId=new AtomicInteger();        threadGroup=new ThreadGroup("NamedThreadFactory");        threadId=new AtomicInteger();    }     @Override    public Thread newThread(Runnable r) {        String name = threadNamePrefix + "-pool-" + poolId.getAndIncrement() + "-thread-" + threadId;        Thread t=new Thread(threadGroup,name);        return t;    }}

然后在我们的测试代码中,可以在进行测试之前,传入这个对象实例

@Testpublic void threadPoolExecutorTest() throws InterruptedException {    ...    ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(corePoolSize, maximumPoolSize,                 keepAliveTime, seconds, workQueue);    threadPoolExecutor.setThreadFactory(new NamedThreadFactory());//设置线程工厂    doTest(keepAliveTime, taskCount, threadPoolExecutor);    }

在使用ThreadPoolExecutor的情况下,笔者是建议编写自己的ThreadFactory的,这样我们使用jstack工具查看内存中的线程的时候,就能很容易的看出来那些线程是属于这个线程池的,这对于我们在解决一些问题,是非常有用的(这是笔者解决一个生产环境的死锁问题后的总结)。

转载请注明原文地址: https://www.6miu.com/read-46416.html

最新回复(0)