写点什么

Android 线程思考

用户头像
轻口味
关注
发布于: 刚刚
Android线程思考

在编程中我们经常遇到多线程相关的问题,记得刚工作的时候对线程没有太多概念,只知道new Thread()run 函数中是新的线程,函数多调用几层,特别是一些别人的回调函数中,就忽略了线程引起的并发问题,产生了并发修改异常的崩溃。今天总结一些线程相关的知识。

线程基础

线程创建

Java 创建线程的两种方式:


  1. new Thread(){}.start();

  2. new Thread(new Runnable(){}).start();

线程生命周期


新建-就绪-运行-阻塞-死亡。

线程同步

Syncronized 关键字

  1. 无论 synchronized 关键字加在方法上还是对象上,如果它作用的对象是非静态的,则它取得的锁是对象;如果 synchronized 作用的对象是一个静态方法或一个类,则它取得的锁是对类,该类所有的对象同一把锁。

  2. 每个对象只有一个锁(lock)与之相关联,谁拿到这个锁谁就可以运行它所控制的那段代码。

  3. 实现同步是要很大的系统开销作为代价的,甚至可能造成死锁,所以尽量避免无谓的同步控制。

线程同步手段

  • AsyncTask

  • runOnUiThread

  • Handler

  • View.post(Runnable r)

线程池

什么是线程池?

线程池是一种多线程处理形式,处理过程中将任务提交到线程池,任务的执行交由线程池来管理。 如果每个请求都创建一个线程去处理,那么服务器的资源很快就会被耗尽,使用线程池可以减少创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。 java.util.concurrent.Executors 提供了一个 java.util.concurrent.Executor 接口的实现用于创建线程池

为什么要使用线程池?

创建线程和销毁线程的花销是比较大的,这些时间有可能比处理业务的时间还要长。这样频繁的创建线程和销毁线程,再加上业务工作线程,消耗系统资源的时间,可能导致系统资源不足。(我们可以把创建和销毁的线程的过程去掉)


多线程技术主要解决处理器单元内多个线程执行的问题,它可以显著减少处理器单元的闲置时间,增加处理器单元的吞吐能力。 假设一个服务器完成一项任务所需时间为:T1 创建线程时间,T2 在线程中执行任务的时间,T3 销毁线程时间。


如果:T1 + T3 远大于 T2,则可以采用线程池,以提高服务器性能。 一个线程池包括以下四个基本组成部分:


  1. 线程池管理器(ThreadPool):用于创建并管理线程池,包括 创建线程池,销毁线程池,添加新任务;

  2. 工作线程(PoolWorker):线程池中线程,在没有任务时处于等待状态,可以循环的执行任务;

  3. 任务接口(Task):每个任务必须实现的接口,以供工作线程调度任务的执行,它主要规定了任务的入口,任务执行完后的收尾工作,任务的执行状态等;

  4. 任务队列(taskQueue):用于存放没有处理的任务。提供一种缓冲机制。

线程池有什么作用?

线程池作用就是限制系统中执行线程的数量


  1. 提高效率 创建好一定数量的线程放在池中,等需要使用的时候就从池中拿一个,这要比需要的时候创建一个线程对象要快的多。

  2. 方便管理 可以编写线程池管理代码对池中的线程同一进行管理,比如说启动时有该程序创建 100 个线程,每当有请求的时候,就分配一个线程去工作,如果刚好并发有 101 个请求,那多出的这一个请求可以排队等候,避免因无休止的创建线程导致系统崩溃。

线程池原理

Java 通过 Executors 提供四种线程池


  • CachedThreadPool():可缓存线程池。如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。比较适合处理执行时间比较小的任务

  • FixedThreadPool():定长线程池。可控制线程最大并发数,超出的线程会在队列中等待。可以用于已知并发压力的情况下,对线程数做限制。

  • ScheduledThreadPool():定时线程池。支持定时及周期性任务执行。适用于需要多个后台线程执行周期任务的场景

  • SingleThreadExecutor():单线程化的线程池。它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。可以用于需要保证顺序执行的场景,并且只有一个线程在执行

使用 ThreadPoolExecutor 自定义的线程池

阿里巴巴 Java 开发手册,明确指出不允许使用上述 Executors 静态工厂构建线程池 原因如下:线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险,同时 Executors 返回的线程池对象的弊端如下:


  1. FixedThreadPool 和 SingleThreadPool:允许的请求队列(底层实现是 LinkedBlockingQueue)长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM

  2. CachedThreadPool 和 ScheduledThreadPool: 允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。

ThreadPoolExecutor 创建

避免使用 Executors 创建线程池,主要是避免使用其中的默认实现,那么我们可以自己直接调用 ThreadPoolExecutor 的构造函数来自己创建线程池。在创建的同时,给 BlockQueue 指定容量就可以了。


  private static ExecutorService executor = new ThreadPoolExecutor(10, 10,      60L, TimeUnit.SECONDS,      new ArrayBlockingQueue(10));   
