为什么不建议通过Executors静态工厂构建线程池?

1,549 阅读7分钟

使用线程池的好处是:

1、降低资源消耗

可以重复利用已创建的线程降低线程创建和销毁造成的消耗。

2、提高响应速度

当任务到达时,任务可以不需要等到线程创建就能立即执行。

3、提高线程的可管理性

线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控

Executors

Executors创建线程池的方式

1、newFiexedThreadPool(int Threads):创建固定数目线程的线程池。

public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads, 0L,
        TimeUnit.MILLISECONDS, new LinkedBlockingQueue());
    }

2、newCachedThreadPool():创建一个可缓存的线程池,调用execute 将重用以前构造的线程(如果线程可用)。如果没有可用的线程,则创建一个新线程并添加到池中。终止并从缓存中移除那些已有 60 秒钟未被使用的线程。


    public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, 2147483647, 60L, TimeUnit.SECONDS,
        new SynchronousQueue());
    }

3、newSingleThreadExecutor()创建一个单线程化的Executor。


    public static ExecutorService newSingleThreadExecutor() {
        return new Executors.FinalizableDelegatedExecutorService(new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, 
        new LinkedBlockingQueue()));
    }

4、newScheduledThreadPool(int corePoolSize)创建一个支持定时及周期性的任务执行的线程池,多数情况下可用来替代Timer类。


    public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
        return new ScheduledThreadPoolExecutor(corePoolSize);
    }
    

    public ScheduledThreadPoolExecutor(int corePoolSize) {
        super(corePoolSize, 2147483647, 10L, TimeUnit.MILLISECONDS, 
        new ScheduledThreadPoolExecutor.DelayedWorkQueue());
    }

为什么不建议通过Executors这几种方式创建线程池呢?因为这几种方式创建的线程池可能会导致OOM(OutOfMemory ,内存溢出)。

1)newFiexedThreadPool、newSingleThreadExecutor使用的是一个无边界的LinkedBlockingQueue(因为没有设置容量),最大长度为Integer.MAX_VALUE,当不断往队列中加入任务时,将会导致OOM。

2)newCachedThreadPool、newScheduledThreadPool创建时线程池时允许的最大线程数是Integer.MAX_VALUE,当不断创建线程时也会导致OOM。

建议直接使用ThreadPoolExecutor的构造函数来自己创建线程池。在创建的同时,给BlockQueue指定容量就可以了。

ThreadPoolExecutor

构造函数参数

new ThreadPoolExecutor(
int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
RejectedExecutionHandler handler)
  • corePoolSize线程池维护线程的最少数量 (core : 核心)

  • maximumPoolSize:线程池维护线程的最大数量

  • keepAliveTime:线程池维护线程所允许的空闲时间 -unit:线程池维护线程所允许的空闲时间的单位(可选的参数为java.util.concurrent.TimeUnit中的几个静态属性:NANOSECONDS、MICROSECONDS、MILLISECONDS、SECONDS。)

  • workQueue:线程池所使用的缓冲队列(常用的是:java.util.concurrent.ArrayBlockingQueue)

  • handler:线程池对拒绝任务的处理策略。

    • ThreadPoolExecutor.AbortPolicy():抛出java.util.concurrent.RejectedExecutionException异常。
    • ThreadPoolExecutor.CallerRunsPolicy(): 重试添加当前的任务,调用execute()的线程自动重复调用execute()方法。
    • ThreadPoolExecutor.DiscardOldestPolicy(): 抛弃队列中最旧的任务(也是将要执行的任务),并再次execute此任务。
    • ThreadPoolExecutor.DiscardPolicy(): 抛弃当前的任务。

工作过程

通过 execute(Runnable)方法将任务添加到线程池,任务就是一个 Runnable类型的对象,任务的执行方法就是 Runnable类型对象的run()方法。

当一个任务通过execute(Runnable)方法欲添加到线程池时: 如果此时线程池中的数量小于corePoolSize,即使线程池中的线程都处于空闲状态,也要创建新的线程来处理被添加的任务。

如果此时线程池中的数量等于 corePoolSize,但是缓冲队列 workQueue未满,那么任务被放入缓冲队列。

