程序员Android开发经验谈Android开发

Android 多线程之线程池(一)

2020-12-25  本文已影响0人  JingChen_

一 为何使用线程池

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

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

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

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

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

线程池的好处

二 线程池工作原理

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

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

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

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

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

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

最大容量 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

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

上一篇下一篇

猜你喜欢

热点阅读