Bootstrap

JAVA多线程进阶篇 11、JUC线程池之ThreadPoolExecutor

1. 为什么要使用线程池?

如果放任线程无限制地创建,会耗尽CPU资源,并且降低系统的响应速度。
为了更好的对线程进行管理,实现线程的统一分配,调优和监控,降低资源消耗。
JUC设计了线程池。线程池从机制上分为两种.,一是ThreadPoolExecutor,另一类是ForkJoinPool。

  • ThreadPoolExecutor
    使用多个线程和阻塞队列实现对线程和任务进行管理的工具。
  • ForkJoinPool
    将一个大任务拆分为很多小任务来异步执行的管理的工具。

1.1 Executor 和 ExecutorService

ThreadPoolExecutor 和 ForkJoinPool 都实现了Executor 和 ExecutorService接口。
Executor 和 ExecutorService接口规定了对线程池使用的提交任务和关闭任务的方法。

1.2 线程池提交任务

  • execute()方法是Executor接口规定的。只能接受Runnable类型的任务。
    void execute(Runnable command);
  • submit()方法是ExecutorService接口规定的。有返回值,且可以通过Future.get()抛出Exception
    <T> Future<T> submit(Callable<T> task);
    <T> Future<T> submit(Runnable task, T result);
    Future<?> submit(Runnable task);

1.3 线程池关闭

  • shutdown()方法,shutdown只是将线程池的状态设置为SHUTWDOWN状态,正在执行的任务会继续执行下去,没有被执行的则中断。

  • shutdownNow(),而shutdownNow则是将线程池的状态设置为STOP,正在执行的任务则被停止,没被执行任务的则返回。

    shutdownNow会停止正在运行的线程。

2. ThreadPoolExecutor

2.1 构造方法与参数

  • int corePoolSize:核心线程数,常驻在线程池内。
  • int maximumPoolSize:最大线程数。
  • long keepAliveTime:当线程池里线程数量大于corePoolSize时,会将超时空闲的线程进行关闭,keepAliveTime代表时间计量。
  • TimeUnit unit:当线程池里线程数量大于corePoolSize时,会将超时空闲的线程进行关闭,unit代表时间单位。
  • BlockingQueue workQueue:存储线程任务的队列。
  • ThreadFactory threadFactory:创建线程的方法,可以自定义。
  • RejectedExecutionHandlerhandler:当线程数大于maximumPoolSize,且workQueue放不下时,拒绝任务的策略,可以自定义。
    • RejectedExecutionHandlerhandler

构造方法:

   public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              RejectedExecutionHandler handler) 

2.2 ThreadPoolExecutor原理

  • 线程池首先当前运行的线程数量是否少于corePoolSize。如果是,则创建一个新的工作线程来执行任务。如果都在执行任务,否则进入下一步。
  • 判断BlockingQueue是否已经满了,倘若还没有满,则将线程放入BlockingQueue。否则进入下一步
  • 如果创建一个新的工作线程将使当前运行的线程数量超过maximumPoolSize,则交给RejectedExecutionHandler来处理任务。

2.3 自定义ThreadPoolExecutor

尽管Executors实现了多个静态方法来创建各种线程池,在正式工作中,我们应当实现自定义的ThreadPoolExecutor 。根据实际场景需求,指定活动线程的数量,限制线程池的大小、创建我们自己的 RejectedExecutionHandler 实现来处理不能适应工作队列的工作。

以下程序创建的线程池,初始大小为2,最大为5,不设置超时,任务队列大小为3,在创建线程时,为线程自定义命名,当超出了线程池处理容量时,将拒绝任务,并将消息写入队列。

public class ThreadFactoryTest {

    static AtomicLong cnt = new AtomicLong();

    public static void main(String[] args) {

        RejectedExecutionHandler handler = new RejectedExecutionHandler() {
            @Override
            public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
                System.out.println("我拒绝了任务,并将任务移到MQ中等待以后执行");
            }
        };

        ThreadFactory factory = new ThreadFactory() {
            @Override
            public Thread newThread(Runnable r) {
                return new Thread(r,"线程"+cnt.incrementAndGet());
            }
        };

        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2, 5, 0, TimeUnit.SECONDS, new ArrayBlockingQueue<>(3), factory, handler);
        for(int i=0;i<1000;i++) {
            threadPoolExecutor.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName()+" ,提交任务");
                }
            });
        }
        threadPoolExecutor.shutdown();
    }
}

3. Executors提供ThreadPoolExecutor构造方法

Executors是一个JUC工具类,实现了多个静态方法来创建各种线程池,下面来了解一下。

在高并发环境中并不建议直接使用Executors的方法,建议实现自定义的ThreadPoolExecutor。

3.1 SingleThreadExecutor

线程池中只有一个线程,因此可以保证所提交任务的顺序执行。

  • 核心线程数 1
  • 最大线程数 1
  • 超时时间为0,即不存在超时移除的情况
  • 使用无界的阻塞队列。可存放的任务数为Integer.MAX_VALUE。
    public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }

3.2 CachedThreadPool

线程池线程不设上限,使用SynchronousQueue作为阻塞队列,当任务来后立即调度线程执行,如果不够,就创建新线程执行。适用于任务压力不平稳的场景。

  • 核心线程数 0
  • 最大线程数 Integer.MAX_VALUE
  • 超时时间为1分钟,1分钟没有任务的线程会被销毁
  • 使用SynchronousQueue作为阻塞队列。可以参考后面介绍JUC集合的博客文章
    public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }

3.3 FixedThreadPool

指定线程池的线程数量,不会主动销毁线程,也不会增加线程数。适用于任务压力较平稳的场景。
线程池的数量一般根据CPU核数、以及CPU的利用率综合考虑,实际工作中通过压测确定。

  • 核心线程数 nThreads
  • 最大线程数 nThreads
  • 超时时间为0,即不存在超时移除的情况
  • 使用无界的阻塞队列。可存放的任务数为Integer.MAX_VALUE。
    public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }

3.4 ScheduledThreadPool

任务提供延迟或周期执行。核心线程数由程序指定,线程数不限,使用DelayedWorkQueue来存储任务。

  • 核心线程数 corePoolSize
  • 最大线程数 Integer.MAX_VALUE
  • 超时时间为0,即不存在超时移除的情况
  • 使用DelayedWorkQueue,为存储周期或延迟任务专门定义的一个延迟队列。
    public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
        return new ThreadPoolExecutor(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
    new DelayedWorkQueue());
    }

总结

为了更好的对线程进行管理,实现线程的统一分配,调优和监控,降低资源消耗,JUC设计了线程池。

Executors实现了多个静态方法来创建各种线程池,在正式工作中,我们应当实现自定义的ThreadPoolExecutor 。

多线程系列在github上有一个开源项目,主要是本系列博客的实验代码。

https://github.com/forestnlp/concurrentlab

如果您对软件开发、机器学习、深度学习有兴趣请关注本博客,将持续推出Java、软件架构、深度学习相关专栏。

您的支持是对我最大的鼓励。

;