美文网首页
【总结】Android中的线程

【总结】Android中的线程

作者: 械勒的时间 | 来源:发表于2019-01-14 18:07 被阅读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);
    

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

    相关文章

      网友评论

          本文标题:【总结】Android中的线程

          本文链接:https://www.haomeiwen.com/subject/uecelqtx.html