复制代码



或者是使用开源类库:开源类库,如 apache 和 guava 等。

ThreadPoolExecutor 的执行流程

  1. 线程数量未达到 corePoolSize,则新建一个线程(核心线程)执行任务。

  2. 线程数量达到了 corePools,则将任务移入队列等待。

  3. 队列已满,新建线程(非核心线程)执行任务。

  4. 队列已满,总线程数又达到了 maximumPoolSize,就会由(RejectedExecutionHandler)抛出异常(拒绝策略)

  5. 新建线程->达到核心数->加入队列->新建线程(非核心)->达到最大数->触发拒绝策略

ThreadPoolExecutor 参数说明

  1. corePoolSize:核心池的大小,这个参数跟后面讲述的线程池的实现原理有非常大的关系。在创建了线程池后,默认情况下,线程池中并没有任何线程,而是等待有任务到来才创建线程去执行任务,除非调用了 prestartAllCoreThreads()或者 prestartCoreThread()方法,从这 2 个方法的名字就可以看出,是预创建线程的意思,即在没有任务到来之前就创建 corePoolSize 个线程或者一个线程。默认情况下,在创建了线程池后,线程池中的线程数为 0,当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到 corePoolSize 后,就会把到达的任务放到缓存队列当中。

  2. maximumPoolSize:线程池最大线程数,这个参数也是一个非常重要的参数,它表示在线程池中最多能创建多少个线程;如果当前阻塞队列满了,且继续提交任务,则创建新的线程执行任务,前提是当前线程数小于 maximumPoolSize;当阻塞队列是无界队列,则 maximumPoolSize 不起作用,因为无法提交至核心线程池的线程会一直持续地放入 workQueue(工作队列)中。

  3. keepAliveTime:表示线程没有任务执行时最多保持多久时间会终止。默认情况下,只有当线程池中的线程数大于 corePoolSize 时,keepAliveTime 才会起作用,直到线程池中的线程数不大于 corePoolSize,即当线程池中线程数大于 corePoolSize 时,如果一个线程空闲的时间达到 keepAliveTime,则会终止,直到线程池中的线程数不超过 corePoolSize。但是如果调用了 allowCoreThreadTimeOut(boolean)方法,在线程池中的线程数不大于 corePoolSize 时,keepAliveTime 参数也会起作用,直到线程池中的线程数为 0。

  4. allowCoreThreadTimeout:默认情况下超过 keepAliveTime 的时候,核心线程不会退出,可通过将该参数设置为 true,让核心线程也退出。

  5. unit:可以指定 keepAliveTime 的时间单位。

  6. workQueue


  • ArrayBlockingQueue 是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序。需要指定队列大小。

  • LinkedBlockingQueue 若指定大小则和 ArrayBlockingQueue 类似,若不指定大小则默认能存储 Integer.MAX_VALUE 个任务,相当于无界队列,此时 maximumPoolSize 值其实是无意义的。此队列按 FIFO (先进先出) 排序元素,吞吐量通常要高于 ArrayBlockingQueue。静态工厂方法 Executors.newFixedThreadPool()使用了这个队列

  • SynchronousQueue 同步阻塞队列,当有任务添加进来后,必须有线程从队列中取出,当前线程才会被释放,newCachedThreadPool 就使用这种队列。

  • PriorityBlockingQueue 一个具有优先级的无限阻塞队列。

  • RejectedExecutionHandler:线程数和队列都满的情况下,线程池会执行的拒绝策略,有四个(也可以使用自定义的策略)。

  • AbortPolicy:不执行新任务,直接抛出异常,提示线程池已满,线程池默认策略。

  • DiscardPolicy:不执行新任务,也不抛出异常,基本上为静默模式。

  • DisCardOldSetPolicy:将消息队列中的第一个任务替换为当前新进来的任务执行。

  • CallerRunPolicy:拒绝新任务进入,如果该线程池还没被关闭,那么这个新的任务在执行线程中被调用。

  • Executors 和 ThreadPoolExecutor 创建线程的区别

如何向线程池中提交任务

可以通过 execute()或 submit()两个方法向线程池提交任务。


  • execute()方法没有返回值,所以无法判断任务知否被线程池执行成功。

  • submit()方法返回一个 future,那么我们可以通过这个 future 来判断任务是否执行成功,通过 future 的 get 方法来获取返回值。

如何关闭线程池

可以通过 shutdown()或 shutdownNow()方法来关闭线程池。


  • shutdown 的原理是只是将线程池的状态设置成 SHUTDOWN 状态,然后中断所有没有正在执行任务的线程。

  • shutdownNow 的原理是遍历线程池中的工作线程,然后逐个调用线程的 interrupt 方法来中断线程,所以无法响应中断的任务可能永远无法终止。shutdownNow 会首先将线程池的状态设置成 STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表。

