【总结】Android中的线程

2019-01-14  本文已影响0人  械勒的时间

概述

线程是操作系统调度的最小单元,且又是一种有限资源,它的创建和销毁都会有相应的系统开销。
若线程数量大于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);

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

上一篇下一篇

猜你喜欢

热点阅读