ThreadPoolExecutor参数图解

 

 

为什么使用ThreadPoolExecutor

在android开发中经常会使用多线程异步来处理相关任务,而如果用传统的newThread来创建一个子线程进行处理,会造成一些严重的问题:

1:在任务众多的情况下,系统要为每一个任务创建一个线程,而任务执行完毕后会销毁每一个线程,所以会造成线程频繁地创建与销毁。

2:多个线程频繁地创建会占用大量的资源,并且在资源竞争的时候就容易出现问题,同时这么多的线程缺乏一个统一的管理,容易造成界面的卡顿。

3:多个线程频繁地销毁,会频繁地调用GC机制,这会使性能降低,又非常耗时。

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.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }

参数

  • corePoolSize=> 线程池里的核心线程数量
  • maximumPoolSize=> 线程池里允许有的最大线程数量
  • keepAliveTime=> 空闲线程存活时间
  • unit=> keepAliveTime的时间单位,比如分钟,小时等
  • workQueue=> 缓冲队列
  • threadFactory=> 线程工厂用来创建新的线程放入线程池
  • handler=> 线程池拒绝任务的处理策略,比如抛出异常等策略

一开始我们看到这些参数内心肯定是拒绝的,看源码也有些崩溃,那这些参数到底是些什么意思呢?

或者给一组实际数据:

corePoolSize:1
mamximumPoolSize:3
keepAliveTime:60s
workQueue:ArrayBlockingQueue,有界阻塞队列,队列大小是4
handler:默认的策略,抛出来一个ThreadPoolRejectException

这些代表什么呢?

参数可视化

我们把线程池比作一个花瓶

这个花瓶由 瓶口 、 瓶颈 、 瓶身 三个部分组成。

这三个部分分别对应着线程池的三个参数:maximumPoolSize, workQueue,corePoolSize。

 

改变corePoolSize

改变workQueue

线程池里的线程,我用一个红色小球表示,每来一个任务,就会生成一个小球:

 

而核心线程,也就是正在处理中的任务,则用灰色的虚线小球表示 (目前第一版动画先这样简陋点吧......)

 

我们往线程池中增加任务

于是画风就变成了这样,“花瓶”有这么几个重要的参数:

  • corePoolSize=> 瓶身的容量
  • maximumPoolSize=> 瓶口的容量
  • keepAliveTime=> 红色小球的存活时间
  • unit=> keepAliveTime的时间单位,比如分钟,小时等
  • workQueue=> 瓶颈,不同类型的瓶颈容量不同
  • threadFactory=> 你投递小球进花瓶的小手 (线程工厂)
  • handler=> 线程池拒绝任务的处理策略,比如小球被排出瓶外

如果往这个花瓶里面放入很多小球时(线程池执行任务);

瓶身 (corePoolSize) 装不下了, 就会堆积到 瓶颈 (queue) 的位置;

瓶颈还是装不下, 就会堆积到 瓶口 (maximumPoolSize);

直到最后小球从瓶口溢出。

还记得上面提到的那一组实际参数吗,代表的花瓶大体上是如下图这样的:

那么参数可视化到底有什么实际意义呢?

阿里的规范

我们最开始 接触ThreadPoolExcutor的时候,一般使用Executors去创建线程池,但是阿里开发手册中对于 Java 线程池的使用规范:

一开始我并不知道为什么要去限制这样使用,这四种线程池为什么会导致OOM,

我们看看这四种线程池的具体参数,然后再用花瓶动画演示一下导致OOM的原因。

线程池FixedThreadPool

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

我们关心的参数如下

corePoolSize:nThreads
mamximumPoolSize:nThreads
workQueue:LinkedBlockingQueue

FixedThreadPool表示的花瓶就是下图这样子:

线程池SingleThreadPool:

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

我们关心的参数如下

corePoolSize:1
mamximumPoolSize:1
workQueue:LinkedBlockingQueue

SingleThreadPool表示的花瓶就是下图这样子:

虽然两个线程池的样子没什么差异,但是这里我们发现了一个问题:

为什么 FixedThreadPool 和 SingleThreadPool 的 corePoolSize和mamximumPoolSize 要设计成一样的?

回答这个问题, 我们应该关注一下线程池的 workQueue 参数。

线程池FixedThreadPool和SingleThreadPool 都用到的阻塞队列 LinkedBlockingQueue。

 

LinkedBlockingQueue

 

 /**
     * Creates a {@code LinkedBlockingQueue} with a capacity of
     * {@link Integer#MAX_VALUE}.
     */
    public LinkedBlockingQueue() {
        this(Integer.MAX_VALUE);
    }

从LinkedBlockingQueue的源码注释中我们可以看到, 如果不指定队列的容量, 那么默认就是接近无限大的。

 

从动画可以看出, 花瓶的瓶颈是会无限变长的, 也就是说不管瓶口容量设计得多大, 都是没有作用的!

所以不管线程池FixedThreadPool和SingleThreadPool 的mamximumPoolSize 等于多少, 都是不生效的!

线程池CachedThreadPool

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

我们关心的参数如下

corePoolSize:0
mamximumPoolSize:Integer.MAX_VALUE
workQueue:SynchronousQueue

表示的花瓶就是下图这样子:

 

 

这里我们由发现了一个问题:

为什么CachedThreadPool的mamximumPoolSize要设计成接近无限大的?

回答这个问题, 我们再看一下线程池CachedThreadPool的 workQueue 参数:SynchronousQueue。

SynchronousQueue

来看SynchronousQueue的源码注释:

A synchronous queue does not have any internal capacity, not even a capacity of one.

从注释中我们可以看到, 同步队列可以认为是容量为0。一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态。

所以如果mamximumPoolSize不设计得很大, 就很容易导致溢出。

但是瓶口设置得太大,堆积的小球太多,又会导致OOM(内存溢出)。

 

 

 

 

线程池ScheduledThreadPool

public ScheduledThreadPoolExecutor(int corePoolSize) {
  super(corePoolSize, Integer.MAX_VALUE, 
  0, NANOSECONDS,
  new DelayedWorkQueue());
}

我们关心的参数如下

corePoolSize:corePoolSize
mamximumPoolSize:Integer.MAX_VALUE
workQueue:DelayedWorkQueue

可以看到, 这里出现了一个新的队列 workQueue:DelayedWorkQueue

DelayedWorkQueue 是无界队列, 基于数组实现, 队列的长度可以扩容到 Integer.MAX_VALUE。

同时ScheduledThreadPool的 mamximumPoolSize 也是接近无限大的。

可以想象得到,ScheduledThreadPool就是史上最强花瓶, 极端情况下长度已经突破天际了!

 

到这里, 相信大家已经明白, 为什么这四种线程会导致OOM了。

线程池的状态

状态:

  private static final int RUNNING    = -1 << COUNT_BITS;
    private static final int SHUTDOWN   =  0 << COUNT_BITS;
    private static final int STOP       =  1 << COUNT_BITS;
    private static final int TIDYING    =  2 << COUNT_BITS;
    private static final int TERMINATED =  3 << COUNT_BITS;

线程池状态含义:

  • RUNNING:接受新任务并且处理阻塞队列里的任务;

  • SHUTDOWN:拒绝新任务但是处理阻塞队列里的任务;

  • STOP:拒绝新任务并且抛弃阻塞队列里的任务,同时会中断正在处理的任务;

  • TIDYING:所有任务都执行完(包含阻塞队列里面任务)当前线程池活动线程为 0,将要调用 terminated 方法;

  • TERMINATED:终止状态,terminated方法调用完成以后的状态。

线程池状态转换:

       1.RUNNING -> SHUTDOWN:显式调用 shutdown() 方法,或者隐式调用了 finalize(),它里面调用了 shutdown() 方法。

       2.RUNNING or SHUTDOWN -> STOP:显式调用 shutdownNow() 方法时候。

       3.SHUTDOWN -> TIDYING:当线程池和任务队列都为空的时候。

       4.STOP -> TIDYING:当线程池为空的时候。

       5.TIDYING -> TERMINATED:当 terminated() hook 方法执行完成时候。

 

线程池处理流程和原理

我们先看提交任务的源码:

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

通过上面可视化的展现几个参数,我们大概了解它们的意思

线程池的原理,当提交一个新的任务到线程池时,线程池的处理流程如下:

 

å¨è¿éæå¥å¾çæè¿°

 

执行ThreadPoolExcutor的execute方法,可能会遇到以下情况:

  1. 如果线程池中的线程数未达到核心线程数,则创建核心线程处理任务。
  2. 如果线程数大于或者等于核心线程数,则将任务加入任务队列中,线程池中的空闲线程会不断的从任务队列中取出任务进行处理。
  3. 如果任务队列满了,并且线程数没有达到最大线程数,则创建非核心线程去处理任务。
  4. 如果线程数超过了最大线程数,则执行上面提到的几种饱和策略。

如何配置线程池:

 

  1. CPU密集型任务:尽量使用较小的线程池,一般为CPU核心数+1。 因为CPU密集型任务使得CPU使用率很高,若开过多的线程数,会造成CPU过度切换。
  2. IO密集型任务:可以使用稍大的线程池,一般为2*CPU核心数。 IO密集型任务CPU使用率并不高,因此可以让CPU在等待IO的时候有其他线程去处理别的任务,充分利用CPU时间。
  3. 混合型任务:可以将任务分成IO密集型和CPU密集型任务,然后分别用不同的线程池去处理。 只要分完之后两个任务的执行时间相差不大,那么就会比串行执行来的高效。因为如果划分之后两个任务执行时间有数据级的差距,那么拆分没有意义。因为先执行完的任务就要等后执行完的任务,最终的时间仍然取决于后执行完的任务,而且还要加上任务拆分与合并的开销,得不偿失。

 

 

 

 

 

 

 

 

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值