一 为何使用线程池
在我们日常的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)之后的流程:
三 常见的线程池
在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。创建只有一个线程的线程池,且线程的存活时间是无限的;当该线程正繁忙时,对于新任务会进入阻塞队列中(无界的阻塞队列)
应用场景:处理需要线程同步的任务。