内容简介:本文已经收录到我的Github个人博客,欢迎大佬们光临寒舍:
前言
本文已经收录到我的Github个人博客,欢迎大佬们光临寒舍:
需要已经具备的知识:
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之旅
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线程中断的正确姿势
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
happens-before happens-before happens-before
这就是 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 |
由 数组 结构组成的 有界 阻塞队列(最常用) |
LinkedBlockingQueue |
由 链表 结构组成的 有界 阻塞队列(最常用)注意:一定要指定大小 |
PriorityBlockingQueue |
支持 优先级排序 的 无界 阻塞队列。默认自然升序 排列 |
DelayQueue |
支持 延时 获取元素的无界阻塞队列。 |
SynchronousQueue |
不存储 元素的阻塞队列(可以看成是一个传球手,负责把生产者线程处理的数据直接传递给消费者线程) |
LinkedTransferQueue |
由 链表 结构组成的 无界 阻塞队列 |
LinkedBlockingDeque |
由 链表 结构组成的 双向阻塞 队列(双向队列指的是可以从队列的两端插入和移出元素) |
Q5:实现原理:
- 底层利用了
ReentrantLock
&Condition
来实现自动加锁和解锁的功能 - 如果想详细了解阻塞队列实现原理的源码,笔者推荐一篇文章: Android并发学习之阻塞队列
2.4 Android
中的线程形态
2.4.1 AsyncTask
Q1: 定义 :一种轻量级的 异步 任务类
在 Android
中实现异步任务机制有两种方式: Handler 和 AsyncTask
-
Handler
机制存在的 问题 :代码相对臃肿;多任务同时执行时不易精确控制线程。 - 引入
AsyncTask
的 好处 :创建异步任务更简单,直接继承它可方便实现后台异步任务的执行和进度的回调更新UI,而无需编写任务线程和Handler
实例就能完成相同的任务。
Q2:五个核心方法:
方法 | 运行线程 | 调用时刻 | 作用 |
---|---|---|---|
onPreExecute() |
主线程 | 在异步任务执行之前被调用 | 可用于进行一些界面上的 初始化 操作 |
doInBackground() |
子线程 | 异步任务执行时 | 可用于处理所有的 耗时任务 。若需要更新 UI 需调用 publishProgress() |
onProgressUpdate() |
主线程 | 调用 publishProgress() 之后 |
可利用方法中携带的参数如 Progress 来对 UI 进行相应地更新 |
onPostExecute() |
主线程 | 在异步任务执行完毕并通过 return 语句返回时被调用 |
可利用方法中返回的数据来进行一些 UI 操作 |
onCancelled() |
主线程 | 当异步任务被取消时被调用 | 可用于做 界面取消 的更新 |
注意:
- 不要直接调用上述方法
-
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:构造方法分析
Executor ThreadPoolExecutor
//构造参数 public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) {
-
corePoolSize
:核心线程数
-
默认情况下,核心线程会在线程中一直存活
-
当设置
ThreadPoolExecutor
的allowCoreThreadTimeOut
属性为A.
true
:表示核心线程闲置超过超时时长,会被回收B.
false
: 表示核心线程不会被回收,会在线程池中一直存活
-
maximumPoolSize
:最大线程数
当活动线程数达到这个数值后,后续的任务将会被阻塞
-
keepAliveTime
:非核心线程超时时间
- 超过这个时长,闲置的非核心线程就会被回收
- 当设置
ThreadPoolExecutor
的allowCoreThreadTimeTout
属性为true
时,keepAliveTime
对核心线程同样有效
-
unit
:用于指定keepAliveTime
参数的时间单位
单位有: TimeUnit.MILLISECONDS
、 TimeUnit.SECONDS
、 TimeUnit.MINUTES
等;
-
workQueue
:任务队列
通过线程池的 execute()
方法提交的 Runnable
对象会存储在这个参数中
-
threadFactory
:线程工厂,可创建新线程
一个接口,只有一个方法 Thread newThread(Runnable r)
-
handler
:在线程池无法执行新任务时进行调度
Q3:ThreadPoolExecutor的默认工作策略
Q4: 线程池的分类
名称 | 含义 | 特点 |
---|---|---|
FixThreadPool |
线程数量固定的线程池,所有线程都是 核心线程 ,当线程空闲时 不会 被回收 | 能 快速 响应外界请求 |
CachedThreadPool |
线程数量不定的线程池(最大线程数为 Integer.MAX_VALUE ),只有 非核心线程 ,空闲线程有超时机制,超时回收 | 适合于执行大量的 耗时较少 的任务 |
ScheduledThreadPool |
核心线程数量 固定 ,非核心线程数量 不定 | 定时 任务和 固定 周期的任务 |
SingleThreadExecutor |
只有 一个核心线程 ,可确保所有的任务都在同一个线程中 按顺序 执行 | 无需处理 线程同步 问题 |
三.再聊聊 AsyTask
的不足
AsyncTask
看似十分美好,但实际上存在着非常多的 不足 ,这些不足使得它逐渐退出了历史舞台,因此如今已经被 RxJava
、 协程
等新兴框架所取代(PS:有机会希望能和大家一起探究下 RxJava
的源码)
- 生命周期
AsyncTask
没有与 Activity
、 Fragment
的生命周期绑定,即使 Activity
被销毁,它的 doInBackground
任务仍然会继续执行
- 取消任务
AsyncTask
的 cancel
方法的参数 mayInterruptIfRunning
存在的意义不大,并且它无法保证任务一定能取消,只能尽快让任务取消(比如如果正在进行一些无法打断的操作时,任务就仍然会运行)
- 内存泄漏
- 由于它没有与
Activity
等生命周期进行绑定,因此它的生命周期仍然可能比Activity
长 - 如果将它作为
Activity
的非static
内部类,则它会持有Activity
的引用,导致Activity
的内存无法释放。(PS:与Handler
的内存泄漏问题类似,参考文章: 进阶之路 | 奇妙的Handler之旅 )
- 并行/串行
由于 AsyncTask
的串行和并行执行在多个版本上都进行了修改,所以当多个 AsyncTask
依次执行时,它究竟是串行还是并行执行取决于用户手机的版本。具体修改如下:
A. Android 1.6
之前:各个 AsyncTask
按串行的顺序进行执行
B. Android 3.0
之前:由于设计者认为串行执行效率太低,因此改为了并行执行,最多五个 AsyncTask
同时执行
C. Android 3.0
之后:由于之前的改动,很多应用出现了并发问题,因此引入 SerialExecutor
改回了串行执行,但对并行执行进行了支持
如果文章对您有一点帮助的话,希望您能点一下赞,您的点赞,是我前进的动力
本文参考链接:
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。