Java并发之ThreadPoolExecutor

180 阅读9分钟

1、Java线程池

1-1、线程池的优点

  • 减少资源消耗:通过池化技术可以重复利用线程,降低线程创建和销毁的消耗;

  • 提高响应速度:任务到达时,可以立刻处理不需要创建线程;

  • 提高线程管理:使用线程池可以进行统一分配、调优和监控;无限创建线程会浪费系统资源;

  • 丰富功能:可以扩展功能,比如定时线程池ScheduledThreadPoolExecutor,允许任务延迟或定时执行;

1-2、线程池设计

  • Executor提供最基本的任务执行功能;

  • ExecutorService在任务执行任务执行功能基础上,又定义了【可返回执行结果】的方法(submit),以及对线程生命周期的管理的方法(shutdown、shutdownNow);

  • AbstractExecutorService继承并实现任务执行;

  • ThreadPoolExecutor一方面维护自身的生命周期,另一方面同时管理线程和任务,使两者良好的结合从而执行并行任务。

1-3、线程池的生命周期管理

说明,-1的二进制值为

1-3-1、线程池最多支持的任务数量

CAPACITY = 536870911

二进制:11111111111111111111111111111,29个1;

1-3-2、关于~CAPACITY,用来计算运行状态

~CAPACITY = 11100000000000000000000000000000,29个0;

1-3-3、COUNT_BITS: 值为29,用整数的后29位来管理执行线程的个数;

用前3位管理运行状态;

1-3-4、再看下面这段代码,首先

RUNNING 的值 == **-536870912,**ctl用来记录当前正在运行的线程,就是说,记录线程运行数量的值是负数;

ctl最多为0,如果超过0,线程池就无法在执行新的任务;

private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));


private static int ctlOf(int rs, int wc) { return rs | wc; }

1-3-5、线程池中的状态

运行状态

状态描述

二进制值

十进制值

RUNNING

能接受新提交的任务,并且也能处理阻塞队列中的任务。

-1<<29

111【00000000000000000000000000000】,29个0;

111】,表示运行状态

-536870912

SHUTDOWN

关闭状态。不在接受新提交的任务,但却可以继续处理阻塞队列中已保存的任务。

0

000】,表示关闭状态

0

STOP

停止状态。不接受新的任务,也不处理队列中的任务,并且会中断处理中的任务。

1【00000000000000000000000000000】,29个0,

001】,表示停止状态

536870912

TIDYING

所有任务都以终止,workerCount(工作线程数量)为0。

10【00000000000000000000000000000】,30个0

010】,表示TIDYING

1073741824

TERMINATED

terminated()方法执行完成进入此状态

11【00000000000000000000000000000】,29个0

011】,表示终止状态

1610612736

1-3-6、线程池状态转换

1-4、线程池初始化

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

构造参数说明:

  • corePoolSize:核心线程数,或最小线程数,在没有任务执行时线程池的大小,只有在工作队列满了,才会创建超出这个数量的线程;
  • maximumPoolSize:最大线程数,表示可同时活动的线程数量上限;
  • keepAliveTime: 线程存活时间;与corePoolSize,maximumPoolSize共同负责线程的创建于销毁;如果某个线程的空闲时间超过这个值,将线程标记为可回收的,并且当线程池当前大小超过corePoolSize时,这个线程将被终止;
  • unit,空闲线程存活时间单位;
  • workQueue,保存待执行任务的队列;
  • threadFactory,线程工厂,用于创建新的线程;Executors中提供了默认线程工程DefaultThreadFactory;线程工厂可以【定制化】线程池信息,如线程池的名字(线程名),设置自定义的UncaughtExceptionHandler等;
  • handle,拒绝策略,默认的拒绝策略是AbortPolicy;

1、为什么需要固定线程数量?

避免每次收到请求创建线程,减少创建和销毁线程带来的资源消耗,

也能加快请求执行速度;

2、为什么要设置队列?要用阻塞队列?

阻塞队列是线程安全的

任务处理的时间 可能 大于 任务到达的速率,当新任务到达后,没有足够CPU(线程)时,新的任务将无法处理,所以用队列缓存起来;

队列就是缓解任务突增问题;

3、为什么需要拒绝策略(RejectedExecutionHandler)?为什么要捕获RejectedExecutionException?

如果使用有界队列,如果队列放满,线程池的线程数量也达到最大线程数时,新的任务该如何处理成为新的问题;

Java提供的拒绝策略AbortPolicy会抛出RejectedExecutionException异常,因为需要知道哪个任务处理失败或无法处理,用另外的线程唤起重新处理,特别是在生产环境,如果上游通过消息队列请求的数据,或者异步任务提交的数据,如果直接丢失了,会对业务造成影响。

1-5、线程池如何执行任务?

1-6、任务调度机制

private static boolean isRunning(int c) {
    return c < SHUTDOWN;
}


/**
 * Executes the given task sometime in the future.  The task
 * may execute in a new thread or in an existing pooled thread.
 *
 * If the task cannot be submitted for execution, either because this
 * executor has been shutdown or because its capacity has been reached,
 * the task is handled by the current {@code RejectedExecutionHandler}.
 *
 * @param command the task to execute
 * @throws RejectedExecutionException at discretion of
 *         {@code RejectedExecutionHandler}, if the task
 *         cannot be accepted for execution
 * @throws NullPointerException if {@code command} is null
 */
public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();
    /*
     * Proceed in 3 steps:
     *
     * 1. If fewer than corePoolSize threads are running, try to
     * start a new thread with the given command as its first
     * task.  The call to addWorker atomically checks runState and
     * workerCount, and so prevents false alarms that would add
     * threads when it shouldn't, by returning false.
     *
     * 2. If a task can be successfully queued, then we still need
     * to double-check whether we should have added a thread
     * (because existing ones died since last checking) or that
     * the pool shut down since entry into this method. So we
     * recheck state and if necessary roll back the enqueuing if
     * stopped, or start a new thread if there are none.
     *
     * 3. If we cannot queue task, then we try to add a new
     * thread.  If it fails, we know we are shut down or saturated
     * and so reject the task.
     */
    int c = ctl.get();
    if (workerCountOf(c) < corePoolSize) {
        // 1、小于核心线程,创建
        if (addWorker(command, true))
            return;
        c = ctl.get();
    }
    // 2、核心线程满了,放入队列
    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);
}

所有任务的调度都是由execute方法完成的,这部分完成的工作是:

检查线程池状态,包括:是否处于运行状态,线程池正在运行线程数和运行策略;

其执行过程如下:

  1. 如果workerCount < corePoolSize,则创建并启动一个线程来执行新提交的任务;
  2. 如果线程池是【运行状态】(线程计数值ctl小于0时),并且任务【入队成功】,继续执行3;
  3. 进行double check,重新检查运行的线程数量,如果发现线程池无法执行(正在执行的任务 + 待执行任务 超过 CAPACITY),并且任务可以【成功出队】时,根据拒绝策略处理任务;条件不满足,跳转到步骤4;
  4. 如果工作线程数量(ctl) == 0【代表处理的线程已达到上限】,则创建新的【非核心】线程;
  5. 如果创建非核心线程失败,根据拒绝策略处理任务;

计算密集型业务,先加队列更好;因为即使创建多余线程,也没有CPU执行;

IO密集型,队列作为缓存,因为IO速度比较慢;

1-7、任务申请

任务的执行有两种可能:

  • 一种是任务直接由新创建的线程执行,仅出现在线程初始创建的时候。
  • 另一种是线程从任务队列中获取任务然后执行,执行完任务的空闲线程会再次去从队列中申请任务再去执行,线程获取任务绝大多数的情况。

1-8、线程池数量的配置

假如CPU的核数为N,

  • 对于【计算密集型】操作,线程池数量为 N 或 N+1,因为线程太多,反倒可能导致大量的上下文切换开销

  • 对于【IO密集型】操作:线程数 = CPU核数 × 目标CPU利用率 ×(1 + 平均等待时间/平均工作时间);

1-9、线程池【核心线程数量】回收

可以,使用setCorePoolSize方法,调节核心线程数量。

