玩转java线程池二:ThreadPoolExecutor的使用

936 阅读7分钟

这是我参与8月更文挑战的第15天,活动详情查看:8月更文挑战

玩转Java线程池:

一、创建线程池

ThreadPoolExecutor有四个构造方法:

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             Executors.defaultThreadFactory(), defaultHandler);
}

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             threadFactory, defaultHandler);
}

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              RejectedExecutionHandler handler) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             Executors.defaultThreadFactory(), handler);
}

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;
    }

创建一个线程池时需要的参数有:

  • int corePoolSize(核心线程池大小):该参数的作用上面的内容已经讲得很清楚了。如果调用了ThreadPoolExecutor的prestartAllCoreThreads()方法,线程池会提前创建并启动所有核心线程(core thread)

  • int maximumPoolSize(最大线程池大小):线程池允许创建的最大线程数。阻塞队列满了之后,就会去创建新线程,且创建之后线程数小于最大线程数,线程池才会去执行任务。但是如果阻塞队列使用的是无界队列,该参数就不会有效果,毕竟阻塞队列都是无界。

  • long keepAliveTime(空闲线程的存活时间):线程池中的线程空闲之后,可以存活的时间,如果过了这个时间,线程就会被销毁。如果任务很多,每个任务执行时间比较短,可以相对的调大keepAliveTime,可以提高线程的利用率。

  • TimeUnit unit(keepAliveTime的时间单位):时分秒,毫秒和微秒等时间单位。

  • BlockingQueue<Runnable> workQueue()(任务队列,阻塞队列):用于保存等待执行的任务的阻塞队列。在jdk中有几个阻塞队列的实现:

    • ArrayBlockingQueue:基于数组实现的有界阻塞队列。按照先进先出(FIFO)的原则对进来的元素进行排序。
    • LinkedBlockingQueue:基于链表实现的游街队列,也是拥有先进先出的队列特性。吞吐量高于ArrayBlockingQueue。
    • SynchronoutQueue:同步队列,不存储元素的阻塞队列。每插入一个元素必须等到该元素被取出,否则插入操作会被一直阻塞,吞吐量同通常高于LinkedBlockingQueue。
    • PriorityBlockingQueue:具有优先级的无限阻塞队列。
  • ThreadFactory threadFactory(线程工厂):可以通过线程工厂给每个创建出来的线程设置名字。

  • RejectedExecutionHandler handler(拒绝执行的处理器):当队列和线程池都已经满了,那么线程池必须采用一种决绝策略来处理还在被提交过来的新任务。默认使用的拒绝策略是AbortPolicy,该策略在处理新任务时会直接抛出异常。jdk中线程池框架提供了四种决绝策略:

    • AbortPolicy:直接抛出异常。
    • CallerRunsPolicy:任务返回给调用者所在线程来执行。
    • DiscardOldestPolicy:对其队列中最近的一个任务,并执行并执行当前任务。
    • DiscardPolicy:不处理任务,直接丢弃。

    如果这四种决绝策略不满足,可以实现RejectedExecutionHandler 接口自定自己的拒绝策略。

二、向线程池提交任务

往ThreadPoolExecutor中提交任务有两个方法:execute()和submit()。

它们两个的不同是execute()方法只接受Runnable类型并且不能返回结果。

submit()方法能够接受Runnable和Callable两种类型的参数,所以submit()方式是完全可以取代execute()方法。需要返回任务的执行结果之类的返回值,只能使用submit()方法。submit()方法可以返回Future类型的对象。可以通过该类型的get()方法得到返回值,该方法会阻塞当前线程直到结果返回,使用get(long timeout,TimeUnit unit)方法会阻塞,知道时间结束,如果超时不返回结果,会抛出错误。

public class ThreadPoolExecutorDemo1 {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
                5,
                10,
                3,
                TimeUnit.SECONDS,
                new LinkedBlockingDeque<>(10),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.DiscardOldestPolicy()
        );

        threadPoolExecutor.execute(() -> {
            System.out.println(Thread.currentThread().getName());
        });

        Future<String> future = threadPoolExecutor.submit(() -> Thread.currentThread().getName());
        System.out.println(future.get());
    }
}

