Android开发Android开发经验谈Android技术知识

进阶之路 | 奇妙的Thread之旅

2020-03-09  本文已影响0人  许朋友爱玩

前言

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

我的GIthub博客

需要已经具备的知识:

学习导图:

学习导图

一.为什么要学习Thread?

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

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

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

二.核心知识点归纳

2.1 线程概述

Q1:含义

线程是CPU调度的最小单位

注意与进程相区分

Q2:特点

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

Q:如何避免频繁创建和销毁线程所带来的系统开销?
A:采用线程池,池中会缓存一定数量的线程,进而达到效果

Q3:分类

  • 主线程:一般一个进程只有一个主线程,主要处理界面交互相关的逻辑

  • 子线程:除主线程之外都是子线程,主要用于执行耗时操作

  • AsyncTask:底层封装了线程池和Handler,便于执行后台任务以及在主线程中进行UI操作
  • HandlerThread:一种具有消息循环的线程,其内部可使用Handler
  • IntentService:一种异步、会自动停止的服务,内部采用HandlerThreadHandler
关系图

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

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

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

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

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

A1:为啥不使用stop?

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

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类定义了如下关于中断的方法:

中断的方法

原理:

具体的interrupt的使用方式可以参考这篇文章:Java线程中断的正确姿势

2.2 同步机制

2.2.1 volatile

  • 有时候仅仅为了读写一个或者两个实例就使用同步synchronized的话,显得开销过大
  • volatile为实例域的同步访问提供了免锁的机制

Q1:先从Java内存模型聊起

  • 线程之间的共享变量存储在主存
  • 每个线程都有一个私有的本地内存(工作内存),本地内存中存储了该线程共享变量的副本
内存关系
  • 线程A将其本地内存更新过的共享变量刷新到主存中去
  • 线程B主存中去读取线程A之前已更新过的共享变量

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

a1:原子性Atomicity

注意:这里的赋值操作是指将数字赋值给某个变量

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

x=3;  //原子性操作
y=x;  //非原子性操作  原因:包括2个操作:先读取x的值,再将x的值写入工作内存
x++;  //非原子性操作  原因:包括3个操作:读取x的值、对x的值进行加1、向工作内存写入新值

a2:可见性Visibility

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

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传递性

Q3:应用场景有哪些?

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

避免指令重排序:

假定创建一个对象需要:

  1. 申请内存
  2. 初始化
  3. 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:原理:

  • 重排序时不能把后面的指令重排序到内存屏障之前的位置
  • 使得本CPUCache写入内存
  • 写入动作也会引起别的CPU或者别的内核无效化其Cache,相当于让新写入的值对别的线程可见

2.2.2 重入锁与条件对象

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

Q1:重入锁的定义

重复调用锁的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

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:synchronizedReentrantLock的关系

  • wait等价于condition.await()
  • notifyAll等价于condition.signalAll()

Q3:使用场景对比

类型 使用场景
阻塞队列 一般实现同步的时候使用
同步方法 如果同步方法适合你的程序
同步代码块 不建议使用
Lock/Condition 需要使用Lock/Condition的独有特性时

2.3 阻塞队列

为了更好地理解线程池的知识,我们需要了解下阻塞队列

Q1:定义

  • 在队列为空时,获取元素的线程会等待队列变为非空
  • 当队列满时,存储元素的线程会等待队列可用

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中的阻塞队列

Q5:实现原理:

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()方法
  • 必须在主线程中调用
  • 作用:表示停止一个异步任务

Q4:工作原理:

  • 作用:将执行环境从线程池切换到主线程;通过它来发送任务执行的进度以及执行结束等消息

  • 注意:必须在主线程中创建

  • SerialExecutor:用于任务的排队,默认是串行的线程池
  • THREAD_POOL_EXECUTOR:用于真正执行任务
  • 把参数Params封装为FutureTask对象,相当于Runnable
  • 调用SerialExecutor.execute()FutureTask插入到任务队列tasks
  • 若没有正在活动的AsyncTask任务,则就会执行下一个AsyncTask任务。执行完毕后会继续执行其他任务直到所有任务都完成。即默认使用串行方式执行任务。

执行流程图:

AsyncTask工作原理

注意AsyncTask不适用于进行特别耗时的后台任务,而是建议用线程池

如果想要了解具体源码的读者,笔者推荐一篇文章:Android AsyncTask完全解析,带你从源码的角度彻底理解

2.4.2 HandlerThread

Q1:定义:

HandlerThread是一个线程类,它继承自Thread

与普通Thread的区别:具有消息循环的效果。原理:

  • 内部HandlerThread.run()方法中有Looper,通过Looper.prepare()来创建消息队列,并通过Looper.loop()来开启消息循环

Q2:实现方法

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:用途

Q4:原理:

想了解源码的话,笔者推荐一篇文章:浅析HandlerThread

2.4.3 IntentService

Q1:定义:

IntentService是一个继承自Service的抽象类

Q2:优点:

Q3:使用方法

  • 运行在子线程,因此可以进行一些耗时操作
  • 作用:从Intent参数中区分具体的任务并执行这些任务
Intent intent = new Intent(this, MyService.class);
intent.putExtra("xxx",xxx);  
startService(intent);//启动服务

注意:无需手动停止服务,onHandleIntent()执行结束之后,IntentService会自动停止。

Q4:工作原理

总体流程图

下面继续来研究下:将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) {
  • 默认情况下,核心线程会在线程中一直存活

  • 当设置ThreadPoolExecutorallowCoreThreadTimeOut属性为

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

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

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

  • 超过这个时长,闲置的非核心线程就会被回收
  • 当设置ThreadPoolExecutorallowCoreThreadTimeTout属性为true时,keepAliveTime对核心线程同样有效

单位有:TimeUnit.MILLISECONDSTimeUnit.SECONDSTimeUnit.MINUTES等;

通过线程池的execute()方法提交的Runnable对象会存储在这个参数中

一个接口,只有一个方法Thread newThread(Runnable r)

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 3.0 之前:由于设计者认为串行执行效率太低,因此改为了并行执行,最多五个 AsyncTask 同时执行

C.Android 3.0 之后:由于之前的改动,很多应用出现了并发问题,因此引入 SerialExecutor 改回了串行执行,但对并行执行进行了支持


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

本文参考链接:

上一篇 下一篇

猜你喜欢

热点阅读