[Android多线程-1] 线程池

248 阅读7分钟

进程和线程

  1. 进程和进程之间数据是隔离的,线程之间可以访问共享内存,访问对方的数据。

  2. 线程是在进程里创建的,一个进程里可以创建多个线程。

线程池

  1. 对于线程的新建和销毁,资源开销比较大,线程池可以用来管理线程,对于新来的线程请求,直接复用旧线程,防止新的开销
  2. 可以管理线程执行的先后顺序
  3. 可以管理线程什么时候执行,比如延后多久执行

    线程池的创建离不开 ThreadExecutor,那么它是怎么维护线程的呢?

ThreadExecutor原理

基本知识
  • ctl 变量

高3位表示线程池状态,低29位表示Worker数量。使用CAS机制来更新和获取。

  • Worker

    线程池中维护的一个集合,用于执行线程,每个Worker内部维护了一个Thread对象,Worker会不停从Runnable队列中获取Runnable,然后去执行。 Worker本身也实现了Runnable接口,所以Thread执行时候,其实就是直接执行它自己。

  • 线程池的5种状态
  1. RUNNING
    线程池会接收新的Runnable请求,并且会执行阻塞队列中的Runnable
  2. SHUTDOWN
    线程池停止接收新的Runnable请求,但是会依然执行阻塞队列中的Runnable
  3. STOP
    线程池决绝接收新的Runnable,并且不会执行阻塞队列中的Runnable,尝试中断正在执行的任务。
  4. TIDYING
    当前所有任务被关闭了,Worker数量为0
  5. TERMINATED
    terminated()执行结束。
流程研究

    我们使用线程池,主要是调用其方法execute来执行Runnable,关于execute的具体流程是什么呢?

ThreadExecutor源码分析.png

总结来说:

  1. ThreadPoolExecutor内部维护了两个集合,一个是用于处理Runnable的Worker集合,一个是等待被执行的Runnable队列。
  2. 对于Worker,每个Worker自身就是一个Runnable,线程池分配线程,其实就是通过分配Worker对象,通过线程启动Worker,在Worker的run方法中,不停的获取Runnable对象,执行其run方法。
  3. 对于execute方法,会判断当前已经运行的Worker对象是否超出定义的核心线程数,如果没超出,则新建一个Worker对象,执行Runnable,如果超出了,那么放到阻塞队列中。

相关问题

  • Worker本身为Runnable的原因

    Worker自己实现了Runnable接口,主要是因为Thread执行时候是直接执行一个Runnable的,执行结束,线程也就结束了。但是线程池是需要复用Thread的,那么该怎么复用呢?     那么是不是可以在Thread start后,在run方法中,依次的执行Runnable 的run方法。是不是就可以实现了呢?Worker就是这个原理,通过让自己跑起来,然后在自己的run方法中,不停的查找等待的Runnable,然后拿出去执行,这样可以保证当前Thread不会因为run执行完毕而线程结束,达到了线程的复用效果。

  • 当等待队列为空了。Worker会回收置为NULL吗?
  • keepAliveTime 和 allowCoreThreadTimeOut方法关系

