线程池从零认识到深层理解——初识

线程系列目录

  1. Thread线程从零认识到深层理解——初识
  2. Thread线程从零认识到深层理解——六大状态
  3. Thread线程从零认识到深层理解——wait()与notify()
  4. Thread线程从零认识到深层理解——线程安全
  5. 线程池从零认识到深层理解——初识
  6. 线程池从零认识到深层理解——进阶

博客创建时间:2020.09.25
博客更新时间:2021.02.25

注意:本系列博文源码分析取自于Android SDK=30,与网络上的一些源码可能不一样,可能他们分析的源码更旧,无需大惊小怪。


前言

在Android开发中常在非UI线程中处理耗时任务,此时需要使用线程来处理异步任务,如果每次都创建一个线程并在执行完毕后销毁,那么在需要多线程使用频繁的场景会消耗大量的资源和性能的损耗。且线程容易各自为政,很难控制。

为了解决线程的使用弊端,在Java1.5中提供了Executor框架用于把任务的提交和执行解耦。任务的提交交给Runnable或者Callable,而Executor框架用来处理任务,Executor框架的核心实现类就是ThreadPoolExecutor


一、线程池意义

程序中并发的线程数量有很多,如果每次手动创建只是执行了时间很短的任务就结束了,那么频繁的创建线程和销毁线程会降低系统的效率,并降低CPU的使用效率。为了节省资源,提高效率,Java中提供了线程池来解决问题。

线程池其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。

线程池结构关系图:
在这里插入图片描述


二、线程池优点

  1. 降低系统资源消耗,通过多次重用已存在的线程,降低线程创建和销毁带来的性能消耗;
  2. 提高系统响应速度,当有任务到达时,通过复用已存在的线程,无需等待新线程的创建便能立即执行;
  3. 提高线程的可管控性,根据系统得承载能力,调整工作线程的数目,防止内存占用过多出现OOM(每个线程消耗约1MB),同时线程数太多,频繁的CPU切换会产生高额的调度时间成本和降低CPU调度片段时间。并且可以使用线程池对线程进行统一分配、调优和监控。
  4. 提供更强大的功能,延时定时线程池,指定线程的优先级。
  5. 避免大量的线程开启后因互相抢占系统资源导致阻塞现象

线程池是可以手动调用关闭的,根据不同的关闭策略,能将待执行或者正在执行的任务终止。


三、线程池创建

线程数量控制
并发线程数量应该在合理范围内,因为CPU只能同时执行固定数量的线程数,一旦同时并发的线程数量超过CPU能执行的阈值,CPU就需要花费时间来判断线程的优先级。每开一个新的线程,都会耗费至少64K+的内存。

Java里面线程池的顶级接口是java.util.concurrent.Executor,但是严格意义上讲Executor并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口java.util.concurrent.ExecutorService。

要配置一个线程池是比较复杂的,如果对于线程池的原理不是很清楚的情况下,很有可能配置的线程池不是较优的,因此在java.util.concurrent.Executors线程工厂类里面提供了一些静态工厂,生成一些常用的线程池。官方建议使用Executors工程类来创建线程池对象,Executors是Executor,ExecutorService, ScheduledExecutorService,ThreadFactory,Callable的工具和工厂类。

如果是创建ThreadPoolExecutor,Executors调用newFixedThreadPool、newSingleThreadExecutor和newCachedThreadPool等方法创建线程池均是ThreadPoolExecutor类型。无论调用Executors类的何种创建方法,最终都会调用ThreadPoolExecutor类的构造函数。

代码如下:

/**
 *
 * @param corePoolSize 保留在池中的线​​程数,即使*处于空闲状态,
 *                     除非设置了{@code allowCoreThreadTimeOut}
 * @param maximumPoolSize 池中允许的最大线程数
 * @param keepAliveTime 当线程数大于核心时,这是多余的空闲线程在终止之前等待新任务的最长时间。
 * @param unit {@code keepAliveTime}参数的时间单位
 * @param workQueue 在执行任务之前用于保留任务的队列。该队列将仅保存由{@code execute}方法提交的{@code Runnable} *任务。
 * @param threadFactory 执行程序创建新线程时要使用的工厂
 * @param handler 当执行被阻塞时要使用的处理程序,因为达到了线程界限和队列容量,注意它并不是一个我们常见的Handler
 * @throws IllegalArgumentException 如果满足下列条件之一,则报IllegalArgumentException异常:
 *         {@code corePoolSize < 0}<br>
 *         {@code keepAliveTime < 0}<br>
 *         {@code maximumPoolSize <= 0}<br>
 *         {@code maximumPoolSize < corePoolSize}
 * @throws NullPointerException  如果 workQueue、threadFactory、handler之一为空,则报NullPointerException异常
 */
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) {
}

