[ING] Android中的一些多线程相关的概念

Posted by 阿呆 on 2019-01-04

概述

除Thread本身外,在Android中可以扮演线程的还有很多,比如 AsyncTask和 IntentService, 同时 HandlerThread也是一种特殊的线程

思考:
Thread/AsyncTask/HandlerThread的生命周期
如何避免内存泄漏
如何在不需要线程的情况下停止线程
回调方法 onXxx到底是开始执行Xxx任务,还是执行完了

虽然形式不一,但他们本质都是传统的线程

  • AsyncTask: 底层采用线程池(同时封装了Handler)
  • IntentService 和 HandlerThread : 底层直接采用线程,其中 IntentService 采用HandlerThread

他们的使用场景不同

1.AsyncTask是方便子线程更新UI(封装的很好,包括任务的进度,还有任务的结束)
2.HandlerThread是一种具有消息循环的线程(在它内部可以使用 Handler )
3.IntentService是一个服务,系统对其封装以便更方便地执行后台任务(它的内部采用HandlerThread来执行任务,当任务完毕后,IntentService会自动退出),从任务地角度来看,IntentService很像一个后台线程(子线程),但是它又是一种服务,而服务是不容易被系统杀死的,从而可以尽量保证任务的执行。而如果是一个普通的线程,那么一旦进程中没有活动的四大组件,这个进程的优先级就会非常低,很容易被系统杀死(任务尽量得到执行且能保活进程)

主线程和子线程

主线程指的是进程所拥有的线程,在Java中一个进程默认只有一个线程(主线程),在Android中主线程又叫UI线程,它只处理交互的部分,所以需要较高的响应速度(不能执行耗时操作 | 网络请求、I/O)

关于线程池:

1.线程是一种受限的系统资源,即线程不能无限制地产生(当达到上限?是否能自动等待?)
并且线程的创建和销毁都会有相应的开销(内存和时间)
2.Android中的线程池来源于Java,主要是通过Excutor来派生特定类型的线程池,不同种类的线程池又具备各自的特征

下面挑选几个重要的来介绍一下

AsyncTask

内部封装了线程池和 Handler
轻量级的异步任务类,它可以在线程池中执行后台任务,然后把执行的进度(publishProgress)和最终的结果(postExcute)传递给主线程并在主线程中更新 UI,但是 AsyncTask并不适合执行特别耗时的任务(why),建议使用线程池(TODO1)

使用

AsyncTask是一个抽象的泛型类
public abstract class AsyncTask<Params,Progress,Result> /你也可以换成 <A,B,C>,但没必要,嘿嘿

四个核心方法:

1
2
3
4
5
6
7
8
9
onPreExecute()  						 	#UI Thread

doInBackground(Params…params) #ThreadPool
//中间会调用 publishProgress(ThreadPool)  onProgressUpdate( UI )

onProgressUpdate(Progress…values) #UI Thread

onPostExecute( Result result) #UI Thread
// result 是后台 doInBackground 的返回值

一个典型的流程如下(一般作为内部类,因为要引用Activity的资源来更新UI,当然也可以通过传入一个listener的形式)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private class DownloadTask extends AsyncTask<URL,Integer,Long>{
protected Long doInbackground( URL... urls){
int count = urls.length;
long totalSize = 0;
for(int i=0 ; i<count ; i++){
totalSize+=Downloader.downloadFile( urls[i] ); //这是个同步操作
publishProgress ( (int) ( (i/(float)count )*100) ); //这是文件下载数百分比
if ( isCancelled() ) //Escape cancel() is called (set Flag cancelled=true )
break;
}
return totalSize;
}
protected void onProgressUpdate(Integer... progress){
setProgressPercent(progress[0]);
}
protected void onPostExcute ( Long result ){ showDialog(“Download”+result+”bytes”);}
}

一行代码执行 AsyncTask 任务:
new DownloadFilesTask().excute(url1,url2,url3 )

补充: onCancelled()

当异步任务取消时,onCancelled()方法会取代 onPostExcute(result)被调用。
onXxx一般指的Xxx任务开始执行,比如onDraw,而onXxxed则是指该任务已经执行完毕的回调,例如onViewCreated,则是指视图已经创建完毕

