Java线程池ThreadPoolExecutor类

一、简述

《阿里巴巴java开发手册》指出线程资源必须通过线程池提供,不允许在应用中自行显示的创建线程,一方面使得线程的创建更加规范,可以合理控制开辟线程的数量;另一方面线程的细节管理交给线程池处理,优化了资源的开销。
线程池不允许使用 Executors 去创建,而要通过 ThreadPoolExecutor 方式,这一方面是由于 jdk 中 Executor 框架虽然提供了如 newFixedThreadPool()、newSingleThreadExecutor()、newCachedThreadPool() 等创建线程池的方法,但都有其局限性,不够灵活;另外由于前面几种方法内部也是通过 ThreadPoolExecutor 方式实现,使用 ThreadPoolExecutor 有助于明确线程池的运行规则,创建符合自己业务场景需要的线程池,避免资源耗尽的风险。ThreadPoolExecutor 的构造函数:

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.acc = System.getSecurityManager() == null ?
                null : AccessController.getContext();
    this.corePoolSize = corePoolSize;
    this.maximumPoolSize = maximumPoolSize;
    this.workQueue = workQueue;
    this.keepAliveTime = unit.toNanos(keepAliveTime);
    this.threadFactory = threadFactory;
    this.handler = handler;
}

源码中对每个参数的解释:

参数含义如下:
1️⃣corePoolSize:指定了线程池中的线程数量,它的数量决定了添加的任务是开辟新的线程去执行,还是放到 workQueue 任务队列中去。
2️⃣maximumPoolSize:指定了线程池中的最大线程数量,这个参数会根据使用的 workQueue 任务队列的类型,决定线程池会开辟的最大线程数量。
3️⃣keepAliveTime:当线程池中空闲线程数量超过 corePoolSize 时,多余的线程会在多长时间内被销毁。
4️⃣unit:keepAliveTime 的单位。
5️⃣workQueue:任务队列,被添加到线程池中,但尚未被执行的任务;它一般分为直接提交队列、有界任务队列、无界任务队列、优先任务队列。
6️⃣threadFactory:线程工厂,用于创建线程,一般用默认即可。
7️⃣handler:拒绝策略;当任务太多来不及处理时,如何拒绝任务。

线程池执行任务逻辑和线程池参数的关系:

二、corePoolSize 线程池核心线程大小

核心线程数由开发者指定,在启动线程池,并没有往里面添加任务时,池子里面的线程数量为 0,当达到核心线程数时,无论是否向池子里提交任务,池子里至少会保持核心线程数数量。
核心线程数,即线程池维护的最小线程数量,即使这些线程处于空闲状态,也不会被销毁,除非设置了 allowCoreThreadTimeOut。如图:创建了一个核心线程数为 2 的线程池,打印一下线程池信息,可以看到,线程池里面的核心线程数为 0:

接下来,向线程池里添加 5 个任务,当所有任务都执行完之后,线程池里面存活的线程数为 2(核心线程数),并没有变为 0。

三、maximumPoolSize 线程池最大线程数量

最大线程数参数,是在已经达到核心线程池参数,并且任务队列已经满的情况下,才去判断该参数。
该参数定义了一个线程池中最多能容纳多少个线程。当一个任务提交到线程池中时,如果线程数量达到了核心线程数,并且任务队列已满,不能再向任务队列中添加任务时,这时会检查任务是否达到了最大线程数,如果未达到,则创建新线程,执行任务,否则,执行拒绝策略。

源码如下:可以看出,当调用 submit(Runnable task),将任务提交到线程池中时,会调用 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);
}

四、keepAliveTime 空闲线程存活时间

线程池中大于核心线程数 corePoolSize 的那部分线程如果处于空闲状态,在指定时间 keepAliveTime 后,该空闲线程会被销毁。即执行完任务之后,多余(maximumPoolSize-corePoolSize)线程在线程池中存活的时间。

五、unit 空闲线程存活时间单位

keepAliveTime 的计量单位

六、workQueue 任务队列直接提交队列、有界任务队列、无界任务队列、优先任务队列

该参数主要是在核心线程数的线程都在执行任务时,才起作用的参数。用来缓存任务的队列。等待线程去执行。
用来做缓冲作用,当核心线程数量 corePoolSize 的线程都在执行任务,不够提交到线程池中的任务用时,会将任务存到任务队列中,等待核心线程来执行。jdk 中提供了四种工作队列:

1️⃣【直接提交队列】SynchronousQuene

一个不缓存任务的阻塞队列,生产者放入一个任务必须等到消费者取出这个任务。也就是说新任务进来时,不会缓存,而是直接被调度执行该任务,如果没有可用线程,则创建新线程。如果线程数量达到 maximumPoolSize,则执行拒绝策略。

