Android 多线程之线程池(一)

作者: JingChen_ | 来源:发表于2020-12-25 16:58 被阅读0次

    一 为何使用线程池

    在我们日常的Android开发中,经常使用多线程来处理异步的任务,第一想到的就是new Thread来创建一个子线程来处理,但是呢,创建一两个还好,但是任务执行完系统会对子线程进行销毁,多个线程频繁地销毁,使性能降低,又非常耗时。

    总结一下造成的问题
    ① 在任务众多的情况下,系统要为每一个任务创建一个线程,而任务执行完毕后会销毁每一个线程,所以会造成线程频繁地创建与销毁。

    ② 多个线程频繁地创建会占用大量的资源,并且在资源竞争的时候就容易出现问题,同时这么多的线程缺乏一个统一的管理,容易造成界面的卡顿。

    ③ 多个线程频繁地销毁,会频繁地调用GC机制,这会使性能降低,又非常耗时。

    创建的线程过多,缺乏对线程的管理,为了解决这一类问题,才有了线程池

    线程池的好处

    • 1、重用线程池中的线程,减少线程的创建和销毁带来的开销。
    • 2、有效的控制线程的最大并发数,避免大量线程之间因为相互抢占系统资源而导致的阻塞现象。
    • 3、提供简单的管理,定时执行,指定间隔循环执行,线程资源常驻及释放。

    二 线程池工作原理

    Android中线程池的概念来源于Java中的Executor,具体实现为 ThreadPoolExecutor。可以通过它的构造参数来创建不同类型的线程池。

    线程池可以理解成一个装线程的池子。线程池创建和管理若干线程,在需要使用的时候可以直接从线程池中取出来使用,在任务结束之后闲置等待复用,或者销毁。

    线程池中的线程分为两种:核心线程和普通线程。
    1、核心线程即线程池中长期存活的线程,即使闲置下来也不会被销毁,需要使用的时候可以直接拿来用。
    2、普通线程则有一定的寿命,如果闲置时间超过寿命,则这个线程就会被销毁。

    创建线程池需调用ThreadPoolExecutor类,里面有很多种构造方法,但最终都调用了到同一个构造方法

      public ThreadPoolExecutor(int corePoolSize,
                                  int maximumPoolSize,
                                  long keepAliveTime,
                                  TimeUnit unit,
                                  BlockingQueue<Runnable> workQueue,
                                  ThreadFactory threadFactory) {
            ...
        }
    

    核心数 corePoolSize :刚才提及到的线程池中核心线程的数量。

    • 情况1:allowCoreThreadTimeout = flase(默认),核心线程会伴随线程池的整个生命周期一直存活,即使处于闲置
    • 情况2:allowCoreThreadTimeout = true,闲置的核心线程在等待新任务时可触发超时策略,时间间隔为keepAliveTime

    最大容量 maximumPoolSize:线程池最大允许保留多少线程。
    超时时间 keepAliveTime:线程池中线程的存活时间。
    unit:keepAliveTim时间属性的单位
    workQueue 任务队列:用于存放待处理的任务的一个阻塞队列
    threadFactory:线程工厂可用于设置线程名字等等一般无须设置该参数。

    在网上看到一个简单的例子:

        val threadPoolExecutor =
            ThreadPoolExecutor(3, 5, 1, TimeUnit.SECONDS, LinkedBlockingQueue<Runnable>(100))
    
        create.setOnClickListener {
            for (i in 1..30) {
                val runnable = Runnable {
                    try {
                        Thread.sleep(3000)
                        Log.e("Thread++", i.toString())
                        Log.e("当前线程++", Thread.currentThread().name)
                    } catch (e: InterruptedException) {
                        e.printStackTrace()
                    }
                }
                threadPoolExecutor.execute(runnable)
            }
        }
    

    分析:
    ① 创建一个线程池,3个核心线程,线程的最大数量为5个,1s的超时时间,一个容量为100的阻塞队列。

    ② 创建30个任务,每个任务延迟3s后执行,创建完后丢到线程池里面execute。

    猜测下点击事件触发后,日志是怎么打印的?
    1.每隔3s一个个打印?
    2.3s后全部打印出来?
    3.每隔3s打印三条?

    带着疑惑来分析下,线程池execute后做了什么?这里先具体说下流程,后面再分析源码:
    还要回到我们刚自定义创建线程池threadPoolExecutor里面的参数
    ① 线程池接收一个任务后,根据创建的线程池ThreadPoolExecutor(3, 5, 1, TimeUnit.SECONDS, LinkedBlockingQueue<Runnable>(100)),执行第一个任务时是没有运行的线程的,当运行线程少于核心线程数(corePoolSize)时,就要新建一个线程执行该任务,此时的线程为核心线程

    ② 当四个任务要被执行时,由于前三个任务已经占用了3个核心线程,此时就要用到刚才创建的任务队列了,当线程池中运行的线程等于核心线程时,就把后续要执行的任务放进任务队列,等正在执行任务的线程完成后,再从任务队列里面取任务执行

    ③ 三秒后,三个任务被执行打印出来,此时3个运行线程处理完当前任务了,就从任务队列取任务执行,也就是说,3个线程分别取了任务执行,后续基本重复该操作。

    打印的日志(截取部分,注意日志时间):

    2020-12-24 19:45:17.131 4506-4704/ E/Thread++: 1
    2020-12-24 19:45:17.131 4506-4705/ E/Thread++: 2
    2020-12-24 19:45:17.131 4506-4704/ E/当前线程++: pool-1-thread-1
    2020-12-24 19:45:17.131 4506-4705/ E/当前线程++: pool-1-thread-2
    2020-12-24 19:45:17.132 4506-4706/ E/Thread++: 3
    2020-12-24 19:45:17.132 4506-4706/ E/当前线程++: pool-1-thread-3
    2020-12-24 19:45:20.132 4506-4705/ E/Thread++: 5
    2020-12-24 19:45:20.132 4506-4704/ E/Thread++: 4
    2020-12-24 19:45:20.132 4506-4704/ E/当前线程++: pool-1-thread-1
    2020-12-24 19:45:20.132 4506-4705/ E/当前线程++: pool-1-thread-2
    2020-12-24 19:45:20.133 4506-4706/ E/Thread++: 6
    2020-12-24 19:45:20.133 4506-4706/ E/当前线程++: pool-1-thread-3
    2020-12-24 19:45:23.133 4506-4704/ E/Thread++: 7
    2020-12-24 19:45:23.133 4506-4705/ E/Thread++: 8
    2020-12-24 19:45:23.133 4506-4706/ E/Thread++: 9
    2020-12-24 19:45:23.133 4506-4705/ E/当前线程++: pool-1-thread-2
    2020-12-24 19:45:23.133 4506-4704/ E/当前线程++: pool-1-thread-1
    2020-12-24 19:45:23.133 4506-4706/ E/当前线程++: pool-1-thread-3
    ...
    

    可以看到,每个三秒打印3条线程的日志。

    那么把队列的设置为25的容量(ThreadPoolExecutor(3, 5, 1, TimeUnit.SECONDS, LinkedBlockingQueue<Runnable>(25))),会是怎么样结果呢?

    分析:
    当前三个任务执行的时候,把剩余的27个任务放进25容量的队列时,有两个是放不进去的,这个时候就用到我们设定的普通线程了,线程池最大容量为5个线程,其中核心线程为3个,其余的为普通线程,所以当队列放满后,就又要去创建2个线程来执行当前放不进去队列的2个任务

    日志如下(注意时间):

    2020-12-24 20:02:29.365 7188-7264/ E/Thread++: 1
    2020-12-24 20:02:29.365 7188-7265/ E/Thread++: 2
    2020-12-24 20:02:29.366 7188-7266/ E/Thread++: 3
    2020-12-24 20:02:29.366 7188-7266/ E/当前线程++: pool-1-thread-3
    2020-12-24 20:02:29.366 7188-7264/ E/当前线程++: pool-1-thread-1
    2020-12-24 20:02:29.366 7188-7265/ E/当前线程++: pool-1-thread-2
    2020-12-24 20:02:29.368 7188-7267/ E/Thread++: 29
    2020-12-24 20:02:29.368 7188-7267/ E/当前线程++: pool-1-thread-4
    2020-12-24 20:02:29.369 7188-7268/ E/Thread++: 30
    2020-12-24 20:02:29.369 7188-7268/ E/当前线程++: pool-1-thread-5
    2020-12-24 20:02:32.367 7188-7264/ E/Thread++: 6
    2020-12-24 20:02:32.367 7188-7266/ E/Thread++: 4
    2020-12-24 20:02:32.367 7188-7265/t E/Thread++: 5
    2020-12-24 20:02:32.367 7188-7266/ E/当前线程++: pool-1-thread-3
    2020-12-24 20:02:32.367 7188-7264/ E/当前线程++: pool-1-thread-1
    2020-12-24 20:02:32.367 7188-7265/ E/当前线程++: pool-1-thread-2
    2020-12-24 20:02:32.368 7188-7267/ E/Thread++: 7
    2020-12-24 20:02:32.368 7188-7267/ E/当前线程++: pool-1-thread-4
    2020-12-24 20:02:32.370 7188-7268/ E/Thread++: 8
    2020-12-24 20:02:32.370 7188-7268/ E/当前线程++: pool-1-thread-5
    ···
    

    刚开始前三个任务是跟之前一样,第29、30个任务由于放不进队列,由新建普通线程线程来执行,所以才跟前3个任务一起执行。3s后,由于此时有5个线程了,后续工作都由这5个线程从队列里面取任务出来执行。

    那么,当任务队列设定24的容量后执行会怎么样呢?

    分析:3个核心线程执行前3个任务,把剩下27个,放进24容量的队列,剩下3个,那就需要3个普通线程,但是线程池里面只能再创建2个普通线程,不满足,所以拒绝执行该任务,采取
    饱和策略,并抛出RejectedExecutionException异常。

    线程池调用execute(runnable)之后的流程:

    线程池工作原理.png

    三 常见的线程池

    在Executors工厂类中提供了多种线程池,典型的有以下四种:

    • FixedThreadPool 固定容量线程池
    • CachedThreadPool 缓存线程池
    • ScheduledThreadPool 调度线程池
    • SingleThreadExecutor 单线程线程池

    FixedThreadPool

    public static ExecutorService newFixedThreadPool(int nThreads) {
            return new ThreadPoolExecutor(nThreads, nThreads,
                                          0L, TimeUnit.MILLISECONDS,
                                          new LinkedBlockingQueue<Runnable>());
        }
    

    可以看到,创建的时候传了个nThreads,核心线程数为nThreads,最大线程数为nThreads。这个线程池只有核心现场,且核心线程不会超时(keepAliveTime为0L),LinkedBlockingQueue<Runnable>()为无界阻塞队列(无界的容量)。

    优点:响应任务的速度快,任务量和吞吐量饱和时,任务处理效率最大化。
    缺点:并发能力较弱,吞吐量>任务数量时不可避免会造成资源浪费(主要是内存)。

    应用场景:处理需要长期快速响应,无很高的并发效率要求。对批量任务执行无较高的时间等待要求。

    CachedThreadPool

    public static ExecutorService newCachedThreadPool() {
            return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                          60L, TimeUnit.SECONDS,
                                          new SynchronousQueue<Runnable>());
        }
    

    核心线程数为0,最大线程数无上限,线程超时时间60秒,所有线程均为普通线程。当有新任务到来,则插入到SynchronousQueue中,由于SynchronousQueue是同步队列(一个无法存储元素的队列),因此会在池中寻找可用线程来执行,若有可用线程则执行,若没有可用线程则创建一个线程来执行该任务;若池中线程空闲时间超过指定大小(60秒),则该线程会被销毁。

    优点:所有任务都会被立即分配线程执行,几乎可以理解为一个突破手。用于处理集中并发任务。在全部线程都超时后,这个线程池几乎是不占任何系统资源的(我将这里类比为主动new N个普通线程)
    缺点:随着任务数量激增可能会导致系统资源匮乏,导致线程阻塞。

    应用场景:处理大量耗时较少的任务或者负载较轻的服务器

    ScheduledThreadPool

    public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
            return new ScheduledThreadPoolExecutor(corePoolSize);
     }
    
     public ScheduledThreadPoolExecutor(int corePoolSize) {
         super(corePoolSize, Integer.MAX_VALUE, DEFAULT_KEEPALIVE_MILLIS, NANOSECONDS,
               new DelayedWorkQueue());
     }
    

    核心线程数自定,最大线程数无上限,创建一个固定大小的线程池,线程池内线程存活时间无限制,线程池可以支持定时及周期性任务执行,如果所有线程均处于繁忙状态,对于新任务会进入DelayedWorkQueue队列中,这是一种按照超时时间排序的队列结构。

    优点:非核心线程10s(DEFAULT_KEEPALIVE_MILLIS为10s)就会被即时回收,释放速度快,吞吐量大于FixedThreadPool 响应速度略高于CachedThreadPool。

    应用场景:使用schedule()方法处理定时任务,或处理固定周期的重复任务,执行完后直到下次执行期间,尽量少的占用资源。可以参考这个配置自定义线程池,更好的适应具体场景,比如将DEFAULT_KEEPALIVE_MILLIS 设置为0s,加速资源释放。

    SingleThreadExecutor

    public static ExecutorService newSingleThreadExecutor() {
            return new FinalizableDelegatedExecutorService
                (new ThreadPoolExecutor(1, 1,
                         0L, TimeUnit.MILLISECONDS,
                         new LinkedBlockingQueue<Runnable>()));
        }
    

    核心线程数为1,最大线程数为1,也就是说SingleThreadExecutor这个线程池中的线程数固定为1。创建只有一个线程的线程池,且线程的存活时间是无限的;当该线程正繁忙时,对于新任务会进入阻塞队列中(无界的阻塞队列)

    应用场景:处理需要线程同步的任务。

    本章主要讲述了线程池的基本使用和常见的线程池类型,下章将对execute后进行源码分析Android 多线程之线程池(二)

    End

    相关文章

      网友评论

        本文标题:Android 多线程之线程池(一)

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