在上一章中,咱们现已认识到了 CPU 对运用的速度至关重要,而且介绍了“指令数、时钟时刻、指令平均时钟时刻”这三个影响 CPU 时刻的关键要素,以及基于这三个要素衍生出的系列优化计划,这些计划都是为了能充分发挥 CPU 的利用率。

那接下来,我会带着你一同深化到具体的优化计划中。咱们会具体讲解下面这 3 种计划:

  1. 合理的线程池和线程的运用;

  2. 充分利用 CPU 搁置时刻;

  3. 削减 CPU 的等候。

之所以具体介绍这三种计划,是由于它们在发挥 CPU 的利用率上投入产出比最高,也最简单落地的。为了方便大家吸收和理解,这三种计划咱们会分两章来讲,这一章首要介绍线程池和线程的运用。

话不多说,咱们直接开端今日的学习吧!

线程池的重要性

作为开发人员必备的常识,线程池的重要性不言而喻。之前在讲经过削减线程的数量来降低虚拟内存的优化计划时,咱们现已介绍过一些线程池的常识,但还不行深化。在提升运用速度上,合理运用线程能极大地进步 CPU 的利用率,那怎样才是合理地运用线程呢?我觉得应该契合这几个条件,

  1. 线程不能太多也不能太少: 线程太多会浪费 CPU 资源用于使命调度上,而且会削减了中心线程在单位时刻内所能耗费的 CPU 资源。线程太少了则发挥不出 CPU 的功能,浪费了 CPU 资源。
  1. 削减线程创立及状况切换导致的 CPU 损耗: 线程的频频创立毁掉,或许频频的状况切换,如休眠状况切换到运转状况,或许运转状况切换都休眠状况,这些都是对 CPU 资源的损耗。

怎么才能在运用线程的时分契合上面讲的两个条件呢?我觉得要尽量做到两点:第一点是要收敛运用中的线程,包含野线程和各个事务的自定义线程池等,具体办法咱们现已在第 7 章讲过;第二点便是要运用线程池, 咱们在运用开发过程中运用的线程,最好悉数都是从线程池创立的,而且咱们还要能正确地运用线程池, 咱们能够从以下 3 方面来入手:

  1. 怎么创立线程池;
  1. 线程池的类型和特性;
  1. 怎么运用线程池。

下面咱们先看怎么创立线程池。

线程池创立

刚开端接触线程的开发者大都是经过 Executors 目标来创立线程池的,这个目标是 Java 供给给咱们用来创立线程池的东西类,而且大家或许会被 Executors 中众多创立线程池的办法所困扰,如下图中所示。Executors 这个目标里面有十几个 newxxxThreadPool 的静态办法来创立线程池,我该选择哪一个呢?

速度优化:CPU 优化(上)

经过查看这些 newxxxThreadPool 的完成,发现这儿创立的其实只是下面这三个目标中的一个罢了。

首先,能够从源码中看到,newSingleThreadExecutor、newFixedThreadPool、newCacheThreadPool 等实践都是创立了 ThreadPoolExecutor 这个目标,ThreadPoolExecutor 是咱们运用最多的线程池,IO 线程池或 CPU 线程池都是 ThreadPoolExecutor 不同入参的实例。

速度优化:CPU 优化(上)
速度优化:CPU 优化(上)
速度优化:CPU 优化(上)

其次,ScheduledThreadPoolExecutor 实践是调度线程池,假如咱们想要履行延时使命或许周期性使命,就需求运用这个线程池,经过源码能够看到 newSingleThreadScheduledExecutor、newSingleThreadScheduledExecutor、newScheduledThreadPool 等办法中创立的都是这个目标。ScheduledThreadPoolExecutor 调度线程池实践也是承继自 ThreadPoolExecutor 然后进行的封装,所以了解了 ThreadPoolExecutor,根本也就了解了调度线程池。

速度优化:CPU 优化(上)
速度优化:CPU 优化(上)
速度优化:CPU 优化(上)

剩下的 newxxxThreadPool 创立了 ForkJoinPool 这个线程池,它其实是在 Java 1.8 才呈现的一种线程池,专门用来处理并发类算法,运用场景较少,所以这儿不必太关怀它。

