进阶之路 | 奇妙的Thread之旅
前言
本文已经收录到我的Github个人博客,欢迎大佬们光临寒舍:
需要已经具备的知识:
Thread
的基本概念及使用AsyncTask
的基本概念及使用
学习导图:
一.为什么要学习Thread
?
在Android
中,几乎完全沿用了Java
中的线程机制。线程是最小的调度单位,在很多情况下为了使APP
更加流程地运行,我们不可能将很多事情都放在主线程上执行,这样会造成严重卡顿(ANR
),那么这些事情应该交给子线程去做,但对于一个系统而言,创建、销毁、调度线程的过程是需要开销的,所以我们并不能无限量地开启线程,那么对线程的了解就变得尤为重要了。
因此,本篇文章将带领大家由浅入深,从线程的基础,谈到同步机制,再讲到阻塞队列,接着提及Android
中的线程形态,最终一览线程池机制。
话不多说,赶紧跟随笔者开始奇妙的Thread
之旅吧!
二.核心知识点归纳
2.1 线程概述
Q1:含义
线程是CPU
调度的最小单位
注意与进程相区分
Q2:特点
线程是一种受限的系统资源。即线程不可无限制的产生且线程的创建和销毁都有一定的开销
- Q:如何避免频繁创建和销毁线程所带来的系统开销?
- A:采用线程池,池中会缓存一定数量的线程,进而达到效果(PS:下文将为您详细讲解)
Q3:分类
- 按用途分为两类:
主线程:一般一个进程只有一个主线程,主要处理界面交互相关的逻辑
子线程:除主线程之外都是子线程,主要用于执行耗时操作
- 按形态可分为三类:
AsyncTask
:底层封装了线程池和Handler
,便于执行后台任务以及在主线程中进行UI
操作HandlerThread
:一种具有消息循环的线程,其内部可使用Handler
IntentService
:一种异步、会自动停止的服务,内部采用HandlerThread
和Handler
想详细了解
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
语义:不管怎么重排序, 单线程程序的执行结果不能被改变- 程序顺序原则
- 如果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 |
由数组结构组成的有界阻塞队列(最常用) |
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
- 方便实现在子线程与子线程直接的通信
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:构造方法分析
- 线程池的概念来源:Java中的
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 1.6
--Android 3.0
:由于设计者认为串行执行效率太低,因此改为了并行执行,最多五个AsyncTask
同时执行C.
Android 3.0
之后:由于之前的改动,很多应用出现了并发问题,因此引入SerialExecutor
改回了串行执行,但对并行执行进行了支持
如果文章对您有一点帮助的话,希望您能点一下赞,您的点赞,是我前进的动力
本文参考链接: