yunshuipiao/Potato

Android Multi Thread

Opened this issue · 0 comments

Android Multi Thread

[TOC]

这篇文章了解一下 多线程 相关的知识。

线程

操作系统的设计者 巧妙地利用了时间片轮转的方式, CPU给每个任务都服务一定的时间,然后把当前任务的状态保存下来,在加载下一任务的状态后,继续服务下一任务任务的状态保存及再加载, 这段过程就叫做上下文切换。时间片轮转的方式使多个任务在同一颗CPU上执行变成了可能。

线程上下文切换的原因

  1. 当前执行任务的时间片用完之后,系统CPU正常调度下一个任务;
  2. 当前执行任务碰到IO阻塞,调度器将此任务挂起,继续下一任务;
  3. 多个任务抢占锁资源,当前任务没有抢到锁资源,被调度器挂起,继续下一任务;
  4. 用户代码挂起当前任务,让出CPU时间;
  5. 硬件中断;

线程池

由于频繁的创建/销毁线程,对内存的消耗很严重,这是就需要线程池。其优点在于:

  • 重用线程池中的线程,避免频繁创建和销毁线程所带来的内存损耗
  • 有效控制线程的最大并发数量,防止线程过大导致抢占资源,系统阻塞,甚至系统崩溃。
  • 对线程进行管理

线程池属于对象池.所有对象池都具有一个非常重要的共性,就是为了最大程度复用对象.那么线程池的最重要的特征也就是最大程度利用线程。

首先,创建线程本身需要额外(相对于执行任务而必须的资源)的开销.
作业系统在每创建一个线程时,至少需要创建以下资源:
(1) 线程内核对象:用于对线程上下文的管理.
(2) 用户模式执行栈.
(3) 内核模式执行栈.
这些资源被线程占有后作业系统和用户都无法使用.
相反的过程,销毁线程需要回收资源,也需要一定开销.
其次,过多的线程将导致过度的切换.线程切换带来的性能更是不可估量.系统完成线程切换要经过以下过程:
(1) 从用户模式切换到内核模式.
(2) 将CPU寄存器的值保存到当前线程的内核对象中.
(3)打开一个自旋锁,根据调度策略决定下一个要执行的线程.释放自旋锁,如果要执行的线程不是同一进程中的线程,还需要切换虚拟内存等进程环境.
(4) 将要执行的线程的内核对象的值写到CPU寄存器中.
(5) 切换到用户模式执行新线程的执行逻辑.
所以线程池的目的就是为了减少创建和切换线程的额外开销,利用已经的线程多次循环执行多个任务从而提高系统的处理能力。

ThreadPoolExecutor

ExecutorService 是最初的线程池接口,而 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();
  	// 核心线程,无任务运行也存在。如果 allowCoreThreadTimeOut 为 true 也会存在超时策略。
    this.corePoolSize = corePoolSize;
  	// 最大线程数:当任务超过核心线程并且 workQueue 已经满时创建,用完销毁。
    this.maximumPoolSize = maximumPoolSize;
  	// 线程池任务队列
    this.workQueue = workQueue;
  	// 非核心线程的超时时间。超时则被回收。
    this.keepAliveTime = unit.toNanos(keepAliveTime);
    this.threadFactory = threadFactory;
    this.handler = handler;
}

线程池的分配遵循这样的规则:

  • 当线程池中的最大线程数量未达到核心线程数时,启动一个核心线程去执行任务;
  • 如果线程池中的最大线程数量达到核心线程数时,那么任务会被插入到任务队列中排队等待执行;
  • 如果在上一步骤中任务队列已满但是线程池中线程数量未达到最大线程总数,那么启动一个非核心线程来处理任务;(非核心=最大-核心)
  • 如果上一步骤中线程数量达到了限定线程总量,那么线程池则拒绝执行该任务,且ThreadPoolExecutor会调用RejectedtionHandler的rejectedExecution方法来通知调用者。

线程池的分类

针对不同情况,一般需要不同的线程池策略,一般情况下有四种:

  • FixedThreadPool:固定核心线程数,响应快,不用担心被回收。
  • CachedThreadPool:核心线程为0,最大数线程的线程池。适合执行大量耗时小的任务任务,由于超时时间为 60s,理论上不会占用系统的无用线程。
  • ScheduledThreadPool:上述两种的合集,使用与执行定时任务和固定周期的重复任务。
  • SingleThreadExector:内部只有一个核心线程,确保所有的任务都排队按顺序执行。常用于 IO 读写的情况。

核心线程的数量

合理设置线程数目,关键点是:1. 尽量减少线程切换和管理的开支;2. 最大化利用CPU

对于1,要求线程数尽量少,这样可以减少线程切换和管理的开支;

对于2,要求尽量多的线程,以保证CPU资源最大化的利用;

所以 对于任务耗时短的情况,要求线程尽量少,如果线程太多,有可能出现线程切换和管理的时间,大于任务执行的时间,那效率就低了;

对于耗时长的任务,要分是CPU任务,还是IO等类型的任务。如果是CPU类型的任务,线程数不宜太多;但是如果是IO类型的任务,线程多一些更好,可以更充分利用CPU。

高并发,低耗时的情况:建议少线程,只要满足并发即可,因为上下文切换本来就多,并且高并发就意味着CPU是处于繁忙状态的, 增加更多地线程也不会让线程得到执行时间片,反而会增加线程切换的开销;例如并发100,线程池可能设置为10就可以;

低并发,高耗时的情况:建议多线程,保证有空闲线程,接受新的任务;例如并发10,线程池可能就要设置为20;