前言
本文已经收录到我的 GitHub 个人博客, 欢迎大佬们光临寒舍:
我的 GitHub 博客 https://lovelifeeveryday.github.io/
需要已经具备的知识:
Thread 的基本概念及使用
AsyncTask 的基本概念及使用
学习导图:
一. 为什么要学习 Thread?
在 Android 中, 几乎完全采用了 Java 中的线程机制. 线程是最小的调度单位, 在很多情况下为了使 App 更加流程地运行, 我们不可能将很多事情都放在主线程上执行, 这样会造成严重卡顿(ANR), 那么这些事情应该交给子线程去做, 但对于一个系统而言, 创建, 销毁, 调度线程的过程是需要开销的, 所以我们并不能无限量地开启线程, 那么对线程的了解就变得尤为重要了.
本篇文章将带领大家由浅入深, 从线程的基础, 谈到同步机制, 再讲到阻塞队列, 接着提及 Android 中的线程形态, 最终一览线程池机制.
话不多说, 赶紧开始奇妙的 Thread 之旅吧!
二. 核心知识点归纳
2.1 线程概述
Q1: 含义
线程是 CPU 调度的最小单位
注意与进程相区分
Q2: 特点
线程是一种受限的系统资源. 即线程不可无限制的产生且线程的创建和销毁都有一定的开销
Q: 如何避免频繁创建和销毁线程所带来的系统开销?
A: 采用线程池, 池中会缓存一定数量的线程, 进而达到效果
Q3: 分类
按用途分为两类:
主线程: 一般一个进程只有一个主线程, 主要处理界面交互相关的逻辑
子线程: 除主线程之外都是子线程, 主要用于执行耗时操作
按形态可分为三类:
AsyncTask: 底层封装了线程池和 Handler, 便于执行后台任务以及在主线程中进行 UI 操作
HandlerThread: 一种具有消息循环的线程, 其内部可使用 Handler
IntentService: 一种异步, 会自动停止的服务, 内部采用 HandlerThread 和 Handler
PS: 想详细了解 Handler 机制的读者, 推荐一篇笔者的文章: 进阶之路 | 奇妙的 Handler 之旅 https://juejin.im/post/5e61bf2de51d4526ea7f00bd
Q4: 如何安全地终止线程?
对于有多线程开发经验的开发者, 应该大多数在开发过程中都遇到过这样的需求, 就是在某种情况下, 希望立即停止一个线程
比如: 做 Android 开发, 当打开一个界面时, 需要开启线程请求网络获取界面的数据, 但有时候由于网络特别慢, 用户没有耐心等待数据获取完成就将界面关闭, 此时就应该立即停止线程任务, 不然一般会内存泄露, 造成系统资源浪费, 如果用户不断地打开又关闭界面, 内存泄露会累积, 最终导致内存溢出, App 闪退
所以, 笔者希望能和大家探究下: 如何安全地终止线程?
A1: 为啥不使用 stop?
Java 官方早已将它废弃, 不推荐使用
stop 是通过立即抛出 ThreadDeath 异常, 来达到停止线程的目的, 此异常抛出有可能发生在任何一时间点, 包括在 catch,finally 等语句块中, 但是此异常并不会引起程序退出
异常抛出, 导致线程会释放全部所持有的锁, 极可能引起线程安全问题
A2: 提供单独的取消方法来终止线程
示例 DEMO:
- public class MoonRunner implements Runnable {
- private long i;
- // 注意的是这里的变量是用 volatile 修饰
- volatile boolean on = true;
- @Override
- public void run() {
- while (on) {
- i++;
- }
- System.out.println("sTop");
- }
- // 设置一个取消的方法
- void cancel() {
- on = false;
- }
- }
注意: 这里的变量是用 volatile 修饰, 以保证可见性, 关于 volatile 的知识, 笔者将在下文为您详细解析
A3: 采用 interrupt 来终止线程
Thread 类定义了如下关于中断的方法:
原理:
调用 Thread 对象的 interrupt 函数并不是立即中断线程, 只是将线程中断状态标志设置为 true
当线程运行中有调用其阻塞的函数时, 阻塞函数调用之后, 会不断地轮询检测中断状态标志是否为 true, 如果为 true, 则停止阻塞并抛出
InterruptedException
异常, 同时还会重置中断状态标志, 因此需要在 catch 代码块中需调用 interrupt 函数, 使线程再次处于中断状态
如果中断状态标志为 false, 则继续阻塞, 直到阻塞正常结束
具体的 interrupt 的使用方式可以参考这篇文章: Java 线程中断的正确姿势 https://www.jianshu.com/p/264d4e1b76af
2.2 同步机制
2.2.1 volatile
有时候仅仅为了读写一个或者两个实例就使用同步 synchronized 的话, 显得开销过大
而 volatile 为实例域的同步访问提供了免锁的机制
Q1: 先从 Java 内存模型聊起
Java 内存模型定义了本地内存和主存之间的抽象关系
线程之间的共享变量存储在主存中
每个线程都有一个私有的本地内存(工作内存), 本地内存中存储了该线程共享变量的副本.
线程之间通信的步骤
线程 A 将其本地内存中更新过的共享变量刷新到主存中去
线程 B 到主存中去读取线程 A 之前已更新过的共享变量
Q2: 原子性, 可见性和有序性了解多少
a1: 原子性 Atomicity:
定义: 原子性操作就是指这些操作是不可中断的, 要做一定做完, 要么就没有执行
对基本数据类型变量的读取和赋值操作是原子性操作
注意: 这里的赋值操作是指将数字赋值给某个变量
下面由 DEMO 解释更加通俗易懂
- x=3; // 原子性操作
- y=x; // 非原子性操作 原因: 包括 2 个操作: 先读取 x 的值, 再将 x 的值写入工作内存
- x++; // 非原子性操作 原因: 包括 3 个操作: 读取 x 的值, 对 x 的值进行加 1, 向工作内存写入新值
volatile 不支持原子性(想探究原因的, 笔者推荐一篇文章: 面试官最爱的 volatile 关键字)
保证整块代码原子性 (例如 i++) 的方法: 借助于 synchronized 和 Lock, 以及并发包下的 atomic 的原子操作类
a2: 可见性 Visibility
定义: 一个线程修改的结果, 另一个线程马上就能看到
Java 就是利用 volatile 来提供可见性的
原因: 当一个变量被 volatile 修饰时, 那么对它的修改会立刻刷新到主存, 同时使其它线程的工作内存中对此变量的缓存行失效, 因此需要读取该变量时, 会去内存中读取新值
其实通过 synchronized 和 Lock 也能够保证可见性, 但是 synchronized 和 Lock 的开销都更大
a3: 有序性 Ordering
指令重排序的定义: 大多数现代微处理器都会采用将指令乱序执行的方法, 在条件允许的情况下, 直接运行当前有能力立即执行的后续指令, 避开获取下一条指令所需数据时造成的等待
什么时候不进行指令重排序:
符合数据依赖性:
- //x 对 a 有依赖
- a = 1;
- x = a;
as-if-serial 语义: 不管怎么重排序, 单线程程序的执行结果不能被改变
程序顺序原则
如果 A happens-before B
如果 B happens-before C
那么 A happens-before C
这就是 happens-before 传递性
volatile 通过禁止指令重排序的方式来保证有序性
Q3: 应用场景有哪些?
状态量标记
线程的终止的时候的状态控制, 示例 DEMO 如前文
DCL
避免指令重排序:
假定创建一个对象需要:
申请内存
初始化
instance 指向分配的那块内存
上面的 2 和 3 操作是有可能重排序的, 如果 3 重排序到 2 的前面, 这时候 2 操作还没有执行, instance!=null, 当然不是安全的
- class Singleton{
- private volatile static Singleton instance = null;
- private Singleton() {}
- public static Singleton getInstance() {
- if(instance==null) {
- synchronized (Singleton.class) {
- if(instance==null)
- instance = new Singleton();
- }
- }
- return instance;
- }
- }
Q4: 原理:
如果把加入 volatile 关键字的代码和未加入 volatile 关键字的代码都生成汇编代码, 会发现加入 volatile 关键字的代码会多出一个 lock 前缀指令
lock 前缀指令实际相当于一个内存屏障, 内存屏障提供了以下功能:
重排序时不能把后面的指令重排序到内存屏障之前的位置
使得本 CPU 的 Cache 写入内存
写入动作也会引起别的 CPU 或者别的内核无效化其 Cache, 相当于让新写入的值对别的线程可见
2.2.2 重入锁与条件对象
synchronized 关键字自动为我们提供了锁以及相关的条件, 大多数需要显式锁的时候, 使用 synchronized 非常方便, 但是当我们了解了重入锁和条件对象时, 能更好地理解 synchronized 和阻塞队列
Q1: 重入锁的定义
可重入锁指的是可重复可递归调用的锁, 在外层使用锁之后, 在内层仍然可以使用, 并且不发生死锁, 这样的锁就叫做可重入锁
ReentrantLock 和 synchronized 都是可重入锁
重复调用锁的 DEMO 如下:
- public class ReentrantTest implements Runnable {
- public synchronized void get() {
- System.out.println(Thread.currentThread().getName());
- set();
- }
- public synchronized void set() {
- System.out.println(Thread.currentThread().getName());
- }
- public void run() {
- get();
- }
- public static void main(String[] args) {
- ReentrantTest rt = new ReentrantTest();
- for(;;){
- new Thread(rt).start();
- }
- }
- }
Q2: 什么是条件对象 Condition?
条件对象来管理那些已经获得了一个锁但是却不能做有用工作的线程, 条件对象又被称作条件变量
一般要配合 ReentrantLock 使用, 用 Condition.await()可以阻塞当前线程, 并放弃锁
Q3: 下面说明重入锁与条件对象如何协同使用
用支付宝转账的例子(支付宝打钱, 狗头. jpg)
场景是这样的:
- // 转账的方法
- public void transfer(int from, int to, int amount){
- //alipay 是 ReentrantLock 的实例
- alipay.lock();
- try{
- // 当要转给别人的钱大于你所拥有的钱的时候, 调用 Condition 的 await 可以阻塞当前线程, 并放弃锁
- while(accounts[from] <amount){
- condition.await();
- }
- ...// 一系列转账的操作
- // 阻塞状态解除, 进入可运行状态
- condition.signalAll();
- }
- finally{
- alipay.unlock();
- }
- }
想要更深一步了解重入锁的读者, 可以看下这篇文章: 究竟什么是可重入锁?
2.2.3 synchronized
Q1:synchronized 有哪几种实现方式?
同步代码块
同步方法
Q2:synchronized 与 ReentrantLock 的关系
两者都是重入锁
两者有些方法互相对应
wait 等价于 condition.await()
notifyAll 等价于
condition.signalAll()
Q3: 使用场景对比
类型 | 使用场景 |
---|---|
阻塞队列 | 一般实现同步的时候使用 |
同步方法 | 如果同步方法适合你的程序 |
同步代码块 | 不建议使用 |
Lock/Condition | 需要使用 Lock/Condition 的独有特性时 |
2.3 阻塞队列
为了更好地理解线程池的知识, 我们需要了解下阻塞队列
Q1: 定义
阻塞队列 BlockingQueue 是一个支持两个附加操作的队列. 这两个附加的操作是:
在队列为空时, 获取元素的线程会等待队列变为非空
当队列满时, 存储元素的线程会等待队列可用
Q2: 使用场景:
阻塞队列常用于生产者和消费者的场景, 生产者是往队列里添加元素的线程, 消费者是从队列里拿元素的线程. 阻塞队列就是生产者存放元素的容器, 而消费者也只从容器里拿元素.
Q3: 核心方法
方法 \ 处理方式 | 抛出异常 | 返回特殊值 | 一直阻塞 | 超时退出 |
---|---|---|---|---|
插入方法 | add(e) | offer(e) | put(e) | offer(e,time,unit) |
移除方法 | remove() | poll() | take() | poll(time,unit) |
检查方法 | element() | peek() | 不可用 | 不可用 |
Q4:JAVA 中的阻塞队列
名称 | 含义 |
---|---|
ArrayBlockingQueue | 由 < strong ztid="337" ow="26" oh="19">数组 结构组成的 < strong ztid="338" ow="26" oh="19"> 有界 阻塞队列(最常用) |
LinkedBlockingQueue | 由 < strong ztid="343" ow="26" oh="19">链表 结构组成的 < strong ztid="344" ow="26" oh="19"> 有界 阻塞队列(最常用)注意:一定要指定大小 |
PriorityBlockingQueue | 支持 < strong ztid="349" ow="65" oh="19">优先级排序 的 < strong ztid="350" ow="26" oh="19"> 无界 阻塞队列。默认自然升序 < strong ztid="351" ow="26" oh="19"> 排列 |
DelayQueue | 支持 < strong ztid="355" ow="26" oh="19">延时 获取元素的无界阻塞队列。 |
SynchronousQueue | 不存储 元素的阻塞队列(可以看成是一个传球手,负责把生产者线程处理的数据直接传递给消费者线程) |
LinkedTransferQueue | 由 < strong ztid="364" ow="26" oh="19">链表 结构组成的 < strong ztid="365" ow="26" oh="19"> 无界 阻塞队列 |
LinkedBlockingDeque | 由 < strong ztid="370" ow="26" oh="19">链表 结构组成的 < strong ztid="371" ow="52" oh="19"> 双向阻塞 队列(双向队列指的是可以从队列的两端插入和移出元素) |
Q5: 实现原理:
底层利用了 ReentrantLock&Condition 来实现自动加锁和解锁的功能
如果想详细了解阻塞队列实现原理的源码, 笔者推荐一篇文章: Android 并发学习之阻塞队列 https://juejin.im/post/5b21ded3e51d4506bb3a84b6
2.4 Android 中的线程形态
2.4.1 AsyncTask
Q1: 定义: 一种轻量级的异步任务类
在 Android 中实现异步任务机制有两种方式: Handler https://juejin.im/post/5e61bf2de51d4526ea7f00bd 和 AsyncTask
Handler 机制存在的问题: 代码相对臃肿; 多任务同时执行时不易精确控制线程.
引入 AsyncTask 的好处: 创建异步任务更简单, 直接继承它可方便实现后台异步任务的执行和进度的回调更新 UI, 而无需编写任务线程和 Handler 实例就能完成相同的任务.
Q2: 五个核心方法:
方法 | 运行线程 | 调用时刻 | 作用 |
---|---|---|---|
onPreExecute() | 主线程 | 在异步任务执行之前被调用 | 可用于进行一些界面上的 < strong ztid="404" ow="39" oh="19"> 初始化 操作 |
doInBackground() | 子线程 | 异步任务执行时 | 可用于处理所有的 < strong ztid="410" ow="52" oh="19"> 耗时任务 。若需要更新 UI 需调用 publishProgress() |
onProgressUpdate() | 主线程 | 调用 publishProgress() 之后 | 可利用方法中携带的参数如 Progress 来对 UI 进行相应地更新 |
onPostExecute() | 主线程 | 在异步任务执行完毕并通过 return 语句返回时被调用 | 可利用方法中返回的数据来进行一些 < strong ztid="421" ow="14" oh="19">UI 操作 |
onCancelled() | 主线程 | 当异步任务被取消时被调用 | 可用于做 < strong ztid="427" ow="52" oh="19"> 界面取消 的更新 |
注意:
不要直接调用上述方法
AsyncTask 对象必须在主线程创建
Q3: 开始和结束异步任务的方法
execute()
必须在主线程中调用
作用: 表示开始一个异步任务
注意: 一个异步对象只能调用一次 execute()方法
cancel()
必须在主线程中调用
作用: 表示停止一个异步任务
Q4: 工作原理:
内部有一个静态的 Handler 对象即 InternalHandler
作用: 将执行环境从线程池切换到主线程; 通过它来发送任务执行的进度以及执行结束等消息
注意: 必须在主线程中创建
内部有两个线程池:
SerialExecutor: 用于任务的排队, 默认是串行的线程池
THREAD_POOL_EXECUTOR
: 用于真正执行任务
排队执行过程:
把参数 Params 封装为 FutureTask 对象, 相当于 Runnable
调用
SerialExecutor.execute()
将 FutureTask 插入到任务队列 tasks
若没有正在活动的 AsyncTask 任务, 则就会执行下一个 AsyncTask 任务. 执行完毕后会继续执行其他任务直到所有任务都完成. 即默认使用串行方式执行任务.
执行流程图:
注意: AsyncTask 不适用于进行特别耗时的后台任务, 而是建议用线程池
如果想要了解具体源码的读者, 笔者推荐一篇文章: Android AsyncTask 完全解析, 带你从源码的角度彻底理解
2.4.2 HandlerThread
Q1: 定义:
HandlerThread 是一个线程类, 它继承自 Thread
与普通 Thread 的区别: 具有消息循环的效果. 原理:
内部
HandlerThread.run()
方法中有 Looper, 通过 Looper.prepare()来创建消息队列, 并通过 Looper.loop()来开启消息循环
Q2: 实现方法
实例化一个 HandlerThread 对象, 参数是该线程的名称
通过
HandlerThread.start()
开启线程
实例化一个 Handler 并传入 HandlerThread 中的 Looper 对象, 使得与 HandlerThread 绑定
利用 Handler 即可执行异步任务
当不需要 HandlerThread 时, 通过
HandlerThread.quit()
/quitSafely()方法来终止线程的执行
- private HandlerThread myHandlerThread ;
- private Handler handler ;
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_main);
- // 实例化 HandlerThread
- myHandlerThread = new HandlerThread("myHandler") ;
- // 开启 HandlerThread
- myHandlerThread.start();
- // 将 Handler 对象与 HandlerThread 线程绑定
- handler =new Handler(myHandlerThread.getLooper()){
- @Override
- publicvoid handleMessage(Message msg) {
- super.handleMessage(msg);
- // 这里接收 Handler 发来的消息, 运行在 handler_thread 线程中
- //TODO...
- }
- };
- // 在主线程给 Handler 发送消息
- handler.sendEmptyMessage(1) ;
- new Thread(new Runnable() {
- @Override
- publicvoid run() {
- // 在子线程给 Handler 发送数据
- handler.sendEmptyMessage(2) ;
- }
- }).start();
- }
- @Override
- protected void onDestroy() {
- super.onDestroy();
- // 终止 HandlerThread 运行
- myHandlerThread.quit() ;
- }
Q3: 用途
进行串行异步通信
构造 IntentService
方便实现在子线程 (工作线程) 中使用 Handler
Q4: 原理:
实际就是
HandlerThread.run()
里面封装了 Looper.prepare()和 Looper.loop(), 以便能在子线程中使用 Handler
同时,
HandlerThread.getLooper()
中使用了
wait()和 synchronized 代码块
, 当 Looper==NULL 的时候, 锁住了当前的对象, 那什么时候唤醒等待呢? 当然是在初始化完该线程关联 Looper 对象的地方, 也就是 run()
想了解源码的话, 笔者推荐一篇文章: 浅析 HandlerThread
2.4.3 IntentService
Q1: 定义:
IntentService 是一个继承自 Service 的抽象类
Q2: 优点:
相比于线程: 由于是服务, 优先级比线程高, 更不容易被系统杀死. 因此较适合执行一些高优先级的后台任务
相比于普通 Service: 可自动创建子线程来执行任务, 且任务执行完毕后自动退出
Q3: 使用方法
新建类并继承 IntentService, 重写 onHandleIntent(), 该方法:
运行在子线程, 因此可以进行一些耗时操作
作用: 从 Intent 参数中区分具体的任务并执行这些任务
在配置文件中进行注册
在活动中利用 Intent 实现 IntentService 的启动:
- Intent intent = new Intent(this, MyService.class);
- intent.putExtra("xxx",xxx);
- startService(intent);// 启动服务
注意: 无需手动停止服务, onHandleIntent()执行结束之后, IntentService 会自动停止.
Q4: 工作原理
在
IntentService.onCreate()
里创建一个 Thread 对象即 HandlerThread, 利用其内部的 Looper 会实例化一个 ServiceHandler
任务请求的 Intent 会被封装到 Message 并通过 ServiceHandler 发送给 Looper 的 MessageQueue, 最终在 HandlerThread 中执行
在
ServiceHandler.handleMessage()
中会调用
IntentService.onHandleIntent()
, 可在该方法中处理后台任务的逻辑, 执行完毕后会调用 stopSelf(), 以实现自动停止
下面继续来研究下: 将 Intent 传递给服务 & 依次插入到工作队列中的流程
如果对 IntentService 的具体源码感兴趣的话, 笔者推荐一篇文章: Android 多线程: IntentService 用法 & 源码分析
2.5 线程池
Q1: 优点
重用线程池中的线程, 避免线程的创建和销毁带来的性能消耗
有效控制线程池的最大并发数, 避免大量的线程之间因互相抢占系统资源而导致阻塞现象
进行线程管理, 提供定时 / 循环间隔执行等功能
Q2: 构造方法分析
- // 构造参数
- public ThreadPoolExecutor(int corePoolSize,
- int maximumPoolSize,
- long keepAliveTime,
- TimeUnit unit,
- BlockingQueue<Runnable> workQueue,
- ThreadFactory threadFactory,
- RejectedExecutionHandler handler) {
来源: https://www.cnblogs.com/xcynice/p/qi_miao_de_thread_zhi_lv.html