SynchronousQueue 是一个特殊的 BlockingQueue,它没有容量,每执行一个插入操作就会阻塞,需要再执行一个删除操作才会被唤醒,反之每一个删除操作也都要等待对应的插入操作。

public class ThreadPool {
    private static ExecutorService pool;
    public static void main(String[] args)  {
        //maximumPoolSize设置为2 ,拒绝策略为AbortPolic策略,直接抛出异常
        pool = new ThreadPoolExecutor(1, 2, 1000, TimeUnit.MILLISECONDS, 
                               new SynchronousQueue<Runnable>(),
                               Executors.defaultThreadFactory(),
                               new ThreadPoolExecutor.AbortPolicy());
        for(int i=0;i<3;i++) {
            pool.execute(new ThreadTask());
        }   
    }
}
public class ThreadTask implements Runnable{
    public ThreadTask() {}  
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }
}

输出结果为:

pool-1-thread-1
pool-1-thread-2
Exception in thread "main" java.util.concurrent.RejectedExecutionException: 
Task com.hhxx.test.ThreadTask@55f96302 
rejected from java.util.concurrent.ThreadPoolExecutor@3d4eac69
[Running, pool size = 2, active threads = 0, queued tasks = 0, completed tasks = 2]
    at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(Unknown Source)
    at java.util.concurrent.ThreadPoolExecutor.reject(Unknown Source)
    at java.util.concurrent.ThreadPoolExecutor.execute(Unknown Source)
    at com.hhxx.test.ThreadPool.main(ThreadPool.java:17)

可以看到,当任务队列为 SynchronousQueue,创建的线程数大于 maximumPoolSize 时,直接执行了拒绝策略抛出异常。

使用 SynchronousQueue,提交的任务不会被保存,总是会马上提交执行。如果用于执行任务的线程数量小于 maximumPoolSize,则尝试创建新的进程,如果达到 maximumPoolSize 设置的最大值,则根据设置的 handler 执行拒绝策略。因此这种方式提交的任务不会被缓存起来,而是会被马上执行,在这种情况下,需要对程序的并发量有个准确的评估,才能设置合适的 maximumPoolSize 数量,否则很容易就会执行拒绝策略。

2️⃣【有界任务队列】ArrayBlockingQueue

基于数组的有界阻塞队列,可以防止资源耗尽问题,按 FIFO 排序。当线程池中线程数量达到 corePoolSize 后,再有新任务进来,则会将任务放入该队列的队尾,等待被调度。如果队列已经是满的,则创建一个新线程,如果线程数量已经达到 maximumPoolSize,则会执行拒绝策略。如下:

pool = new ThreadPoolExecutor(1, 2, 1000, TimeUnit.MILLISECONDS, 
                              new ArrayBlockingQueue<Runnable>(10), 
                              Executors.defaultThreadFactory(),
                              new ThreadPoolExecutor.AbortPolicy());

使用 ArrayBlockingQueue 有界任务队列,线程数量的上限与该任务队列的状态有直接关系。如果有界队列初始容量较大或者没有达到超负荷的状态,线程数将一直维持在 corePoolSize 以下,反之当任务队列已满时,则会以 maximumPoolSize 为最大线程数上限。

3️⃣【无界阻塞队列】LinkedBlockingQuene

基于链表的无界阻塞队列(其实最大容量为Interger.MAX),按照 FIFO 排序。由于该队列的近似无界性,当线程池中线程数量达到 corePoolSize 后,再有新任务进来,会一直存入该队列,而不会去创建新线程直到 maximumPoolSize,因此使用该工作队列时,参数 maximumPoolSize 其实是不起作用的。

pool = new ThreadPoolExecutor(1, 2, 1000, TimeUnit.MILLISECONDS, 
                              new LinkedBlockingQueue<Runnable>(),
                              Executors.defaultThreadFactory(),
                              new ThreadPoolExecutor.AbortPolicy());

使用无界任务队列,线程池的任务队列可以无限制的添加新的任务,而线程池创建的最大线程数量就是 corePoolSize 设置的数量,也就是说在这种情况下 maximumPoolSize 这个参数是无效的,哪怕任务队列中缓存了很多未执行的任务,当线程池的线程数达到 corePoolSize 后,就不会再增加了;若后续有新的任务加入,则直接进入队列等待,当使用这种任务队列模式时,一定要注意任务提交与处理之间的协调与控制,不然会出现队列中的任务由于无法及时处理导致一直增长,直到最后资源耗尽的问题。