public void setCorePoolSize(int corePoolSize) {
    // 如果参数小于0,直接抛出异常
    if (corePoolSize < 0)
        throw new IllegalArgumentException();
    int delta = corePoolSize - this.corePoolSize;
    this.corePoolSize = corePoolSize;
    // 当工作线程大于核心线程,需要中断空闲线程
    if (workerCountOf(ctl.get()) > corePoolSize)
        interruptIdleWorkers();
    else if (delta > 0) {
        // 如果新的核心线程数大于原有核心线程,则扩展线程,假如队列为空,则不增加核心线程数量
        int k = Math.min(delta, workQueue.size());
        while (k-- > 0 && addWorker(null, true)) {
            if (workQueue.isEmpty())
                break;
        }
    }
}

1-10、线程池的饱和策略

Java提供的饱和策略如下

  • AbortPolicy(中止):是默认的饱和策略,该策略将抛出一个未检查(Unchecked)的RejectedExecutionException,调用者可以捕获这个一次,然后根据需求编写自己的代码。

    public static class AbortPolicy implements RejectedExecutionHandler {

    public AbortPolicy() { }
    
    
    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        throw new RejectedExecutionException("Task " + r.toString() +
                                             " rejected from " +
                                             e.toString());
    }
    

    }

  • DiscardPolicy(抛弃):当新提交的任务无法保存到队列中等待执行时,将会抛弃该任务;

  • DiscardOldestPolicy(抛弃最旧的),抛弃队列中第一个任务(最早入队或者优先级最高的任务);

  • CallerRunsPolicy(调用者运行):实现一个调节机制,该策略既不会抛弃任务,也不会抛出异常,而是讲某些任务回退到调用者,从而降低新任务的流量。

1-11、线程池中的线程是如何创建的

调用以下方法,其中core代表是否为核心线程,true是,false不是;

内部使用ThreadFactory(线程工厂)创建一个线程;

addWorker(Runnable firstTask, boolean core)

1-12、为什么不建议使用 Executors提供的工厂方法创建线程池

1、因为Executors的【newFixedThreadPool】和【newSingleThreadPool】使用的是 LinkedBlockingQueue,这个队列的长度是Integer.MAX_VALUE,如果任务较多并且处理速度慢,队列中任务变得太多,造成内存空间占满;

2、而Executors的【newCachedThreadPool】,是创建一个最大线程数量等于Integer.MAX_VALUE,线程数量会无限增加,导致系统资源占满;

更多线程池说明参考:

tech.meituan.com/2020/04/02/…

2、Executor框架

先看Executor 框架的基本组成

  • Executor 是一个基础的接口,其初衷是将任务提交和任务执行细节解耦,这一点可以体会其定义的唯一方法。

    public interface Executor {

    /**
     * Executes the given command at some time in the future.
     * The command may execute in a new thread,
     * in a pooled thread, or in the calling thread,
     * at the discretion of the {@code Executor} implementation.
     *
     * @param command the runnable task
     * @throws RejectedExecutionException if this task cannot be
     * accepted for execution
     * @throws NullPointerException if command is null
    */
    void execute(Runnable command);
    

    }

  • ExecutorService 则更加完善,不仅提供 service 的管理功能,比如 shutdown 等方法,也提供了更加全面的提交任务机制;

    public interface ExecutorService extends Executor {

    /**
     * 启动有序关闭,在该关闭中执行先前提交的任务,但不接受任何新任务
     */ 
    void shutdown();
    
    /**
     * 尝试停止所有正在执行的任务,暂停正在等待的任务的处理,并返回正在等待执行的任务的列表。
     */
    List<Runnable> shutdownNow();
    
    boolean isShutdown();
    
    boolean isTerminated();
    
    <T> Future<T> submit(Callable<T> task);
    
    /**
     * Submits a Runnable task for execution and returns a Future
     * representing that task. The Future's {@code get} method will
     * return the given result upon successful completion.
     *
     * @param task the task to submit
     * @param result the result to return
     * @param <T> the type of the result
     * @return a Future representing pending completion of the task
     * @throws RejectedExecutionException if the task cannot be
     *         scheduled for execution
     * @throws NullPointerException if the task is null
     */
    <T> Future<T> submit(Runnable task, T result);
    
    /**
     * Submits a Runnable task for execution and returns a Future
     * representing that task. The Future's {@code get} method will
     * return {@code null} upon <em>successful</em> completion.
     *
     * @param task the task to submit
     * @return a Future representing pending completion of the task
     * @throws RejectedExecutionException if the task cannot be
     *         scheduled for execution
     * @throws NullPointerException if the task is null
     */
    Future<?> submit(Runnable task);
    

    }

  • Executors 则从简化使用的角度,为我们提供了各种方便的静态工厂方法。