速度优化:CPU 优化(上)
速度优化:CPU 优化(上)

剖析 Executors 目标创立线程池的办法完成,咱们能够发现 ThreadPoolExecutor 才是真实的线程池完成类,所以咱们一同深化了解一下 ThreadPoolExecutor。

线程池结构函数剖析

咱们创立的线程池大都是 ThreadPoolExecutor 不同入参的完成类,所以咱们先看一下它入参最多的结构函数:

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

假如咱们将 ThreadPoolExecutor 结构函数中的入参悉数了解了,咱们也就完全掌握了线程池的用法,下面就具体讲解一下该结构函数中每个入参的含义:

入参 说明
int corePoolSize 表明中心线程数量:在创立了线程池后,线程池中此刻线程数为 0,当有使命来到需求履行时,就会创立一个线程去履行使命,当线程池中的线程数目到达 corePoolSize 后,就会把后面到来的使命放到缓存行列中。除非手动调用了allowCoreThreadTimeOut(boolean) 这个办法,用来申明中心线程需求退出,否则中心线程发动后便一向是存活不退出的状况。
int maximumPoolSize 表明线程池中最多能创立线程数量:当中心线程全在履行使命时,又有新使命到来,使命会放在缓存行列中,假如缓存行列也满了,才会发动新的线程来履行这些使命,这些线程也成为非中心线程,非中心线程的数量加上中心线程的数量便是线程池最多能创立的线程数量。
long keepAliveTime 表明非中心线程的存活时刻:当线程池中某个非中心线程线程空闲的时刻到达 keepAliveTime,该线程就会退出,直到线程池中的线程数不超越 corePoolSize,所以这个参数对中心线程是无效的,由于中心线程不会退出,只对非中心线程有用。
TimeUnit unit 表明 keepAliveTime 的时刻单位,如秒、毫秒等
BlockingQueue workQueue 表明使命缓存行列:常见的缓存行列有这些:1. LinkedBlockingDeque 是一个双向的并发行列,首要用于 CPU 线程池;2. SynchronousQueue 尽管也是一个行列,但它并不能存储 task,所以每逢这个行列添加一个 task 时,由于超出了存储行列的容量线程,线程池这个时分都会创立一个新线程来履行这个 task,用于 IO 线程池中。
ThreadFactory threadFactory 线程工厂:可自定义创立线程的方式,设置线程名称,能够默许运用Executors.DefaultThreadFactory(“线程名”),在虚拟内存优化时,也提到过能够运用自定义的线程工厂,来创立栈空间只要 512 KB 的线程。
RejectedExecutionHandler handler 反常处理:所以因反常而无法履行的线程,比方线程池现已满了之后,新的使命就无法履行了,都会放在 RejectedExecutionHandler 中做兜底处理。

这儿需求特别留意,只要缓存行列容量满了,即缓存行列中缓存的 task 到达上限时,才会开端创立非中心线程,咱们能够经过 ThreadPoolExecutor 的 execute 办法完成证实这一点:

public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();
    int c = ctl.get();
    //当线程数小于中心线程,则直接创立线程履行使命
    if (workerCountOf(c) < corePoolSize) {
        if (addWorker(command, true))
            return;
        c = ctl.get();
    }
    //当中心线程已满而且都在运转状况,则将task添加到workQueue缓存行列中
    if (isRunning(c) && workQueue.offer(command)) {
        int recheck = ctl.get();
        if (! isRunning(recheck) && remove(command))
            reject(command);
        else if (workerCountOf(recheck) == 0)
            //假如线程数为 0,则调用addWorker创立线程
            addWorker(null, false);
    }
    //当task往行列添加失利时,才会调用addWorker发动新的线程
    else if (!addWorker(command, false))
        reject(command);
}

经过上面临入参的解释,咱们根本能看懂 Executors 目标中创立的线程池代码,也能自己去创立一个线程池了,但想要创立一个能正确匹配事务场景的线程池,还需求对线程池的类型有深化了解,下面咱们就看一下线程池类型有哪些,它们又各有什么特性。

线程池类型及特性

