【总结】Android中的线程
概述
线程是操作系统调度的最小单元,且又是一种有限资源,它的创建和销毁都会有相应的系统开销。
若线程数量大于CPU核心数量(一般来说,线程数量都会大于CPU核心数量),系统会通过时间片轮转的方式调度每一个线程。
频繁的创建和销毁线程,所带来的系统开销巨大,需要通过线程池来避免这个问题。
Android沿用了JAVA的线程模型,分为主线程与子线程。
主线程是指进程所拥有的线程,默认情况下,一个进程只有一个线程,即主线程。主线程用来运行四大组件,以及处理界面相关的逻辑。主线程为了保持较高的响应速度,不能执行耗时操作,否则会出现界面卡顿。
子线程又叫做工作线程,除了主线程以外的线程都叫做子线程。子线程用来处理耗时任务,比如网络请求,I/O操作等。
Android中线程的形态
Android中,可以作为线程的类,除了传统的Thread以外,还有AsyncTask、IntentService、HandlerThread,他们的底层实现也是线程,但他们有特殊的表现形式,使用起来也各有优缺点。
AsyncTask
AsyncTask是一种轻量的异步类,内部封装了线程池和Handler。在线程池中执行后台任务,并把执行的进度和结果传递给主线程,主要被用来在子线程中更新UI。
AsyncTask是一个抽象泛型类,他的声明如下
public abstract class AsyncTask<Params, Progress, Result>
其中,
Params表示参数类型;
Progress表示后台任务执行进度的类型;
Rusult表示后台任务返回值的类型。
AsyncTask有几个常用的回调方法,他们的作用分别为:
- onPreExecute(),在主线程中执行,异步任务执行之前调用,用来做些准备工作;
- doInBackground(Params... params),在线程池中执行,用于执行异步任务。并且在此方法中,可以通过调用publishProgress方法来更新任务进度。此方法还要提供返回值给onPostExecute;
- onProgressUpdate(Progress... value),在主线程中执行,后台任务执行进度发生改变时调用。publishProgress方法会调用onProgressUpdate方法,不要直接调用它;
- onPostExecute(Result result),在主线程中执行,异步任务之后被调用。result即为doInBackground提供的返回值。
- onCancelled(),在主线程中执行,当异步任务被取消的时候会被调用。
这几个方法的执行顺序是onPreExecute->doInBackground->onPostExecute。当异步任务被取消时,onCancelled会被调用,此时onPostExecute不会被调用。
一个例子:
class Download extends AsyncTask<String, Integer, Integer> {
@Override
protected void onPreExecute() {
// 在主线程中执行,异步任务执行之前调用,用来做些准备工作
super.onPreExecute();
}
@Override
protected Integer doInBackground(String... strings) {
// 在线程池中执行,用于执行异步任务。并且在此方法中,可以通过调用publishProgress方法来更新任务进度。此方法还要提供返回值给onPostExecute
tv_text.setText("TheThread 准备下载: " + strings[0]);
int i = 0;
for (; i < 100; i++) {
SystemClock.sleep(1000);
publishProgress(i);
}
return i;
}
@Override
protected void onProgressUpdate(Integer... values) {
// 在出现场中执行,后台任务执行进度发生改变时调用。publishProgress方法会调用onProgressUpdate方法,不要直接调用它
tv_text.setText("TheThread 正在进行: " + values[0]);
super.onProgressUpdate(values);
}
@Override
protected void onPostExecute(Integer integer) {
// 在主线程中执行,异步任务之后被调用。result即为doInBackground提供的返回值
tv_text.setText("TheThread 完成: " + integer);
super.onPostExecute(integer);
}
@Override
protected void onCancelled() {
// 在主线程中执行,当异步任务被取消的时候会被调用
super.onCancelled();
}
}
...
// 执行
new Download().execute("一个文件");
下载也是AsyncTask常见用途,下载过程中可以通过publishProgress更新进度条,当下载完成,也就是doInBackground给出返回值时,onPostExecute会被调用代表这个任务已经结束。
AsyncTask在使用时,也有一些限制条件:
- 一个AsyncTask只能调用一次execute;
- AsyncTask执行任务是串行执行的,若想并行执行,需要调用executeOnExecutor方法。
PS:关于网上一个讨论
《安卓开发艺术探索》:
AsyncTask的对象必须在主线程中创建
execute方法必须在UI线程中调用
书中讲得很清楚,必须在主线程中首次加载,是因为AsyncTask底层用到了Handler,在AsyncTask加载时会初始化其内部的Handler。但是在4.1以后,ActivityThread的main方法会调用AsyncTask的init方法,此时其内部的Handler已被初始化,所以现在在子线程中调用AsyncTask的创建并execute也没问题。
所以书上的这句话大概是笔误?
// 可以正常执行不会报错的代码
new Thread(new Runnable() {
@Override
public void run() {
new Download().execute("一个文件");
}
}).start();
HandlerThread
HandlerThread是Thread,特殊之处在于它的内部,主动开启了消息循环Looper。
结合Hanlder的内容,HandlerThread其实很好理解。我们知道如果在Activity中要使用Handler,是不需要刻意创建Looper的,因为Activity会为我们创建好一个Looper供我们使用,但是在子线程中使用Handler就必须自己创建Looper,否则会报错。HandlerThread就是为我们提供了一个自带Looper的Thread,作用主要是为我们提供一个存在于子线程中的Looper。
普通Thread是通过run方法执行一个任务,HandlerThread需要通过一个Handler的消息的方式来执行一个任务,它的run方法是一个无限循环,在不使用的时候要通过quit方法来终止其线程的执行。
HandlerThread适用于会长时间在后台运行,间隔触发的情况,比如实时更新。这就是谷歌爸爸给开发者提供的一个轮子,当然自己根据这个原理实现的话,也不差。
HandlerThread的主要应用场景是IntentService。
IntentService
IntentService是一种特殊的Service,它是一个抽象类,内部封装了HandlerThread和Handler。可以用于执行后台耗时的任务,完成后会自动停止。由于他是一个Service,所以它的优先级比单纯的线程要高很多,不容易被系统杀死。
当IntentService第一次启动时,会创建一个HandlerThread,再通过这个HandlerThread的Looper来构造一个Handler对象mServiceHandler。因为HandlerThread的Looper是在子线程中初始化的,所以mServiceHandler会把从主线程(Service线程)中的任务拿到子线程中执行,从而避免在Service线程中执行耗时操作导致ANR。
PS:为什么要使用HandlerThread类?因为HandlerThread类的Looper是子线程中的Looper。如果在当前类中(IntentService类)直接获取Looper的话,获取到的是主线程(Server线程)中的Looper,如果在IntentServer中创建一个子线程再获取Looper的话就相当于是又实现了一次HandlerThread,所以直接使用HandlerThread。
IntentService有一个抽象方法onHandlerIntent,需要在子类中实现。
protected void onHandleIntent(@Nullable Intent intent) {}
他的参数intent来源于IntentService,就是从其他组件中传递过来的Intent原模原样的传递给onHandleIntent方法。
每次启动IntentService,都会通过mServiceHandler发送一个消息,然后传递到HandlerThread中处理。所有传递进来的消息会进入Handler的消息队列中,等待被Looper检索并执行,等待所有消息都处理完毕后,IntentService会停止服务。
工作流程如下:
IntentService工作流程.png
与使用Handler在子线程中操作UI原理相同,IntentService是将UI线程中的耗时操作切换到子线程中执行。
以及示例:
public class MyIntentService extends IntentService {
private final String TAG = this.getClass().getSimpleName();
public final static String action = "ACTION";
public MyIntentService() {
super(action);
}
@Override
protected void onHandleIntent(@Nullable Intent intent) {
String name = intent.getStringExtra(action);
Log.e(TAG, "下载文件:" + name);
SystemClock.sleep(3000);
Log.e(TAG, name + "下载完成");
}
@Override
public void onDestroy() {
super.onDestroy();
Log.e(TAG, "销毁");
}
}
Intent service = new Intent(AsyncTaskActivity.this, MyIntentService.class);
service.putExtra(MyIntentService.action, "一号文件");
AsyncTaskActivity.this.startService(service);
service.putExtra(MyIntentService.action, "二号文件");
AsyncTaskActivity.this.startService(service);
01-10 15:48:16.124 30551-30725/com.zx.studyapp E/MyIntentService: 下载文件:一号文件
01-10 15:48:19.124 30551-30725/com.zx.studyapp E/MyIntentService: 一号文件下载完成
01-10 15:48:19.126 30551-30725/com.zx.studyapp E/MyIntentService: 下载文件:二号文件
01-10 15:48:22.126 30551-30725/com.zx.studyapp E/MyIntentService: 二号文件下载完成
01-10 15:48:22.128 30551-30551/com.zx.studyapp E/MyIntentService: 销毁
同样是假设下载任务,可以看到依次下载第一个与第二个文件,所有任务执行完成之后,IntentService便自行销毁。
线程池
其实这部分属于Java知识,而非Android。
线程池有三好:简单,重用,不阻塞。
- 简单。对于大量线程的管理简单。
- 重用。重用线程池中的线程,减少性能开销。
- 不阻塞。能有效控制最大并发数,避免大量线程抢占资源导致的系统阻塞。
Android的线程池来源于他爹Java的Executer接口,其实现为ThreadPoolExecutor 。它提供了一系列的参数来配置线程池。
ThreadPoolExecutor
是Android线程池的真正的实现,他的构造方法提供了一系列的参数来配置线程池。
public ThreadPoolExecutor(
int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory)
corePoolSize:线程池的核心线程数。默认情况下核心线程会一直存活,如果将ThreadPoolExecutor的allowCoreThreadTimeOut属性设置为true则会超时,这个时间由keepAliveTime给出,超时的线程会被终止。
maximumPoolSize:线程池最大线程数量,超出任务会被阻塞
keepAliveTime:非核心线程超时时间,超时线程会被回收。
unit:枚举类型,用于指定keepAliveTime的时间单位。常用的类型有TimeUnit.MICROSECONDS(毫秒),TimeUnit.SECONDS(秒),TimeUnit.MINUTES(分钟)等。
workQueue:线程池的任务队列。
threadFactory:线程工厂,为线程池提供创建新线程的功能。
此外还有一个不常用的参数,RejectedExecutionHandler handler。当线程池无法执行新任务时,handler的rejectedExecution方法会被调用抛出异常。
线程池执行任务的时候遵循以下规则:
- 如果线程池中的线程数量小于核心线程的总数,则会启动一个核心线程来执行任务。
- 如果线程池中的线程数量大于等于核心线程总数,任务会被插入任务队列中等待。
- 若任务队列已满,但线程数量没有达到线程池的最大值,则会启动一个非核心线程来执行任务。
此处需要注意,启动一个非核心线程立即执行任务,而非从队列中读取一个任务,就是说,这个场景下,后来的任务可能会先被执行。看以下例子:
ThreadPoolExecutor executor = new ThreadPoolExecutor(1,//一个核心线程
10,//10个非核心线程
1,
TimeUnit.MINUTES,//非核心超时时间1分钟
new LinkedBlockingQueue<Runnable>(2));//任务队列长度为2
在这个线程池中,我们直接执行4个任务,从直觉上来说,应该是先来先执行,但是实际情况不一定。
Runnable run1 = new Runnable() {
@Override
public void run() {
Log.e(TAG, "start,run: 1");
SystemClock.sleep(5000);
Log.e(TAG, "end,run: 1");
}
};
Runnable run2 = ...;
Runnable run3 = ...;
Runnable run4 = ...;
executor.execute(run1);
executor.execute(run2);
executor.execute(run3);
executor.execute(run4);
01-11 17:53:34.263 9549-9690/com.zx.studyapp E/ThreadPoolExecutor: start,run: 1
01-11 17:53:34.263 9549-9691/com.zx.studyapp E/ThreadPoolExecutor: start,run: 4
01-11 17:53:39.264 9549-9690/com.zx.studyapp E/ThreadPoolExecutor: end,run: 1
01-11 17:53:39.264 9549-9691/com.zx.studyapp E/ThreadPoolExecutor: end,run: 4
01-11 17:53:39.264 9549-9690/com.zx.studyapp E/ThreadPoolExecutor: start,run: 2
01-11 17:53:39.264 9549-9691/com.zx.studyapp E/ThreadPoolExecutor: start,run: 3
01-11 17:53:44.264 9549-9691/com.zx.studyapp E/ThreadPoolExecutor: end,run: 3
01-11 17:53:44.264 9549-9690/com.zx.studyapp E/ThreadPoolExecutor: end,run: 2
结果是,先执行1,4,再执行2,3。
PS:学校学的东西都还给老师了,这种基础问题都要想好久,老师我对不起你。
- 如果线程数量也达到了线程池的最大值,则此任务会被拒绝,并通过RejectedExecutionHandler抛出异常。
线程池的分类
最后介绍四种常见的线程池,他们都是同过配置ThreadPoolExecutor来实现的。
- FixedThreadPool 线程数量固定的线程池。
是一个固定线程数的线程池,它的任务队列大小没有限制,并且没有超时。若提交新任务时,所有核心线程都处于活动状态,那么新任务会进行等待,直到有核心线程空出来。在线程池被关闭之前,池中的线程将一直存在。
它的优势是,可以更加快速的响应外界请求。
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
Runnable run = new Runnable() {...};
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(10);
fixedThreadPool.execute(run);
- SingleThreadExecutor 单线程的线程池
是一个只有一条线程的线程池,它的任务队列没有大小限制。
它的优势是,可以保证所有任务都是顺序执行的,因为所有任务都是在同一线程执行,所以不用考虑线程同步的问题。
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
Runnable run = new Runnable() {...};
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
singleThreadExecutor.execute(run);
关于SingleThreadExecutor与newFixedThreadPool(1)的区别,Java文档上有这么一句话
Unlike the otherwise equivalent {@code newFixedThreadPool(1)} the returned executor is guaranteed not to be reconfigurable to use additional threads.
翻阅stackoverflow的一些帖子,明白它的大致意思就是,SingleThreadExecutor就有且只能有一条线程,无法通过某些方法变成多条线程的线程池。比如看下面一个例子:
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(1);
ThreadPoolExecutor poolExecutor = (ThreadPoolExecutor) fixedThreadPool;
poolExecutor.setCorePoolSize(10);
而SingleThreadExecutor 由于加了包装类FinalizableDelegatedExecutorService,隐藏了一些方法,使得无法配置线程池,就可以保证它永远就只有一条线程了。
- CachedThreadPool 线程数量不固定的线程池
是一个线程数量不固定,根据需要创建线程的线程池。只有非核心线程,最大线程数可以认为是无穷大。如果有新任务加入进来,但是没有空闲线程,则会创建一个新线程并添加到线程池中。线程超时时间为60s,超时线程会被回收掉。
它的优势是,对于执行很多短期异步任务的程序而言,这些线程池通常可提高程序性能。并且在没有任务执行时,他几乎是不占资源的。
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
Runnable run = new Runnable() {...};
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
cachedThreadPool.execute(run);
可以使用ThreadPoolExecutor构造方法创建具有类似属性但细节不同(例如超时参数)的线程池。
- ScheduledThreadPool 计划线程池
是一个核心线程数量固定,非核心线程没有数量限制的一个线程池,且超时时间为0s,执行完成会被立刻回收。
这类线程池主要用于执行定时任务和重复任务。
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
public ScheduledThreadPoolExecutor(int corePoolSize){
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS, new DelayedWorkQueue());
}
Runnable run = new Runnable() {...};
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(10);
scheduledThreadPool.execute(run);
个人理解,难免有错误纰漏,欢迎指正。转载请注明出处。