进阶之路 | 奇妙的Thread之旅

1,647 阅读21分钟

前言

本文已经收录到我的Github个人博客,欢迎大佬们光临寒舍:

我的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:一种异步、会自动停止的服务,内部采用HandlerThreadHandler
关系图
关系图

想详细了解Handler机制的读者,推荐一篇笔者的文章:进阶之路 | 奇妙的Handler之旅

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

对于有多线程开发经验的开发者,应该大多数在开发过程中都遇到过这样的需求,就是在某种情况下,希望立即停止一个线程

比如:做Android开发,当打开一个界面时,需要开启线程请求网络获取界面的数据,但有时候由于网络特别慢,用户没有耐心等待数据获取完成就将界面关闭,此时就应该立即停止线程任务,不然一般会内存泄露,造成系统资源浪费,如果用户不断地打开又关闭界面,内存泄露会累积,最终导致内存溢出,APP闪退

所以,笔者希望能和大家探究下:如何安全地终止线程?

A1:为啥不使用stop?

Java官方早已将它废弃,不推荐使用

  • 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; } }

注意:这里的变量是用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++)的方法:借助于synchronizedLock,以及并发包下的atomic的原子操作类

a2:可见性Visibility

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

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

原因:当一个变量被volatile修饰时,那么对它的修改会立刻刷新到主存,同时使其它线程的工作内存中对此变量的缓存行失效,因此需要读取该变量时,会去内存中读取新值

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

a3:有序性Ordering

  • 指令重排序的定义:大多数现代微处理器都会采用将指令乱序执行的方法, 在条件允许的情况下, 直接运行当前有能力立即执行的后续指令, 避开获取下一条指令所需数据时造成的等待
  • 什么时候不进行指令重排序
  • 符合数据依赖性:
//x对a有依赖
a = 1;
x = a;
  • as-if-serial语义:不管怎么重排序, 单线程程序的执行结果不能被改变
  • 程序顺序原则
  1. 如果A happens-before B
  2. 如果B happens-before C
  3. 那么A happens-before C

这就是happens-before传递性

  • volatile通过禁止指令重排序的方式来保证有序性

Q3:应用场景有哪些?

  • 状态量标记

线程的终止的时候的状态控制,示例DEMO如前文

  • DCL

避免指令重排序:

假定创建一个对象需要:

  1. 申请内存
  2. 初始化
  3. instance指向分配的那块内存

上面的2和3操作是有可能重排序的, 如果3重排序到2的前面, 这时候2操作还没有执行, instance!=null, 当然不是安全的

class Singleton{
    private volatile static Singleton instance = null;
<span class="hljs-function" style="line-height: 26px;"><span class="hljs-keyword" style="color: #c678dd; line-height: 26px;">private</span> <span class="hljs-title" style="color: #61aeee; line-height: 26px;">Singleton</span><span class="hljs-params" style="line-height: 26px;">()</span> </span>{}

<span class="hljs-function" style="line-height: 26px;"><span class="hljs-keyword" style="color: #c678dd; line-height: 26px;">public</span> <span class="hljs-keyword" style="color: #c678dd; line-height: 26px;">static</span> Singleton <span class="hljs-title" style="color: #61aeee; line-height: 26px;">getInstance</span><span class="hljs-params" style="line-height: 26px;">()</span> </span>{
    <span class="hljs-keyword" style="color: #c678dd; line-height: 26px;">if</span>(instance==<span class="hljs-keyword" style="color: #c678dd; line-height: 26px;">null</span>) {
        <span class="hljs-keyword" style="color: #c678dd; line-height: 26px;">synchronized</span> (Singleton.class) {
            <span class="hljs-keyword" style="color: #c678dd; line-height: 26px;">if</span>(instance==<span class="hljs-keyword" style="color: #c678dd; line-height: 26px;">null</span>)
                instance = <span class="hljs-keyword" style="color: #c678dd; line-height: 26px;">new</span> Singleton();
        }
    }
    <span class="hljs-keyword" style="color: #c678dd; line-height: 26px;">return</span> instance;
}

}

Q4:原理:

  • 如果把加入volatile关键字的代码和未加入volatile关键字的代码都生成汇编代码,会发现加入volatile关键字的代码会多出一个lock前缀指令
  • lock前缀指令实际相当于一个内存屏障,内存屏障提供了以下功能:
  • 重排序时不能把后面的指令重排序到内存屏障之前的位置
  • 使得本CPUCache写入内存
  • 写入动作也会引起别的CPU或者别的内核无效化其Cache,相当于让新写入的值对别的线程可见

2.2.2 重入锁与条件对象

synchronized 关键字自动为我们提供了锁以及相关的条件,大多数需要显式锁的时候,使用synchronized 非常方便,但是当我们了解了重入锁和条件对象时,能更好地理解synchronized 和阻塞队列

Q1:重入锁的定义

  • 可重入锁指的是可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁,这样的锁就叫做可重入锁
  • ReentrantLocksynchronized都是可重入锁

重复调用锁的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();
        }
    ...<span class="hljs-comment" style="color: #5c6370; font-style: italic; line-height: 26px;">//一系列转账的操作</span>
        <span class="hljs-comment" style="color: #5c6370; font-style: italic; line-height: 26px;">//阻塞状态解除,进入可运行状态</span>
    condition.signalAll();
}
<span class="hljs-keyword" style="color: #c678dd; line-height: 26px;">finally</span>{
    alipay.unlock();
}

}

想要更深一步了解重入锁的读者,可以看下这篇文章:究竟什么是可重入锁?

2.2.3 synchronized

Q1:synchronized有哪几种实现方式?

  • 同步代码块
  • 同步方法

Q2:synchronizedReentrantLock的关系

  • 两者都是重入锁
  • 两者有些方法互相对应
  • 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 链表结构组成的双向阻塞队列(双向队列指的是可以从队列的两端插入和移出元素)
JAVA中的阻塞队列
JAVA中的阻塞队列

Q5:实现原理:

  • 底层利用了ReentrantLock&Condition来实现自动加锁和解锁的功能
  • 如果想详细了解阻塞队列实现原理的源码,笔者推荐一篇文章:Android并发学习之阻塞队列

2.4 Android中的线程形态

2.4.1 AsyncTask

Q1:定义:一种轻量级的异步任务类

Android中实现异步任务机制有两种方式:HandlerAsyncTask

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

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

Intent传递流程
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:核心线程数
  • 默认情况下,核心线程会在线程中一直存活

  • 当设置ThreadPoolExecutorallowCoreThreadTimeOut属性为

    A.true:表示核心线程闲置超过超时时长,会被回收

    B.false: 表示核心线程不会被回收,会在线程池中一直存活

  • maximumPoolSize:最大线程数

当活动线程数达到这个数值后,后续的任务将会被阻塞

  • keepAliveTime:非核心线程超时时间
  • 超过这个时长,闲置的非核心线程就会被回收
  • 当设置ThreadPoolExecutorallowCoreThreadTimeTout属性为true时,keepAliveTime对核心线程同样有效
  • unit:用于指定keepAliveTime参数的时间单位

单位有:TimeUnit.MILLISECONDSTimeUnit.SECONDSTimeUnit.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 没有与 ActivityFragment 的生命周期绑定,即使 Activity 被销毁,它的 doInBackground 任务仍然会继续执行

  • 取消任务

AsyncTaskcancel 方法的参数 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 改回了串行执行,但对并行执行进行了支持


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

本文参考链接:

本文使用 mdnice 排版