AsyncTask使用的一些条件限制

1.AsyncTask的类必须在主线程加载(TODO3)
2.Asynctask的对象必须在主线程创建
3.excute方法必须在UI线程调用
4.不要再程序中直接调用它的四个核心方法
5.一个AsyncTask对象只能执行一次,即调用一次execute方法,否则会报运行时异常
6.在Android 1.6之前,AsyncTask是串行执行任务,之后开始采用线程池处理并行任务,但是从3.0开始,为了避免AsyncTask所带来的并发错误,AsyncTask又采用一个线程来串行执行任务。尽管如此,在3.0及以后的版本中,你可以手动通过 AsyncTask的executeOnExecutor来并行地执行任务

要得到上述问题的答案,需要来了解一下AsyncTask的工作原理
1.首先系统会把AsyncTask的Params参数封装为 FutureTask对象
2.接着这个FutureTask会交给SerialExecutor的execute方法处理
3.execute方法会首先将任务插到任务队列 mTasks中
4.判断是否有正在活动的 AsyncTask任务,没有就调用 SerialExecutor的scheduleNext方法来执行下一个AsyncTask任务,直到所有Task都执行完毕为止

什么是多个任务呢?类似下面这样

1
2
3
4
for(int i=0; i<10;i++){
MyTask myTask = new MyTask(params[i]);
myTask.execute();
} //结果是这些任务是串行执行的,这个串行线程池是静态的么?(TODO4)

AsyncTask结构

AsyncTask中有两个线程池 (SerialExecutor | THREAD_POOL_EXECUTO)和一个Handler (InternalHandler)
SerialExecutor用于任务的排队,而 THREAD_POOL_EXECUTOR用于真正地执行任务,InternalHandler用于将执行环境从线程池切换到线程(三个核心方法都是运行在UI线程)
为什么需要两个线程池呢?采用单线程线程池不就可以做到排队加串行执行么( TODO5 )

HandlerThread

HandlerThread继承自Thread,它是一种可以使用Handler的Thread
来看一下 HandlerThread的run方法

1
2
3
4
5
6
7
8
9
10
11
12
public void run(){
mTid = Process.myTid();
Looper.prepare();
synchronized( this ){
mLooper = Looper.myLooper();
notifyAll();
}
Process.setThreadPriority(mPriority);
onLooperPrepared();
Looper.loop();
mTid = -1;
}

分析HandlerThread 和 普通Thread的不同:

这里先强调一点:
HandlerThread不会像普通线程那样,自动结束,因为它开启了消息循环队列,会一直等待任务的到来,没有的话,就阻塞在那里。
那么如何结束一个 HandlerThread 呢?
我们可以调用 Looper 的 quit方法或者 quitSafely 方法,当他们执行后,消息队列不再接收新的消息,这时再调用Handler的sendMessage均会返回false,表示发送消息失败。

