【总结】Android中的线程

概述

线程是操作系统调度的最小单元,且又是一种有限资源,它的创建和销毁都会有相应的系统开销。
若线程数量大于CPU核心数量(一般来说,线程数量都会大于CPU核心数量),系统会通过时间片轮转的方式调度每一个线程。
频繁的创建和销毁线程,所带来的系统开销巨大,需要通过线程池来避免这个问题。

Android沿用了JAVA的线程模型,分为主线程与子线程。
主线程是指进程所拥有的线程,默认情况下,一个进程只有一个线程,即主线程。主线程用来运行四大组件,以及处理界面相关的逻辑。主线程为了保持较高的响应速度,不能执行耗时操作,否则会出现界面卡顿。
子线程又叫做工作线程,除了主线程以外的线程都叫做子线程。子线程用来处理耗时任务,比如网络请求,I/O操作等。

Android中线程的形态

Android中,可以作为线程的类,除了传统的Thread以外,还有AsyncTaskIntentServiceHandlerThread,他们的底层实现也是线程,但他们有特殊的表现形式,使用起来也各有优缺点。

AsyncTask

AsyncTask是一种轻量的异步类,内部封装了线程池和Handler。在线程池中执行后台任务,并把执行的进度和结果传递给主线程,主要被用来在子线程中更新UI。

AsyncTask是一个抽象泛型类,他的声明如下

public abstract class AsyncTask<Params, Progress, Result>

其中,
Params表示参数类型;
Progress表示后台任务执行进度的类型;
Rusult表示后台任务返回值的类型。

AsyncTask有几个常用的回调方法,他们的作用分别为:

  1. onPreExecute(),在主线程中执行,异步任务执行之前调用,用来做些准备工作;
  2. doInBackground(Params... params),在线程池中执行,用于执行异步任务。并且在此方法中,可以通过调用publishProgress方法来更新任务进度。此方法还要提供返回值给onPostExecute;
  3. onProgressUpdate(Progress... value),在主线程中执行,后台任务执行进度发生改变时调用。publishProgress方法会调用onProgressUpdate方法,不要直接调用它;
  4. onPostExecute(Result result),在主线程中执行,异步任务之后被调用。result即为doInBackground提供的返回值。
  5. 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在使用时,也有一些限制条件:

  1. 一个AsyncTask只能调用一次execute;
  2. 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。
线程池有三好:简单,重用,不阻塞。

  1. 简单。对于大量线程的管理简单。
  2. 重用。重用线程池中的线程,减少性能开销。
  3. 不阻塞。能有效控制最大并发数,避免大量线程抢占资源导致的系统阻塞。
    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方法会被调用抛出异常。

线程池执行任务的时候遵循以下规则:

  1. 如果线程池中的线程数量小于核心线程的总数,则会启动一个核心线程来执行任务。
  2. 如果线程池中的线程数量大于等于核心线程总数,任务会被插入任务队列中等待。
  3. 若任务队列已满,但线程数量没有达到线程池的最大值,则会启动一个非核心线程来执行任务。
    此处需要注意,启动一个非核心线程立即执行任务,而非从队列中读取一个任务,就是说,这个场景下,后来的任务可能会先被执行。看以下例子:
    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:学校学的东西都还给老师了,这种基础问题都要想好久,老师我对不起你。

  1. 如果线程数量也达到了线程池的最大值,则此任务会被拒绝,并通过RejectedExecutionHandler抛出异常。

线程池的分类

最后介绍四种常见的线程池,他们都是同过配置ThreadPoolExecutor来实现的。

  1. 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);
  1. 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,隐藏了一些方法,使得无法配置线程池,就可以保证它永远就只有一条线程了。

  1. 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构造方法创建具有类似属性但细节不同(例如超时参数)的线程池。

  1. 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);

个人理解,难免有错误纰漏,欢迎指正。转载请注明出处。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容