如果此时线程池中的数量大于corePoolSize,缓冲队列workQueue满,并且线程池中的数量小于maximumPoolSize,建新的线程来处理被添加的任务。

如果此时线程池中的数量大于corePoolSize,缓冲队列workQueue满,并且线程池中的数量等于maximumPoolSize,那么通过 handler所指定的策略来处理此任务。

也就是:处理任务的优先级为:核心线程corePoolSize、任务队列workQueue、最大线程maximumPoolSize,如果三者都满了,使用handler处理被拒绝的任务。 当线程池中的线程数量大于 corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被终止。这样,线程池可以动态的调整池中的线程数。

execute()方法和submit()方法的区别

两个方法都是用来执行任务的,在说execute()方法和submit()方法的区别之前,我们先来了解下实现Runnable接口和Callable接口的区别。

  • Runnable 接口不会返回结果但 是 Callable 接口可以返回结果。

  • Callable接口的call()方法允许抛出异常;而Runnable接口的run()方法的异常只能在内部消化,不能继续上抛

execute()方法和submit()方法有以下区别:

1、接受的参数不一样

execute()方法只接收Runnable类型的参数,而submit()方法既接收Runnable类型也接收Callable类型参数

execute()方法最后执行的是Runnable里面的run()方法,而submit()方法里面最后致性的是Callable里面的call()方法。(submit()方法传进去的Runnable或者Callable都会被封装成FutureTask里的Callable,此类是一个Runnable,最后执行的是Callable里的call()方法)

2、返回值不一样

execute() 方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否;

submit()方法用于提交需要返回值的任务。线程池会返回一个future类型的对象,通过这个future对象可以判断 任务是否执行成功,并且可以通过future的get()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用 get(long timeout,TimeUnit unit) 方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。

3、异常处理不一样

execute()方法是会直接抛出异常的,而submit()方法就不会抛出异常,而是另作处理。

submit在执行过程中与execute不一样,不会抛出异常而是把异常保存在成员变量中,在FutureTask.get阻塞获取的时候再把异常抛出来。

execute直接抛出异常之后线程就死掉了,submit保存异常线程没有死掉。

ThreadPoolExector原理解析

直接看execute()方法

public void execute(Runnable command) {
        if (command == null) {
            throw new NullPointerException();
        } else {
            int c = this.ctl.get(); //ctl是一个AtomicInteger类型的变量,保存线程池的状态和正在运行的worker数量(可理解为线程数量)
            if (workerCountOf(c) < this.corePoolSize) {//如果当前运行线程数小于核心数
                if (this.addWorker(command, true)) {//addWorker()其实就是创建线程并执行任务
                    return;
                }

                c = this.ctl.get();//再次获取ctl,因为要考虑并发情况
            }

            
            if (isRunning(c) && this.workQueue.offer(command)) {//放入队列中
                int recheck = this.ctl.get();
                if (!isRunning(recheck) && this.remove(command)) {
                    this.reject(command);
                } else if (workerCountOf(recheck) == 0) {
                    this.addWorker((Runnable)null, false);
                }
            } else if (!this.addWorker(command, false)) {//队列满了的情况,尝试创建新线程,若线程数达到最大,则执行拒绝策略
                this.reject(command);
            }

        }
    }

AtomicInteger类型的ctl属性,ctl为线程池的控制状态,用来表示线程池的运行状态(整形的高3位)和运行的worker数量(低29位)),有如下5中状态:

  • RUNNING 运行态
  • SHUTDOWN 关闭,此时不接受新的任务,但继续处理队列中的任务。
  • STOP 停止,此时不接受新的任务,不处理队列中的任务,并中断正在执行的任务
  • TIDYING 所有的工作线程全部停止,并工作线程数量为0,将调用terminated方法,进入到TERMINATED
  • TERMINATED 终止状态

workQueue缓存没有被立即执行的Runnable。

 private final class Worker extends AbstractQueuedSynchronizer implements Runnable

worker重写了AQS的相应函数和重写了Runnable的run函数。可以理解它就是线程池里执行任务的线程。其run()方法是执行其当前的任务或者从队列中拿到的任务。

workers是Runnable的一个封装类Worker的集合,实现了Runnable接口,workers真正的线程池核心实现,它是一个HashSet集合,保存当前正在执行和等待执行的任务。