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

线程基础

线程创建

Java创建线程的两种方式:

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

线程生命周期

image.png

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

线程同步

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看到的:

image

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

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

线程注意事项

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

总结

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


轻口味
16.9k 声望3.9k 粉丝

移动端十年老人,主要做IM、音视频、AI方向,目前在做鸿蒙化适配,欢迎这些方向的同学交流:wodekouwei