四、构造函数详解

参数说明
corePoolSize核心线程数。当线程数小于该值时,线程池会优先创建新线程来执行新任务
maximumPoolSize线程池所能维护的最大线程数
keepAliveTime空闲线程的存活时间
unit参数keepAliveTime空闲线程的存活时间的单位 这是一个枚举类
workQueue任务队列,用于缓存未执行的任务
threadFactory线程工厂。可通过工厂为新建的线程设置更有意义的名字
handler拒绝策略。当线程池和任务队列均处于饱和状态时,使用拒绝策略处理新任务。默认是 AbortPolicy,即直接抛出异常

1. corePoolSize
默认情况下线程池是空的,只是任务提交时才会创建线程。如果当前运行的线程数大于corePoolSize,线程看下情况创建。如果调用线程池的prestartAllcoreThread方法,线程池会提前创建并启动所有的核心线程来等待任务。

2. maximumPoolSize
如果任务队列满了并且线程数小于maximumPoolSize时,则线程池仍然会创建新的线程来处理任务

3. keepAliveTime
当线程数大于核心时,这是多余的空闲线程在终止之前等待新任务的最长时间,超过这时间则回收。

<如果任务很多,并且每个任务的执行时间很短,则可以调大keepAliveTime来提高线程的利用率。另外,如果设置allowCoreThreadTimeOut属性来true时,keepAliveTime也会应用到核心线程上。

4. TimeUnit
keepAliveTime参数的时间单位。可选的单位有天Days、小时HOURS、分钟MINUTES、秒SECONDS、毫秒MILLISECONDS等。

5. workQueue
在执行任务之前用于保留任务的队列。该队列将仅保存由 execute 方法提交的 Runnable任务。该任务队列是BlockingQueue类型的,即阻塞队列。

6. ThreadFactory
可以使用线程工厂给每个创建出来的线程设置名字。一般情况下无须设置该参数,默认使用DefaultThreadFactory。

7. RejectedExecutionHandler
拒绝策略。当执行被阻塞时要使用的处理程序,因为达到了线程界限和队列容量,注意它并不是一个我们常见的Handler,默认是AbordPolicy。它的几种策略方式在下一篇博客中会详细说明。

ThreadPoolExecutor的构造参数需要满足一定的条件:

  1. <如果出现如下其中一种情况,则构建中抛IllegalArgumentException异常
    1. corePoolSize < 0
    2. keepAliveTime < 0
    3. maximumPoolSize <= 0
    4. maximumPoolSize < corePoolSize
  2. <如果出现如下其中一种情况,则构建过程中抛NullPointerException异常
    1. workQueue is null
    2. threadFactory is null
    3. handler is null

五、常见几种线程池

通过直接或间接的配置ThreadPoolExecutor的参数可以创建不同类型的ThreadPoolExecutor。Executors提供了多种创建线程池的方法,其中有5种线程池比较常用:newFixedThreadPool、CachedThreadPool、SingleThreadExecutor、ScheduledTheadPool。

1. newFixedThreadPool

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

从构造方法中可以看出corePoolSize=maximumPoolSize=nThreads,FixedThreadPool是可重用固定核心线程数的线程池,控制线程最大并发数
其特点是:

  1. 只有核心线程,并且数量是固定的,没有非核心线程
  2. keepAliveTime=0L,但是由于无非核心线程,该参数无效
  3. 任务队列默认采用无界的阻塞队列LinkedBlockingQueue,无大小限制
  4. 线程工厂可以自行定义,默认使用DefaultThreadFactory

2. CachedThreadPool

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

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

从构造方法中可以看出CachedThreadPool是一个根据需要自动创建线程的线程池。它有如下几个特点:

  1. corePoolSize=0,无核心线程
  2. maximumPoolSize=Integer.MAX_VALUE,非核心线程数原则上可以有20多亿个。
  3. KeepAliveTime=60s,线程空闲60s后将被回收,空闲线程灵活回收
  4. 任务队列使用的是SynchronousQueue,和LinkedBlockingQueue不同,它是一个不存储元素的阻塞队列。后面章节会对它详细描述。

根据其特点知道CachedThreadPool比较适于大量的需要立即处理并且耗时较少的任务,且CachedThreadPool使用SynchronousQueue队列,线程任务即提交即执行,无需等待