初始化线程池时线程数的选择

  • 如果任务是 IO 密集型,一般线程数需要设置 2 倍 CPU 数以上,以此来尽量利用 CPU 资源。

  • 如果任务是 CPU 密集型,一般线程数量只需要设置 CPU 数加 1 即可,更多的线程数也只能增加上下文切换,不能增加 CPU 利用率。


上述只是一个基本思想,如果真的需要精确的控制,还是需要上线以后观察线程池中线程数量跟队列的情况来定。

线程优先级

Linux 中,使用 nice value(以下成为 nice 值)来设定一个进程的优先级,系统任务调度器根据 nice 值合理安排调度。


nice 的取值范围为-20 到 19。 通常情况下,nice 的默认值为 0。视具体操作系统而定。 nice 的值越大,进程的优先级就越低,获得 CPU 调用的机会越少,nice 值越小,进程的优先级则越高,获得 CPU 调用的机会越多。 一个 nice 值为-20 的进程优先级最高,nice 值为 19 的进程优先级最低。 父进程 fork 出来的子进程 nice 值与父进程相同。父进程 renice,子进程 nice 值不会随之改变。


由于 Android 基于 Linux Kernel,在 Android 中也存在 nice 值。但是一般情况下我们无法控制,原因如下:


Android 系统并不像其他 Linux 发行版那样便捷地使用 nice 命令操作。 renice 需要 root 权限,一般应用无法实现。


Android 中的线程优先级别目前规定了如下,了解了进程优先级与 nice 值的关系,那么线程优先级与值之间的关系也就更加容易理解。


  • THREAD_PRIORITY_DEFAULT,默认的线程优先级,值为 0。

  • THREAD_PRIORITY_LOWEST,最低的线程级别,值为 19。

  • THREAD_PRIORITY_BACKGROUND 后台线程建议设置这个优先级,值为 10。

  • THREAD_PRIORITY_FOREGROUND 用户正在交互的 UI 线程,代码中无法设置该优先级,系统会按照情况调整到该优先级,值为-2。

  • THREAD_PRIORITY_DISPLAY 也是与 UI 交互相关的优先级界别,但是要比 THREAD_PRIORITY_FOREGROUND 优先,代码中无法设置,由系统按照情况调整,值为-4。

  • THREAD_PRIORITY_URGENT_DISPLAY 显示线程的最高级别,用来处理绘制画面和检索输入事件,代码中无法设置成该优先级。值为-8。 THREAD_PRIORITY_AUDIO 声音线程的标准级别,代码中无法设置为该优先级,值为 -16。

  • THREAD_PRIORITY_URGENT_AUDIO 声音线程的最高级别,优先程度较 THREAD_PRIORITY_AUDIO 要高。代码中无法设置为该优先级。值为-19。

  • THREAD_PRIORITY_MORE_FAVORABLE 相对 THREAD_PRIORITY_DEFAULT 稍微优先,值为-1。

  • THREAD_PRIORITY_LESS_FAVORABLE 相对 THREAD_PRIORITY_DEFAULT 稍微落后一些,值为 1。


使用 Android API 为线程设置优先级也很简单,只需要在线程执行时调用 android.os.Process.setThreadPriority 方法即可。这种在线程运行时进行修改优先级,效果类似 renice。

Android 应用程序包含线程

我们创建一个只有一个页面一个按钮的 android 应用,启动时会产生几个线程呢?这些线程分别是做什么?


我们可以想到的有:


  • 主线程

  • 6.0 开始有了渲染线程

  • gc 线程 回收守护线程, 回收监控线程

  • binder 线程池 4 个线程

  • JVM agent *2


看看通过 AndroidStudio profile 看到的:



像 Profile Saver 猜测是性能检测工具注入的。其它的我们可以带着问题从 framework 中寻找。


之前做电视项目的时候遇到了录音丢帧问题,最后定位到是因为 CPU 打满,录音线程被阻塞引起。为了解决问题首先想到的是提升录音线程优先级,但是不管调用 Android 哪个录音 API 系统都会为应用分配一个 AudioRecorder 线程,我们无法修改这个线程的优先级,而且 AudioRecorder 线程本身优先级就是-19,已经很高了。所以后续的优化思路只能是整个 APP 层面性能优化。

线程注意事项

我们不管是在写代码还是阅读别人代码时,要经常思考所看的方法是运行在哪个线程,避免多线程并发引起的问题。在我们做架构设计或者 SDK 设计时要考虑对外暴露的接口的线程安全性。

总结

本文总结了线程的基础知识,以及线程池,线程优先级相关的东西,并且介绍了一个最简单 APP 所包含的线程及作用。

发布于: 刚刚阅读数: 2
用户头像

轻口味

关注

🏆2021年InfoQ写作平台-签约作者 🏆 2017.10.17 加入

Android音视频、AI相关领域从业者,开源RTMP播放器:https://github.com/qingkouwei/oarplayer

评论

发布
暂无评论
Android线程思考