二者的不同点在于:
1.Looper.quit() : 清空消息池中的所有消息
2.Looper.quitSafly() : 清空消息池中所有的 延迟消息,并将所有的非延迟消息派发给 Handler去处理
实际上,我们来看一下 Looper.loop()的核心代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
while (true) {
Message msg = queue.next(); // might block
if (msg != null) {
if (msg.target == null) {
// No target is a magic identifier for the quit message.
return;
}

long wallStart = 0;
long threadStart = 0;

// This must be in a local variable, in case a UI event sets the logger
Printer logging = me.mLogging;
if (logging != null) {
logging.println(">>>>> Dispatching to " + msg.target + " " +
msg.callback + ": " + msg.what);
wallStart = SystemClock.currentTimeMicro();
threadStart = SystemClock.currentThreadTimeMicro();
}

msg.target.dispatchMessage(msg);
}

loop退出的条件就是 msg.target == null;
而quit和 quitesafely就是向其发送一条target为null的msg

Android 中的线程池

线程池的优点:

1.重用已有的线程,避免创建和销毁线程带来的性能消耗
2.有效控制并发数,避免造成大量的线程之间因相互抢占系统资源而导致的阻塞现象
3.能够对线程进行简单的管理,并提供定时执行以及指定间隔循环执行等功能

Android中的线程池的概念源自 Java中的Executor,Executor是一个接口,真正的线程池的实现是 ThreadPoolExecutor。ThreadPoolExecutor 提供了一系列的参数来配置线程池。掌握这些参数的含义和使用,是用好线程池的关键。

Android中的线程池大体分为4类,都可以通过 Executors的工厂方法来获取。都是直接或者间接地通配置 ThreadPoolExecutor 来实现地,所以,我们先来了解一下ThreadPoolExecutor

ThreadPoolExecutor

1
2
3
4
5
6
public ThreadPoolExecutor ( int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory);

1.corePoolSize //核心线程数
默认情况下,核心线程会一直在线程中运行(即便处于闲置状态)
如果将ThreadPoolExecutor 的 allowCoreThreadTimeOut属性设置为true,则核心线程启用超时策略(等待时长超过 KeepAliveTime所指定的时长后,核心线程停止)

2.maximumPoolSize //最大线程数
当活动线程数达到这个数值后,后续的新任务将会被阻塞

3.keepAliveTime //保活时常
一般情况下作用于普通线程,当 allowCoreThreadTimeOut 属性被设置为 true 时,它同样会作用于核心线程(感觉这种情况下,核心线程和普通线程就没有区别了)

4.uni //时间单位

5.workQueue //任务队列
通过 excute 提交的任务会存储在这个队列中

6.ThreadFactory //线程工厂
为线程池提供创建新线程的功能。ThreadFactory 是一个接口,它只有一个方法
Thread newThread (Runnable r)

ThreadPoolExecutor还有一个不常用的参数 RejectedExecutionHandler handler

线程池执行任务时,会遵循如下规则:
1.ThreadNum < CoreSize ,启动核心线程执行任务
2.ThreadNum >= CoreSize ,任务会插入到队列中排队等待执行
3.如果在步骤2中无法将任务插入到队列中,很可能是因为任务队列已满,这个时候会判断,线程数量是否大于最大线程值?
4.ThreadNum>MaximumSize,则将这个无法插入队列的任务交给一个新创建的非核心线程(所以从这里可以看出,线程池不能保证按照任务提交的顺序来执行);
5.ThreadNum < MaximumSize 如果大于的话,拒绝这个任务。(一般情况下,任务队列都比较大,所以,拒绝的情况较少,当然也要根据系统的具体情况来决定任务队列的大小)

ThreadPool

总结:实际上,我觉得线程池的思想就是,最好是用不大于 coreSize 的线程数量来执行任务,超过了就排队,可是如果要执行的任务实在太多了,Exxcutor迫于无奈,只能再开几个线程来执行它们,正常情况下,应该是队列里的任务先行,但是由于线程池的特殊性,一般不要求顺序执行,所以为了避免更多的消耗性能的操作或者同步,它将这个过程简化为直接让后面的任务先行

Android中CPU核心数的获取:

1
2
3
int CPU_COUNT = Runtime
.getRunTime()
.avaliableProcessors();

常用的配置策略:

线程池的分类四种常用类型)

1.FixedThreadPool: 线程数固定的线程池(只有核心线程,且核心线程不会被回收),当所有线程都处于活动状态时,新任务处于等待状态,直到有线程空出来。
通过 Executors 的 newFixedThreadPool来创建,没有超时机制,任务队列没有大小限制
2.CachedThreadPool: 只有非核心线程,且最大线程数为 Interger.MAX_VALUE(相当于任意大),当线程池中的人物都处于活动状态时,创建新的线程来处理任务,否则就利用空闲的线程来执行任务,线程的超时策略为60s。它非常适合用来执行大量任务
3.ScheduledThreadPool: 核心线程数是固定的,而非核心线程是不固定的,并且当非核心线程闲置时会被立即回收。它主要用于执行定时任务和具有固定周期的重复任务
4.SingleThreadExecutor: 只有一个核心线程,他能确保所有的任务都按顺序在同一个线程中执行(不需要处理线程同步问题)

To be continue

关于对于线程的控制 | 线程的启动与终止
关于以上各种线程/线程池的使用场景