3. newSingleThreadExecutor

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

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

从构建方法中可以看出newSingleThreadExecutor是使用单个工作线程的线程池,其corePoolSize和maximumPoolSize都为1。它的特点是:

  1. 只有一个核心线程,无非核心线程,任务按照指定顺序在一个线程中执行
  2. 其他参数和FixedThreadPool的一样,可以把它看作是Executors.newSingleThreadExecutor(1)的线程池。

newSingleThreadExecutor线程池一般用来进行数据库操作、文件操作或者打印操作等应用场景,但不适合可能引起IO阻塞及影响UI线程响应等操作


4. ScheduledTheadPool

 public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
        return new ScheduledThreadPoolExecutor(corePoolSize);
    }
 public ScheduledThreadPoolExecutor(int corePoolSize) {
        super(corePoolSize, Integer.MAX_VALUE,
              DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
              new DelayedWorkQueue());
    }
  public static ScheduledExecutorService newScheduledThreadPool(
            int corePoolSize, ThreadFactory threadFactory) {
        return new ScheduledThreadPoolExecutor(corePoolSize, threadFactory);
    }
   public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             threadFactory, defaultHandler);
    }

ScheduledThreadPoolExecutor是继承自ThreadPoolExecutor,实现了ScheduledExecutorService接口,它是一种特殊的线程池专用于实现定和周期性任务的线程池。根据其构造参数能看出如下特点:

  1. 核心线程自定义控制
  2. 非核心线程数可以达到Integer.MAX_VALUE
  3. 非核心线程的空闲保活时长是10ms
  4. 任务队列使用的是DelayedWorkQueue,是一种无界队列

5. SingleThreadScheduledExecutor

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

等价于 newScheduledThreadPool(1)


六、线程池使用流程

线程池的使用流程很简单,分三步走,创建线程池、提交任务、关闭线程池。

1. 创建线程池
创建时,通过配置线程池的参数,从而实现自己所需的线程池

   Executor threadPool = new ThreadPoolExecutor(
                                              CORE_POOL_SIZE,
                                              MAXIMUM_POOL_SIZE,
                                              KEEP_ALIVE,
                                              TimeUnit.SECONDS,
                                              sPoolWorkQueue,
                                              sThreadFactory
                                              );

2. 提交任务
execute和submit两种提交线程的方式,其详细分析请阅读下一篇博文

      threadPool.execute(new Runnable() {
            @Override
            public void run() {
                ... // 线程执行任务
            }
        });
       threadPool.submit(new Runnable() {
           @Override
           public void run() {
                ... // 线程执行任务
           }
       });

3. 关闭线程池
shutdown和shutdownNow两种关闭线程池的方法,其具体区别和源码分析请阅读下一篇博文。

threadPool.shutdown();
threadPool.shutdownNow();

注意:
在开发过程中线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。

这里进行讲解是为了帮助大家理解各种线程池的特性和构造,真实场景中老老实实用new ThreadPoolExecutor(…)的方式来创建线程池。因为使用Executors提供的方法返回的ThreadPoolExecutor有如下弊端:

  1. FixedThreadPool 和SingleThreadPool :使用的是LinkedBlockingQueue无界队列,允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM
  2. CachedThreadPool 和ScheduledThreadPool :允许的创建线程数量为
    Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM。

总结

  1. newFixedThreadPool 和 newSingleThreadExecutor: 主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至 OOM。
  2. newCachedThreadPool 和 newScheduledThreadPool: 主要问题是线程数最大数是 Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至 OOM。

四种线程可以多多配合使用,其中Okhttp中线程池的实现和有特色,方便且安全,还避免上面两点的尴尬。

本篇博客主要从线程池的意义,优势,如何配参构建等方面讲解,该篇博文学习完成后你就已基本能掌握线程池的使用。

但是对于线程池的进阶深度学习,请继续阅读我的下一篇博客 《线程池从零认识到深层理解——进阶》中将进行详细介绍。


相关链接

  1. Thread线程从零认识到深层理解——初识
  2. Thread线程从零认识到深层理解——六大状态
  3. Thread线程从零认识到深层理解——wait()与notify()
  4. Thread线程从零认识到深层理解——线程安全
  5. 线程池从零认识到深层理解——初识
  6. 线程池从零认识到深层理解——进阶

扩展链接:

  1. Android CameraX 使用入门
  2. Android Studio 4.0新特性及升级异常

博客书写不易,您的点赞收藏是我前进的动力,千万别忘记点赞、 收藏 ^ _ ^ !

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值