进阶之路|奇妙的Thread之旅
学习导图:
一.为什么要学习Thread?
在Android中,几乎完全采用了Java中的线程机制。线程是最小的调度单位,在很多情况下为了使APP更加流程地运行,我们不可能将很多事情都放在主线程上执行,这样会造成严重卡顿(ANR),那么这些事情应该交给子线程去做,但对于一个系统而言,创建、销毁、调度线程的过程是需要开销的,所以我们并不能无限量地开启线程,那么对线程的了解就变得尤为重要了。
本篇文章将带领大家由浅入深,从线程的基础,谈到同步机制,再讲到阻塞队列,接着提及Android中的线程形态,最终一览线程池机制。
话不多说,赶紧开始奇妙的Thread之旅吧!
二.核心知识点归纳
2.1 线程概述
Q1:含义线程是CPU调度的最小单位
注意与进程相区分
Q2:特点
线程是一种受限的系统资源。即线程不可无限制的产生且线程的创建和销毁都有一定的开销
Q:如何避免频繁创建和销毁线程所带来的系统开销?A:采用线程池,池中会缓存一定数量的线程,进而达到效果
Q3:分类
按用途分为两类:主线程:一般一个进程只有一个主线程,主要处理界面交互相关的逻辑子线程:除主线程之外都是子线程,主要用于执行耗时操作
按形态可分为三类:AsyncTask:底层封装了线程池和Handler,便于执行后台任务以及在主线程中进行UI操作HandlerThread:一种具有消息循环的线程,其内部可使用HandlerIntentService:一种异步、会自动停止的服务,内部采用HandlerThread和Handler
PS:想详细了解Handler机制的读者,推荐一篇笔者的文章:进阶之路 | 奇妙的Handler之旅
Q4:如何安全地终止线程?
对于有多线程开发经验的开发者,应该大多数在开发过程中都遇到过这样的需求,就是在某种情况下,希望立即停止一个线程比如:做Android开发,当打开一个界面时,需要开启线程请求网络获取界面的数据,但有时候由于网络特别慢,用户没有耐心等待数据获取完成就将界面关闭,此时就应该立即停止线程任务,不然一般会内存泄露,造成系统资源浪费,如果用户不断地打开又关闭界面,内存泄露会累积,最终导致内存溢出,APP闪退所以,笔者希望能和大家探究下:如何安全地终止线程?
A1:为啥不使用stop?
Java官方早已将它废弃,不推荐使用
stop是通过立即抛出ThreadDeath异常,来达到停止线程的目的,此异常抛出有可能发生在任何一时间点,包括在catch、finally等语句块中,但是此异常并不会引起程序退出异常抛出,导致线程会释放全部所持有的锁,极可能引起线程安全问题A2:提供单独的取消方法来终止线程
示例DEMO:
注意:这里的变量是用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不支持原子性保证整块代码原子性(例如i++)的方法:借助于synchronized和Lock,以及并发包下的atomic的原子操作类a2:可见性
Visibility
定义:一个线程修改的结果,另一个线程马上就能看到Java就是利用volatile来提供可见性的原因:当一个变量被volatile修饰时,那么对它的修改会立刻刷新到主存,同时使其它线程的工作内存中对此变量的缓存行失效,因此需要读取该变量时,会去内存中读取新值
其实通过synchronized和Lock也能够保证可见性,但是synchronized和Lock的开销都更大a3:有序性
Ordering
指令重排序的定义:大多数现代微处理器都会采用将指令乱序执行的方法, 在条件允许的情况下, 直接运行当前有能力立即执行的后续指令, 避开获取下一条指令所需数据时造成的等待什么时候不进行指令重排序:符合数据依赖性:Copy//x对a有依赖a = 1;x = a;as-if-serial语义:不管怎么重排序, 单线程程序的执行结果不能被改变程序顺序原则如果Ahappens-beforeB如果Bhappens-beforeC那么Ahappens-beforeC这就是happens-before传递性
volatile通过禁止指令重排序的方式来保证有序性Q3:应用场景有哪些?
状态量标记线程的终止的时候的状态控制,示例DEMO如前文
DCL避免指令重排序:假定创建一个对象需要:申请内存初始化instance指向分配的那块内存上面的2和3操作是有可能重排序的, 如果3重排序到2的前面, 这时候2操作还没有执行,instance!=null, 当然不是安全的
Q4:原理:
如果把加入volatile关键字的代码和未加入volatile关键字的代码都生成汇编代码,会发现加入volatile关键字的代码会多出一个lock前缀指令lock前缀指令实际相当于一个内存屏障,内存屏障提供了以下功能:重排序时不能把后面的指令重排序到内存屏障之前的位置使得本CPU的Cache写入内存写入动作也会引起别的CPU或者别的内核无效化其Cache,相当于让新写入的值对别的线程可见
2.2.2 重入锁与条件对象
synchronized关键字自动为我们提供了锁以及相关的条件,大多数需要显式锁的时候,使用synchronized非常方便,但是当我们了解了重入锁和条件对象时,能更好地理解synchronized和阻塞队列
Q1:重入锁的定义
可重入锁指的是可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁,这样的锁就叫做可重入锁ReentrantLock和synchronized都是可重入锁重复调用锁的DEMO如下:
Q2:什么是条件对象Condition?
条件对象来管理那些已经获得了一个锁但是却不能做有用工作的线程,条件对象又被称作条件变量一般要配合ReentrantLock使用,用Condition.await()可以阻塞当前线程,并放弃锁Q3:下面说明重入锁与条件对象如何协同使用
用支付宝转账的例子(支付宝打钱,狗头.jpg)场景是这样的:
想要更深一步了解重入锁的读者,可以看下这篇文章:究竟什么是可重入锁?
2.2.3 synchronizedQ1:synchronized有哪几种实现方式?
同步代码块同步方法Q2:synchronized与ReentrantLock的关系
两者都是重入锁两者有些方法互相对应wait等价于condition.await()notifyAll等价于condition.signalAll()
Q3:使用场景对比
2.3 阻塞队列
为了更好地理解线程池的知识,我们需要了解下阻塞队列
Q1:定义
阻塞队列BlockingQueue是一个支持两个附加操作的队列。这两个附加的操作是:在队列为空时,获取元素的线程会等待队列变为非空当队列满时,存储元素的线程会等待队列可用
Q2:使用场景:
阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素。
Q3:核心方法
Q4:JAVA中的阻塞队列
Q5:实现原理:
底层利用了ReentrantLock&Condition来实现自动加锁和解锁的功能如果想详细了解阻塞队列实现原理的源码,笔者推荐一篇文章:Android并发学习之阻塞队列2.4 Android中的线程形态
2.4.1 AsyncTaskQ1:定义:一种轻量级的异步任务类
在Android中实现异步任务机制有两种方式:Handler和AsyncTaskHandler机制存在的问题:代码相对臃肿;多任务同时执行时不易精确控制线程。引入AsyncTask的好处:创建异步任务更简单,直接继承它可方便实现后台异步任务的执行和进度的回调更新UI,而无需编写任务线程和Handler实例就能完成相同的任务。
Q2:五个核心方法:
注意:不要直接调用上述方法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()方法来终止线程的执行
Q3:用途
进行串行异步通信构造IntentService方便实现在子线程(工作线程)中使用HandlerQ4:原理:
实际就是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,提供一系列参数来配置线程池
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:线程池的分类
三.再聊聊
AsyTask
的不足#AsyncTask看似十分美好,但实际上存在着非常多的不足,这些不足使得它逐渐退出了历史舞台,因此如今已经被RxJava、协程等新兴框架所取代
生命周期AsyncTask没有与 Activity、Fragment的生命周期绑定,即使Activity被销毁,它的doInBackground 任务仍然会继续执行
取消任务AsyncTask的 cancel 方法的参数 mayInterruptIfRunning存在的意义不大,并且它无法保证任务一定能取消,只能尽快让任务取消(比如如果正在进行一些无法打断的操作时,任务就仍然会运行)
内存泄漏由于它没有与 Activity等生命周期进行绑定,因此它的生命周期仍然可能比 Activity 长如果将它作为Activity的非static内部类,则它会持有Activity的引用,导致Activity 的内存无法释放。
并行/串行由于AsyncTask 的串行和并行执行在多个版本上都进行了修改,所以当多个 AsyncTask 依次执行时,它究竟是串行还是并行执行取决于用户手机的版本。具体修改下:A.Android 1.6 之前:各个 AsyncTask 按串行的顺序进行执行B.Android 3.0 之前:由于设计者认为串行执行效率太低,因此改为了并行执行,最多五个 AsyncTask 同时执行C.Android 3.0 之后:由于之前的改动,很多应用出现了并发问题,因此引入 SerialExecutor 改回了串行执行,但对并行执行进行了支持