4️⃣【优先任务队列】PriorityBlockingQueue

具有优先级的无界阻塞队列,优先级通过参数Comparator实现。

public class ThreadPool {
    private static ExecutorService pool;
    public static void main(String[] args) {
        //优先任务队列
        pool = new ThreadPoolExecutor(1, 2, 1000, TimeUnit.MILLISECONDS, 
                                      new PriorityBlockingQueue<Runnable>(),
                                      Executors.defaultThreadFactory(),
                                      new ThreadPoolExecutor.AbortPolicy());
        for(int i=0;i<20;i++) {
            pool.execute(new ThreadTask(i));
        }    
    }
}
public class ThreadTask implements Runnable,Comparable<ThreadTask>{
    private int priority;
    public int getPriority() {
        return priority;
    }
    public void setPriority(int priority) {
        this.priority = priority;
    }
    public ThreadTask() {}
    public ThreadTask(int priority) {
        this.priority = priority;
    }
//当前对象和其他对象做比较,当前优先级大就返回-1,优先级小就返回1,值越小优先级越高
    public int compareTo(ThreadTask o) {
         return  this.priority>o.priority?-1:1;
    }    
    public void run() {
        try {
            //让线程阻塞,使后续任务进入缓存队列
            Thread.sleep(1000);
System.out.println("priority:"+this.priority+",ThreadName:"+Thread.currentThread().getName());
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
}

执行结果:

priority:0,ThreadName:pool-1-thread-1
priority:9,ThreadName:pool-1-thread-1
priority:8,ThreadName:pool-1-thread-1
priority:7,ThreadName:pool-1-thread-1
priority:6,ThreadName:pool-1-thread-1
priority:5,ThreadName:pool-1-thread-1
priority:4,ThreadName:pool-1-thread-1
priority:3,ThreadName:pool-1-thread-1
priority:2,ThreadName:pool-1-thread-1
priority:1,ThreadName:pool-1-thread-1

可以看到除了第一个任务直接创建线程执行外,其他的任务都被放入了优先任务队列,按优先级进行了重新排列执行,且线程池的线程数一直为 corePoolSize,也就是只有一个。

通过运行的代码可以看出 PriorityBlockingQueue 它其实是一个特殊的无界队列,它其中无论添加了多少个任务,线程池创建的线程数也不会超过 corePoolSize 的数量,只不过其他队列一般是按照先进先出的规则处理任务,而 PriorityBlockingQueue 队列可以自定义规则根据任务的优先级顺序先后执行。

七、ThreadFactory 线程工厂

线程池中线程就是通过 ThreadPoolExecutor 中的 ThreadFactory 线程工厂创建的。通过自定义 ThreadFactory,可以按需要对线程池中创建的线程进行一些特殊的设置,如命名、优先级、是否为 daemon 线程等等,下面代码通过 ThreadFactory 对线程池中创建的线程进行记录与命名:

public class ThreadPool {
    private static ExecutorService pool;
    public static void main( String[] args )  {
        //自定义线程工厂
        pool = new ThreadPoolExecutor(2, 4, 1000, 
                                     TimeUnit.MILLISECONDS, 
                                     new ArrayBlockingQueue<Runnable>(5),
                                     new ThreadFactory() {
            public Thread newThread(Runnable r) {
                System.out.println("线程"+r.hashCode()+"创建");
                //线程命名
                Thread th = new Thread(r,"threadPool"+r.hashCode());
                return th;
            }
        }, new ThreadPoolExecutor.CallerRunsPolicy());
        for(int i=0;i<10;i++) {
            pool.execute(new ThreadTask());
        }    
    }
}
public class ThreadTask implements Runnable{    
    public void run() {
        //输出执行线程的名称
        System.out.println("ThreadName:"+Thread.currentThread().getName());
    }
}

输出结果:

线程118352462创建
线程1550089733创建
线程865113938创建
ThreadName:threadPool1550089733
ThreadName:threadPool118352462
线程1442407170创建
ThreadName:threadPool1550089733
ThreadName:threadPool1550089733
ThreadName:threadPool1550089733
ThreadName:threadPool865113938
ThreadName:threadPool865113938
ThreadName:threadPool118352462
ThreadName:threadPool1550089733
ThreadName:threadPool1442407170

可以看到线程池中,每个线程的创建都进行了记录输出与命名。

八、handler 拒绝策略

一般创建线程池时,为防止资源被耗尽,任务队列都会选择创建有界任务队列,但这种模式下如果出现任务队列已满且线程池创建的线程数达到设置的最大线程数时,这时就需要指定 ThreadPoolExecutor 的 RejectedExecutionHandler 参数即合理的拒绝策略,来处理线程池“超载”的情况。jdk 中 ThreadPoolExecutor 自带的四种拒绝策略如下:

1️⃣【CallerRunsPolicy】如果线程池的线程数量达到上限,该策略会在调用者线程中直接执行被拒绝任务的 run 方法,除非线程池已经 shutdown,则直接抛弃任务。

2️⃣【AbortPolicy】该策略直接抛出RejectedExecutionException,阻止系统正常工作。

3️⃣【DiscardPolicy】该策略下,直接丢弃无法处理的任务,什么都不做。使用此策略,业务场景需允许任务的丢失。

4️⃣【DiscardOldestPolicy】该策略下,抛弃进入队列最早的那个任务,然后尝试把这次拒绝的任务放入队列。

以上内置的策略均实现了 RejectedExecutionHandler,当然也可以自己扩展 RejectedExecutionHandler,定义自己的拒绝策略,看下示例代码:

public class ThreadPool {
    private static ExecutorService pool;
    public static void main(String[] args) {
        //自定义拒绝策略
        pool = new ThreadPoolExecutor(1, 2, 1000, TimeUnit.MILLISECONDS, 
                                      new ArrayBlockingQueue<Runnable>(5),
                                      Executors.defaultThreadFactory(), 
                                      new RejectedExecutionHandler() {
            public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
                System.out.println(r.toString()+"执行了拒绝策略");
                
            }
        });
        for(int i=0;i<10;i++) {
            pool.execute(new ThreadTask());
        }    
    }
}
public class ThreadTask implements Runnable{    
    public void run() {
        try {
            //让线程阻塞,使后续任务进入缓存队列
            Thread.sleep(1000);
            System.out.println("ThreadName:"+Thread.currentThread().getName());
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
}

输出结果:

com.hhxx.test.ThreadTask@33909752执行了拒绝策略
com.hhxx.test.ThreadTask@55f96302执行了拒绝策略
com.hhxx.test.ThreadTask@3d4eac69执行了拒绝策略
ThreadName:pool-1-thread-2
ThreadName:pool-1-thread-1
ThreadName:pool-1-thread-1
ThreadName:pool-1-thread-2
ThreadName:pool-1-thread-1
ThreadName:pool-1-thread-2
ThreadName:pool-1-thread-1

可以看到由于任务加了休眠阻塞,执行需要花费一定时间,导致会有一定的任务被丢弃,从而执行自定义的拒绝策略。

九、ThreadPoolExecutor扩展

ThreadPoolExecutor 扩展主要是围绕 beforeExecute()、afterExecute() 和 terminated() 三个接口实现的:
1️⃣beforeExecute:线程池中任务运行前执行
2️⃣afterExecute:线程池中任务运行完毕后执行
3️⃣terminated:线程池退出后执行

通过这三个接口可以监控每个任务的开始和结束时间,或者其他一些功能。下面可以通过代码实现一下:

public class ThreadPool {
    private static ExecutorService pool;
    public static void main( String[] args ) throws InterruptedException
    {
        //实现自定义接口
        pool = new ThreadPoolExecutor(2, 4, 1000, 
                                      TimeUnit.MILLISECONDS, 
                                      new ArrayBlockingQueue<Runnable>(5),
                                      new ThreadFactory() {
            public Thread newThread(Runnable r) {
                System.out.println("线程"+r.hashCode()+"创建");
                //线程命名
                Thread th = new Thread(r,"threadPool"+r.hashCode());
                return th;
            }
        }, new ThreadPoolExecutor.CallerRunsPolicy()) {
    
            protected void beforeExecute(Thread t,Runnable r) {
                System.out.println("准备执行:"+ ((ThreadTask)r).getTaskName());
            }
            
            protected void afterExecute(Runnable r,Throwable t) {
                System.out.println("执行完毕:"+((ThreadTask)r).getTaskName());
            }
            
            protected void terminated() {
                System.out.println("线程池退出");
            }
        };
          
        for(int i=0;i<10;i++) {
            pool.execute(new ThreadTask("Task"+i));
        }    
        pool.shutdown();
    }
}

public class ThreadTask implements Runnable{    
    private String taskName;
    public String getTaskName() {
        return taskName;
    }
    public void setTaskName(String taskName) {
        this.taskName = taskName;
    }
    public ThreadTask(String name) {
        this.setTaskName(name);
    }
    public void run() {
        //输出执行线程的名称
        System.out.println("TaskName"+this.getTaskName()+"---
                            ThreadName:"+Thread.currentThread().getName());
    }
}

输出结果:

线程118352462创建
线程1550089733创建
准备执行:Task0
准备执行:Task1
TaskNameTask0---ThreadName:threadPool118352462
线程865113938创建
执行完毕:Task0
TaskNameTask1---ThreadName:threadPool1550089733
执行完毕:Task1
准备执行:Task3
TaskNameTask3---ThreadName:threadPool1550089733
执行完毕:Task3
准备执行:Task2
准备执行:Task4
TaskNameTask4---ThreadName:threadPool1550089733
执行完毕:Task4
准备执行:Task5
TaskNameTask5---ThreadName:threadPool1550089733
执行完毕:Task5
准备执行:Task6
TaskNameTask6---ThreadName:threadPool1550089733
执行完毕:Task6
准备执行:Task8
TaskNameTask8---ThreadName:threadPool1550089733
执行完毕:Task8
准备执行:Task9
TaskNameTask9---ThreadName:threadPool1550089733
准备执行:Task7
执行完毕:Task9
TaskNameTask2---ThreadName:threadPool118352462
TaskNameTask7---ThreadName:threadPool865113938
执行完毕:Task7
执行完毕:Task2
线程池退出

可以看到通过对 beforeExecute()、afterExecute()和terminated() 的实现,对线程池中线程的运行状态进行了监控,在其执行前后输出了相关打印信息。另外使用 shutdown 方法可以比较安全的关闭线程池,当线程池调用该方法后,线程池中不再接受后续添加的任务。但是,此时线程池不会立刻退出,直到添加到线程池中的任务都已经处理完成,才会退出。

十、线程池线程数量

线程池线程数量的设置没有一个明确的指标,根据实际情况,只要不是设置的偏大和偏小都问题不大,结合下面这个公式即可:

/**
  * Nthreads=CPU数量
  * Ucpu=目标CPU的使用率,0<=Ucpu<=1
  * W/C=任务等待时间与任务计算时间的比率
  */
  Nthreads = Ncpu*Ucpu*(1+W/C)

十一、提交任务时,线程池队列已满,会发生什么

1️⃣无界队列LinkedBlockingQueue
如果使用的是无界队列 LinkedBlockingQueue,继续添加任务到阻塞队列中等待执行,因为 LinkedBlockingQueue 可以近乎认为是一个无穷大的队列,可以无限存放任务。

2️⃣有界队列
如果使用的是有界队列比如 ArrayBlockingQueue,任务首先会被添加到 ArrayBlockingQueue 中,ArrayBlockingQueue 满了,会根据 maximumPoolSize 的值增加线程数量,如果增加了线程数量还是处理不过来,ArrayBlockingQueue 继续满,则会使用拒绝策略 RejectedExecutionHandler 处理满了的任务,默认是 AbortPolicy。

十二、如何定义线程池参数

1️⃣【CPU密集型】=> 线程池的大小推荐为CPU数量 + 1,CPU数量可以根据Runtime.availableProcessors方法获取

2️⃣【IO密集型】=> CPU数量 * CPU利用率 * (1 + 线程等待时间/线程CPU时间)

3️⃣【混合型】=> 将任务分为CPU密集型和IO密集型,然后分别使用不同的线程池去处理,从而使每个线程池可以根据各自的工作负载来调整

4️⃣【阻塞队列】=> 推荐使用有界队列,有界队列有助于避免资源耗尽的情况发生

5️⃣【拒绝策略】=> 默认采用的是 AbortPolicy 拒绝策略,直接在程序中抛出RejectedExecutionException,因为是运行时异常,不强制catch,这种处理方式不够优雅。处理拒绝策略有以下几种比较推荐:

  1. 在程序中捕获RejectedExecutionException,在捕获异常中对任务进行处理。针对默认拒绝策略。

  2. 使用 CallerRunsPolicy 拒绝策略,该策略会将任务交给调用 execute 的线程执行【一般为主线程】,此时主线程将在一段时间内不能提交任何任务,从而使工作线程处理正在执行的任务。此时提交的线程将被保存在 TCP 队列中,TCP 队列满将会影响客户端,这是一种平缓的性能降低。

  3. 自定义拒绝策略,只需要实现RejectedExecutionHandler接口即可

  4. 如果任务不是特别重要,使用 DiscardPolicy 和 DiscardOldestPolicy 拒绝策略将任务丢弃也是可以的。

如果使用 Executors 的静态方法创建 ThreadPoolExecutor 对象,可以通过使用 Semaphore 对任务的执行进行限流也可以避免出现 OOM。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

JFS_Study

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

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

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

打赏作者

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

抵扣说明:

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

余额充值
>