Android 多线程之线程池(一)

一 为何使用线程池

在我们日常的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

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

推荐阅读更多精彩内容