Executors 目标中有很多线程池的静态办法,如 newFixedThreadPool、newFixedThreadPool、newCachedThreadPool 等等,这些办法经过不同入参来完成不同类型的 ThreadPoolExecutor 线程池实例,可是咱们先不必关怀这些线程池的用法,而是来创立契合自己事务的线程池。那在事务中运用最频频的,首要是以下 3 类线程池。

  1. CPU 线程池:用来处理 CPU 类型使命,如核算,逻辑操作,UI 渲染等。
  1. IO 线程池:用来处理 IO 类型使命,如拉取网络数据,往本地磁盘、数据读写数据等。
  1. 其他线程池:自定义用来满意事务独特化需求的线程池。

不同类型的线程池有不同的职责,专门用来处理对应类型的使命,下面一同来看一下怎么创立不同类型的线程池。

CPU 线程池

首先是 corePoolSize 中心线程数。CPU 线程池是用来履行 CPU 类型使命的,所以它的中心线程数量一般为 CPU 的核数,理想状况下等于核数的线程数量功能是最高的,由于咱们既能充分发挥 CPU 的功能,还削减了频频调度导致的 CPU 损耗。不过,程序在实践运转过程中无法到达理想状况,所以将中心线程数设置为 CPU 核数个或许不是最优的,但肯定是最稳妥且相对较优的计划。

接着是 maximumPoolSize 最大线程数。关于 CPU 线程池来说,最大线程数便是中心线程数,为什么呢?由于 CPU 的最大利用率便是每个核都满载,想要到达满载只需求核数个并发线程就行了,咱们现已设置了等于核数量的中心线程,那中心线程就能够完全发挥出 CPU 资源了,所以即便咱们用更多的线程,只会添加 CPU 调度的损耗。已然最大线程数便是中心线程数,那 keepAliveTime 这个非中心线程数的存活时刻便是零了。

然后是 workQueue 存储行列。CPU 线程池中一致运用 LinkedBlockingDeque,这是一个能够设置容量并支撑并发的行列。由于 CPU 线程池的线程数量较少,假如较多使命来临的话,就需求放在存储行列中,所以这个存储行列不能太小,否则行列满了之后,新来的使命就会进入到错误兜底的处理逻辑中。咱们能够将存储行列设置成无限大,但假如想要追求更好的程序稳定性则不主张这样做了。

假如程序有些反常的死循环逻辑不断地往行列添加使命,而这个行列就能一向接受使命,可是却会导致程序表现反常,由于 CPU 线程池悉数用来履行这个反常使命了。可是当咱们将这个行列设置成有限的,比方 64 个,那这个反常的死循环就会将行列打满,让接下来的使命进入到兜底逻辑中,而咱们能够在兜底逻辑中设置监控,就能及时发现这个反常了。

至于 ThreadFactory 线程工程和 RejectedExecutionHandler 兜底处理的 handler 逻辑,能够运用默许的,假如咱们有特别的需求,比方经过 ThreadFactory 设置优先级,线程名或许优化线程栈巨细,或许在兜底逻辑中添加监控,都能够经过承继对应的类来进行扩展。

了解了 CPU 线程需求的入参,咱们再来看 Exectors 东西类,就能够发现经过 newFixedThreadPool 创立的线程池实践上便是 CPU 线程池的,经过命名也能够猜到,这是一个线程数固定的线程池,所以契合 CPU 线程池线程数固定是 CPU 核数个这一特性。咱们在运用的时分,还能够经过带 ThreadFactory 入参的这个办法 ,调整 FixedThreadPool 线程池的线程优先级。

速度优化:CPU 优化(上)

IO 线程池

IO 线程池首要用来履行 IO 使命,IO 使命实践上耗费的 CPU 资源是十分少的,当咱们要读写数据的时分,会交给 DMA (直接存储器拜访)芯片去做,此刻调度器就会把 CPU 资源切换给其他的线程去运用。由于 IO 使命对 CPU 资源耗费少,所以每来一个 IO 使命就直接发动一个线程去履行它就行了,不需求放入缓存行列中,即便此刻履行了十分多的 IO 使命,也都是 DMA 芯片在处理,和 CPU 无关。了解了这一特性,咱们再来看看 IO 线程池的入参怎么设置。