三、线程池的关闭

可以通过调用ThreadPoolExecutor的shutdown或shutdownNow方法来关闭线程池。原理是遍历线程池中的工作线程,然后逐个调用线程的interrupt方法来中断线程,所以无法响应中断的任务可能永远无法终止。但是它们存在一定的区别,shutdownNow首先将线程池的状态设置成STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表,而shutdown只是将线程池的状态设置成SHUTDOWN状态,然后中断所有没有正在执行任务的线程。

只要调用了这两个关闭方法中的任意一个,isShutdown方法就会返回true。当所有的任务都已关闭后,才表示线程池关闭成功,这时调用isTerminaed方法会返回true。至于应该调用哪一种方法来关闭线程池,应该由提交到线程池的任务特性决定,通常调用shutdown方法来关闭线程池,如果任务不一定要执行完,则可以调用shutdownNow方法。

四、线程池参数配置解析

要合理地配置线程池,必须分析任务特性,一般从以下几个角度来分析:

  • 任务的性质:CPU密集型任务、IO密集型任务和混合型任务。
  • 任务的优先级:高、中和低。
  • 任务的执行时间:长、中和短。
  • 任务的依赖性:是否依赖其他系统资源,如数据库连接。

性质不同的任务可以用不同规模的线程池分开处理。

CPU密集型任务应配置尽可能小的线程,如配置cpu +1个线程的线程池

由于IO密集型任务线程并不是一直在执行任务,则应配置尽可能多的线程,如2*cpu

混合型的任务,如果可以拆分,将其拆分成一个CPU密集型任务和一个IO密集型任务,只要这两个任务执行的时间相差不是太大,那么分解后执行的吞吐量将高于串行执行的吞吐量。如果这两个任务执行时间相差太大,则没必要进行分解。可以通Runtime.getRuntime().availableProcessors()方法获得当前设备的CPU个数。

优先级不同的任务可以使用优先级队列PriorityBlockingQueue来处理。它可以让优先级高的任务先执行。如果一直有优先级高的任务提交到队列里,那么优先级低的任务可能永远不能执行,造成饥饿现象

执行时间不同的任务可以交给不同规模的线程池来处理,或者可以使用优先级PriorityBlockingQueue队列,让执行时间短的任务先执行

例如依赖数据库连接池的任务,因为线程提交SQL后需要等待数据库返回结果,等待的时间越长,则CPU空闲时间就越长,那么线程数应该设置得越大,这样才能更好地利用CPU

使用有界队列。有界队列能增加系统的稳定性和预警能力,可以根据需要设大一点,比如几千。如果设置成无界队列,那么线程池的队列就会越来越多,有可能会撑满内存引发OOM,导致整个系统不可用

五、监控线程池

如果在系统中大量地使用了线程池,有必要对线程池进行监控,这样在出现问题地时候,可以快速定位线程池的问题。具体可以通过线程池的参数进行监控:

  • taskCount:线程池需要执行的任务数量。

  • completedTaskCount:线程池在运行过程中已完成的任务数量,小于或等于taskCount。

  • largestPoolSize:线程池里曾经创建过的最大线程数量。通过这个数据可以知道线程池是否曾经满过。如该数值等于maximumPoolSize,表示线程池曾经满过。

  • getPoolSize:线程池的线程数量。如果线程池不销毁的话,线程池里的线程不会自动销毁,所以这个大小只增不减。

  • getActiveCount:获取活动的线程数。

通过扩展线程池进行监控。可以通过继承线程池来自定义线程池,重写线程池的beforeExecute、afterExecute和terminated方法,也可以在任务执行前、执行后和线程池关闭前执行一些代码来进行监控。例如,监控任务的平均执行时间、最大执行时间和最小执行时间等。 这几个方法在线程池里是空方法。

ThreadPoolExecutor中的方法,这些方法还是空方法,可以对其进行扩展。

protected void beforeExecute(Thread t, Runnable r) { }
protected void afterExecute(Runnable r, Throwable t) { }
protected void terminated() { }