Executors 目前提供了 5 种不同的线程池创建配置:

2-1、newCachedThreadPool

newCachedThreadPool(),它是一种用来处理大量短时间工作任务的线程池,具有几个鲜明特点:

  • 初始化核心线程数为0,最大线程数为Integer.MAX_VALUE;
  • 会试图缓存线程并重用,当无缓存线程可用时,就会创建新的工作线程;
  • 如果线程闲置的时间超过 60 秒,则被终止并移出缓存;
  • 长时间闲置时,这种线程池,不会消耗什么资源。
  • 其内部使用 SynchronousQueue 作为工作队列,SynchronousQueue 每个插入操作必须等待另一个线程进行相应的删除操作,反之亦然。 同步队列没有任何内部容量,甚至没有一个容量。

极端情况可能导致 OOM;

public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}

2-2、newFixedThreadPool

newFixedThreadPool(int nThreads),重用【固定数目(nThreads)】的线程,

任何时候最多有 nThreads 个工作线程是活动的;

这意味着,如果任务数量超过了活动队列数目,将在工作队列中等待空闲线程出现;

如果有工作线程退出,将会有新的工作线程被创建,以补足指定的数目 nThreads;

创建的线程池不会超时;

其内部使用****LinkedBlockingQueue作为工作队列。LinkedBlockingQueue【并不是一个无界队列】,队列最大长度为Integer.MAX_VALUE

队列中元素无限增加;

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

2-3、newSingleThreadExecutor

newSingleThreadExecutor(),它的特点在于工作线程数目被限制为 1,操作一个工作队列,所以它保证了所有任务的都是被顺序执行,最多会有一个任务处于活动状态,并且不允许使用者改动线程池实例,因此可以避免其改变线程数目。

1、单个线程;

2、队列接近 Integer.MAX_VALUE;

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

2-4、newSingleThreadScheduledExecutor

newSingleThreadScheduledExecutor() 和 newScheduledThreadPool(int corePoolSize),创建的是个 ScheduledExecutorService,可以进行定时或周期性的工作调度,区别在于单一工作线程还是多个工作线程;

public static ScheduledExecutorService newSingleThreadScheduledExecutor() {
    return new DelegatedScheduledExecutorService
        (new ScheduledThreadPoolExecutor(1));
}


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

2-5、newWorkStealingPool

newWorkStealingPool(int parallelism),是java 8 才加入的创建方法,其内部会构建ForkJoinPool,利用Work-Stealing算法,并行地处理任务,不保证处理顺序。

  • 工作队列负责存储用户提交的各个任务,这个工作队列,可以是容量为 0 的 SynchronousQueue(使用 newCachedThreadPool),也可以是像固定大小线程池(newFixedThreadPool)那样使用 LinkedBlockingQueue。
  • 内部的“线程池”,这是指保持工作线程的集合,线程池需要在运行过程中管理线程创建、销毁。例如,对于带缓存的线程池,当任务压力较大时,线程池会创建新的工作线程;当业务压力退去,线程池会在闲置一段时间(默认 60 秒)后结束线程。
  • ThreadFactory 提供上面所需要的创建线程逻辑。
  • 如果任务提交时被拒绝,比如线程池已经处于 SHUTDOWN 状态,需要为其提供处理逻辑,Java 标准库提供了类似ThreadPoolExecutor.AbortPolicy等默认实现,也可以按照实际需求自定义。