前言

需要已经具备的知识:

  • Thread的基本概念及使用
  • AsyncTask的基本概念及使用

学习导图:

进阶之路 | 奇妙的Thread之旅-LMLPHP

一.为什么要学习Thread?

Android中,几乎完全沿用了Java中的线程机制。线程是最小的调度单位,在很多情况下为了使APP更加流程地运行,我们不可能将很多事情都放在主线程上执行,这样会造成严重卡顿(ANR),那么这些事情应该交给子线程去做,但对于一个系统而言,创建、销毁、调度线程的过程是需要开销的,所以我们并不能无限量地开启线程,那么对线程的了解就变得尤为重要了。

因此,本篇文章将带领大家由浅入深,从线程的基础,谈到同步机制,再讲到阻塞队列,接着提及Android中的线程形态,最终一览线程池机制

话不多说,赶紧跟随笔者开始奇妙的Thread之旅吧!

二.核心知识点归纳

2.1 线程概述

Q1:含义

线程是CPU调度的最小单位

Q2:特点

线程是一种受限的系统资源。即线程不可无限制的产生且线程的创建和销毁都有一定的开销

Q3:分类

  • 按用途分为两类:
  • 按形态可分为三类:

进阶之路 | 奇妙的Thread之旅-LMLPHP

Q4:如何安全地终止线程?

A1:为啥不使用stop?

  • stop是通过立即抛出ThreadDeath异常,来达到停止线程的目的,此异常抛出有可能发生在任何一时间点,包括在catchfinally等语句块中,但是此异常并不会引起程序退出
  • 异常抛出,导致线程会释放全部所持有的,极可能引起线程安全问题

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之旅-LMLPHP

原理:

  • 调用Thread对象的interrupt函数并不是立即中断线程,只是将线程中断状态标志设置为true

  • 当线程运行中有调用其阻塞的函数时,阻塞函数调用之后,会不断地轮询检测中断状态标志是否为true,如果为true,则停止阻塞并抛出InterruptedException异常,同时还会重置中断状态标志因此需要在catch代码块中需调用interrupt函数,使线程再次处于中断状态

  • 如果中断状态标志为false,则继续阻塞,直到阻塞正常结束

2.2 同步机制

2.2.1 volatile

Q1:先从Java内存模型聊起

  • Java 内存模型定义了本地内存和主存之间的抽象关系

进阶之路 | 奇妙的Thread之旅-LMLPHP

  • 线程之间通信的步骤

Q2:原子性、可见性和有序性了解多少

a1:原子性Atomicity

  • 定义:原子性操作就是指这些操作是不可中断的,要做一定做完,要么就没有执行
  • 对基本数据类型变量的读取和赋值操作是原子性操作

下面由DEMO解释更加通俗易懂

x=3;  //原子性操作
y=x; //非原子性操作 原因:包括2个操作:先读取x的值,再将x的值写入工作内存
x++; //非原子性操作 原因:包括3个操作:读取x的值、对x的值进行加1、向工作内存写入新值
  • volatile不支持原子性(想探究原因的,笔者推荐一篇文章:面试官最爱的volatile关键字
  • 保证整块代码原子性(例如i++)的方法:借助于synchronizedLock,以及并发包下的atomic的原子操作类

a2:可见性Visibility

  • 定义:一个线程修改的结果,另一个线程马上就能看到

  • Java就是利用volatile来提供可见性的

  • 其实通过synchronizedLock也能够保证可见性,但是synchronizedLock的开销都更大

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:重入锁的定义

  • 可重入锁指的是可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁,这样的锁就叫做可重入锁
  • ReentrantLocksynchronized都是可重入锁
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:synchronizedReentrantLock的关系

  • 两者都是重入锁
  • 两者有些方法互相对应

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链表结构组成的双向阻塞队列(双向队列指的是可以从队列的两端插入和移出元素)

进阶之路 | 奇妙的Thread之旅-LMLPHP

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
  • 内部有两个线程池:
  • 排队执行过程:

执行流程图:

进阶之路 | 奇妙的Thread之旅-LMLPHP

注意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发送给LooperMessageQueue,最终在HandlerThread中执行
  • ServiceHandler.handleMessage()中会调用IntentService.onHandleIntent(),可在该方法中处理后台任务的逻辑,执行完毕后会调用stopSelf(),以实现自动停止

进阶之路 | 奇妙的Thread之旅-LMLPHP

下面继续来研究下:将Intent 传递给服务 & 依次插入到工作队列中的流程

进阶之路 | 奇妙的Thread之旅-LMLPHP

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的默认工作策略

进阶之路 | 奇妙的Thread之旅-LMLPHP

​Q4:线程池的分类

FixThreadPool线程数量固定的线程池,所有线程都是核心线程,当线程空闲时不会被回收快速响应外界请求
CachedThreadPool线程数量不定的线程池(最大线程数为Integer.MAX_VALUE),只有非核心线程,空闲线程有超时机制,超时回收适合于执行大量的耗时较少的任务
ScheduledThreadPool核心线程数量固定,非核心线程数量不定定时任务和固定周期的任务
SingleThreadExecutor只有一个核心线程,可确保所有的任务都在同一个线程中按顺序执行无需处理线程同步问题

三.再聊聊AsyTask的不足

  • 生命周期
  • 取消任务
  • 内存泄漏
  • 并行/串行

如果文章对您有一点帮助的话,希望您能点一下赞,您的点赞,是我前进的动力

本文参考链接:

05-11 17:08