corePoolSize 核线程数没有定性规定,它和咱们 App 的类型有关 假如 IO 使命比较多,比方新闻咨询类的运用或许大型运用,能够设置得多一些,十几个也能够,太少了就会由于 IO 线程频率创立和毁掉而产生损耗。假如运用较少,IO 使命不多,直接设置为 0 个也没问题。

maximumPoolSize 最大线程数能够多设置一些,保证每个 IO 使命都能有线程来履行,毕竟 IO 使命对 CPU 的耗费不高。一般来说,中小型运用设置 60 个左右就足够了,大型运用则能够设置 100 个以上。这儿不主张将数量设置得特别大,是为了防止程序呈现反常 BUG创立很多的 IO 线程(比方某个场景标志位错误导致逻辑不退出,然后一向创立 IO 线程) ,尽管 IO 使命履行耗费 CPU 资源不多,可是线程的创立和毁掉是需求耗费 CPU 资源的。

接着是 IO 线程池的缓存行列,关于 IO 线程池来说,是不需求缓存行列的,由于每来一个 IO 使命,都会创立一个新的线程去履行,可是为了契合线程池的规划架构,仍是需求传一个行列数据结构进去,所以传入 SynchronousQueue 这个行列即可,它是一个容量为 0 的行列。

了解了上面的常识,咱们再来看 Exectors 东西类,发现经过 newCacheThreadPool 创立的线程池实践上便是对应 IO 线程池的,可是经过 newCacheThreadPool 创立出来的 IO 线程池并不是最优的。咱们能够看到,它的中心线程池数量为 0,而且最大线程数量为无限大。咱们完全能够抛弃Exectors 供给的办法,按照自己的规矩去创立 IO 线程池。这儿需求留意的是,咱们在设置 IO 线程池的线程优先级时,需求比 CPU 线程池的线程优先级高一些,由于 IO 线程中的使命是不怎么耗费 CPU 资源的 优先级 更高一些,能够避免得不到调度的状况呈现。

速度优化:CPU 优化(上)

其他线程池

其他类线程池有很多,但这儿都一致归于一类,这些线程都是为了满意特定事务运用,并不是每个事务都需求用到。比方说,咱们有很多需求履行延时使命或许周期性使命的事务,这时就需求运用 ScheduledThreadPoolExecutor 调度线程池,该线程池也是承继自 ThreadPoolExecutor 目标然后进行的封装,所以和 ThreadPoolExecutor 的原理和用法差别并不大。像是 Java 1.8 版本中才开端呈现的 ForkJoinPool,便是专门用来处理并发类算法,一般在服务端或许特别的 App 上才用到;比方关于既有IO 逻辑又有 CPU 核算逻辑,还无法拆开的使命,咱们还能够创立混合型线程池,用来履行这种混合型使命。 其他类型的线程池就不展开讲了,假如你有兴趣也能够自己学习一下相关常识。

线程池运用

当咱们创立好 ThreadPoolExecutor 实例后,直接调用 ThreadPoolExecutor 的 execute(Runnable command) 办法就能履行使命了。可是咱们在前面学了那么多线程池相关的常识,所以再也不会像开发新手一样,随随便便调用 execute 办法来履行使命了,而是会根据使命的类型来进行调度。假如是 CPU 类型的使命,就需求放在 CPU 线程池中去运转,假如是 IO 类型使命,就需求放在 IO 线程池去运转 。那 假如咱们对所运转的使命类型不清楚怎么办?咱们能够经过插桩将 Runnable 的 run 办法的履行时刻以及对应的线程池打印出来,假如使命耗时较久, 是在 CPU 线程池履行的,那咱们就需求考虑该使命是否需求放在 IO 线程池去履行了。

小结

正确地运用线程池能够协助咱们合理地运用线程,并将 CPU 的功能充分发挥。因而,熟练掌握线程池是 Android 开发者进阶的必经之路。

所以这一章具体介绍了线程池的常识,包含怎么创立线程池,以及线程池的种类和特性,特别是 CPU 线程池和 IO 线程池的定义,它们是怎么创立的。当咱们能了解线程池的原理、了解怎么合理设置线程池的数据、了解各类线程池的特性后,运用线程池就简单很多了,将特定的使命放入特定的线程池中履行,各司其职即可。