上方两个问题需要放到一起来研究。

  1. 当我们调用方法 allowCoreThreadTimeOut 设置允许核心线程超时后,回收线程。这个方法会配合 实例化ThreadExecutor对象时候的入参变量keepAliveTime来使用。

    当这个方法我们主动设置为TRUE后(默认是false),当等待队列为空,且当前Worker没有正在运行的Runnable,那么等待keepAliveTime时间一到,就会回收Worker,将其置为NULL,否则 则会阻塞在队列读取这里,等待队列返回下一个Runnable。

  1. 如果我们没有调用方法 allowCoreThreadTimeOut,那么也会等待keepAliveTime时间一到,回收Worker,但是这里就不是回收所有的worker了,而是会剩下数量为 corePoolSize 的Worker数持续运行着,等待执行Runnable,多余出来的Worker会被回收。

    所以对于超出corePoolSize数量的Worker一定会被回收,对于corePoolSize数量范围内的Worker,除非我们主动调用方法 allowCoreThreadTimeOut 允许其被回收,才会触发核心线程Worker的回收机制。

  • corePoolSize 和 maximumPoolSize 的区别关系

    corePoolSize代表核心线程数,也就是当前同时可以有多少个线程运行。当线程数目超出了这个值后,会将多余的Runnable放入到等待队列中。

    当等待队列满了后,会判断当前正在执行的线程数(Worker数)是否大于 maximumPoolSize,如果是直接抛出reject异常,表示不再接收Runnable,     如果否,那么立即新建一个Worker来执行当前Runnable。

    所以,这里可以看到当线程池等待队列满了后,后续来的新Runnable请求反而可以优先被执行。

    同时,当我们使用无界队列时候,maximumPoolSize 也就没有意义了。

  • corePoolSize 设置多少合适

    [从网上搜索的数据]一般对于IO密集型(网络请求,文件读写等),推荐是2N,对于CPU密集型(计算处理比较多),推荐是N+1,N为CPU核数

Android 中可以直接使用的有四种类型的线程池

  1. FixThreadPool

    固定大小的线程池,corePoolSize 和 maximumPoolSize相等。对于请求的线程,先看当前是否有空闲的线程,如果有直接复用运行,如果没有那么将其放到等待队列中。 线程等待使用队列为 LinkedBlockingQueue.

  1. SingleThreadPool

    可运行线程数大小固定为1的线程池,corePoolSize 和 maximumPoolSize 都固定为1.同一时刻只可以运行一个线程,多余Runnable进入到等待队列,使用队列为 LinkedBlockingQueue

  1. CachedThreadPool

    无固定大小的线程池,来的Runnable可以理解创建Worker进行执行,并且超时后,可以回收Worker对象, corePoolSize为0,maximumPoolSize为Int最大值。使用队列为SynchronousQueue

  1. ScheduleThreadPool

    可以定义Runnable执行时间的线程池,corePoolSize默认是1,可以自定义,maximumPoolSize为Int最大值。使用队列为DelayedWorkQueue

队列类型

    ThreadPoolExecutor允许传入的等待队列需要实现 BlockingQueue 接口,目前系统中已经存在实现该接口的如下队列供我们使用。

下方所有队列均线程安全,使用锁ReentrantLock来保证线程安全。
  • LinkedBlockingQueue

    内部以Node链表保存数据,链表大小为Int最大值,相当于无上限。当添加元素时候,超出链表长度会抛出异常。获取数据Node对象时候,如果队列为空,可以阻塞直到队列非空,返回Node对象。

    该队列先入先出。队列使用了ReentrantLock 锁保证了线程之间的安全性。当链表满了后,存放值也会直接阻塞,直到链表有数据释放。

  • ArrayBlockingQueue

    内部维护了个数组,和两个int值,分别代表取值的下标和存值的下标,当数组存满后,下次再存值会阻塞,直到数组出现空余位置,才可以插入成功。取值时候也可以设置阻塞取值,即没值时候阻塞等待。

  • DelayedWorkQueue

    无界队列,内部维护了一个数组,数组中元素数据结构为堆排序,以执行时间节点为准进行的堆排序。取值时候,如果数组为空,则阻塞等待。

  • SynchronousQueue

    该队列不会存储元素,但是提供了数据插入和取出的阻塞机制。对于插入一条数据,在插入第二条数据时候,会进行阻塞等待,直到第一条数据被取出,同理取数据时候,如果没有数据也会阻塞等待,直接有数据被插入。

线程

  • 线程如何同时开始

    使用CountDownLatch.在每个线程的run方法中加上CountDownLatch的await方法,然后再开始时间点调用其countDown,这样可以实现所有线程同时开始执行。

  • 线程如何顺序执行
  1. 使用CountDownLatch来限制,但是线程数量一多,可能就得定义对应数目减一的CountDownLatch,每个CountDownLatch的容量为1.
  2. 使用同步锁
  3. 使用join