前言
需要已经具备的知识:
Thread
的基本概念及使用AsyncTask
的基本概念及使用
学习导图:
一.为什么要学习Thread
?
在Android
中,几乎完全沿用了Java
中的线程机制。线程是最小的调度单位,在很多情况下为了使APP
更加流程地运行,我们不可能将很多事情都放在主线程上执行,这样会造成严重卡顿(ANR
),那么这些事情应该交给子线程去做,但对于一个系统而言,创建、销毁、调度线程的过程是需要开销的,所以我们并不能无限量地开启线程,那么对线程的了解就变得尤为重要了。
因此,本篇文章将带领大家由浅入深,从线程的基础,谈到同步机制,再讲到阻塞队列,接着提及Android
中的线程形态,最终一览线程池机制。
话不多说,赶紧跟随笔者开始奇妙的Thread
之旅吧!
二.核心知识点归纳
2.1 线程概述
Q1:含义
线程是CPU
调度的最小单位
Q2:特点
线程是一种受限的系统资源。即线程不可无限制的产生且线程的创建和销毁都有一定的开销
Q3:分类
- 按用途分为两类:
- 按形态可分为三类:
Q4:如何安全地终止线程?
A1:为啥不使用stop
?
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;
}
}
A3:采用interrupt
来终止线程
Thread
类定义了如下关于中断的方法:
原理:
调用
Thread
对象的interrupt
函数并不是立即中断线程,只是将线程中断状态标志设置为true
当线程运行中有调用其阻塞的函数时,阻塞函数调用之后,会不断地轮询检测中断状态标志是否为
true
,如果为true
,则停止阻塞并抛出InterruptedException
异常,同时还会重置中断状态标志,因此需要在catch
代码块中需调用interrupt
函数,使线程再次处于中断状态如果中断状态标志为
false
,则继续阻塞,直到阻塞正常结束
2.2 同步机制
2.2.1 volatile
Q1:先从Java
内存模型聊起
Java
内存模型定义了本地内存和主存之间的抽象关系
- 线程之间通信的步骤
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
来提供可见性的
- 其实通过
synchronized
和Lock
也能够保证可见性,但是synchronized
和Lock
的开销都更大
a3:有序性Ordering
- 指令重排序的定义:大多数现代微处理器都会采用将指令乱序执行的方法, 在条件允许的情况下, 直接运行当前有能力立即执行的后续指令, 避开获取下一条指令所需数据时造成的等待
- 什么时候不进行指令重排序:
volatile
通过禁止指令重排序的方式来保证有序性
Q3:应用场景有哪些?
- 状态量标记
DCL
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
前缀指令实际相当于一个内存屏障,内存屏障提供了以下功能:
2.2.2 重入锁与条件对象
Q1:重入锁的定义
- 可重入锁指的是可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁,这样的锁就叫做可重入锁
ReentrantLock
和synchronized
都是可重入锁
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:下面说明重入锁与条件对象如何协同使用
//转账的方法
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
的关系
- 两者都是重入锁
- 两者有些方法互相对应
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:定义:一种轻量级的异步任务类
Q2:五个核心方法:
onPreExecute() | 主线程 | 在异步任务执行之前被调用 | 可用于进行一些界面上的初始化操作 |
doInBackground() | 子线程 | 异步任务执行时 | 可用于处理所有的耗时任务。若需要更新UI 需调用 publishProgress() |
onProgressUpdate() | 主线程 | 调用publishProgress() 之后 | 可利用方法中携带的参数如Progress 来对UI 进行相应地更新 |
onPostExecute() | 主线程 | 在异步任务执行完毕并通过return 语句返回时被调用 | 可利用方法中返回的数据来进行一些UI 操作 |
onCancelled() | 主线程 | 当异步任务被取消时被调用 | 可用于做界面取消的更新 |
Q3:开始和结束异步任务的方法
execute()
cancel()
Q4:工作原理:
- 内部有一个静态的
Handler
对象即InternalHandler
- 内部有两个线程池:
- 排队执行过程:
执行流程图:
注意:AsyncTask
不适用于进行特别耗时的后台任务,而是建议用线程池
2.4.2 HandlerThread
Q1:定义:
HandlerThread
是一个线程类,它继承自Thread
- 与普通
Thread
的区别:具有消息循环的效果。原理:
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()
2.4.3 IntentService
Q1:定义:
IntentService
是一个继承自Service
的抽象类
Q2:优点:
- 相比于线程:由于是服务,优先级比线程高,更不容易被系统杀死。因此较适合执行一些高优先级的后台任务
- 相比于普通
Service
:可自动创建子线程来执行任务,且任务执行完毕后自动退出
Q3:使用方法
- 新建类并继承
IntentService
,重写onHandleIntent()
,该方法:
- 在配置文件中进行注册
- 在活动中利用
Intent
实现IntentService
的启动:
Intent intent = new Intent(this, MyService.class);
intent.putExtra("xxx",xxx);
startService(intent);//启动服务
Q4:工作原理
- 在
IntentService.onCreate()
里创建一个Thread
对象即HandlerThread
,利用其内部的Looper
会实例化一个ServiceHandler
- 任务请求的
Intent
会被封装到Message
并通过ServiceHandler
发送给Looper
的MessageQueue
,最终在HandlerThread
中执行 - 在
ServiceHandler.handleMessage()
中会调用IntentService.onHandleIntent()
,可在该方法中处理后台任务的逻辑,执行完毕后会调用stopSelf()
,以实现自动停止
下面继续来研究下:将Intent
传递给服务 & 依次插入到工作队列中的流程
2.5 线程池
Q1:优点
- 重用线程池中的线程,避免线程的创建和销毁带来的性能消耗
- 有效控制线程池的最大并发数,避免大量的线程之间因互相抢占系统资源而导致阻塞现象
- 进行线程管理,提供定时/循环间隔执行等功能
Q2:构造方法分析
//构造参数
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
corePoolSize
:核心线程数
maximumPoolSize
:最大线程数
keepAliveTime
:非核心线程超时时间
unit
:用于指定keepAliveTime
参数的时间单位
workQueue
:任务队列
threadFactory
:线程工厂,可创建新线程
handler
:在线程池无法执行新任务时进行调度
Q3:ThreadPoolExecutor
的默认工作策略
Q4:线程池的分类
FixThreadPool | 线程数量固定的线程池,所有线程都是核心线程,当线程空闲时不会被回收 | 能快速响应外界请求 |
CachedThreadPool | 线程数量不定的线程池(最大线程数为Integer.MAX_VALUE),只有非核心线程,空闲线程有超时机制,超时回收 | 适合于执行大量的耗时较少的任务 |
ScheduledThreadPool | 核心线程数量固定,非核心线程数量不定 | 定时任务和固定周期的任务 |
SingleThreadExecutor | 只有一个核心线程,可确保所有的任务都在同一个线程中按顺序执行 | 无需处理线程同步问题 |
三.再聊聊AsyTask
的不足
- 生命周期
- 取消任务
- 内存泄漏
- 并行/串行
如果文章对您有一点帮助的话,希望您能点一下赞,您的点赞,是我前进的动力
本文参考链接: