引言
在Android当中根据用途分为主线程与子线程,主线程当中主要处理与界面相关的操作,子线程主要进行耗时操作。除了Thread本身以外,在Android当中还有很多扮演者线程的角色,比如AsyncTask( 底层为线程池,但是现在并不推荐使用)、IntentService和一个特殊的线程HandlerThread。
对于不同的线程有不同的使用场景,AsyncTask封装了线程池和Handler,主要是为了在子线程里面更新UI。HandlerThread是一种具有消息循环的线程,它的内部可以使用Handler。IntentService是一个服务,系统对内部进行了封装使其更方便的进行后台服务,内部采用HandlerThread来执行任务,当任务执行完毕IntentService会自动退出,它的作用很像一个后台进程(被弃用,WorkManager
或 JobIntentService
。WorkManager
是 Google 推荐的用于执行后台任务的解决方案,它支持一次性任务和周期性任务,并能够处理任务的重试、链式依赖等。而 JobIntentService
可以在后台处理任务,并且在需要时重新启动服务,适用于需要向后兼容较旧的 Android 版本的场景)。
在操作系统当中,线程是操作系统调度的最小单元, 同时线程又是一种受限的系统资源,即线程不可以无限制的产生,并且线程的创建和销毁都会有相应的开销。当系统当中存在大量的线程的时候,系统会通过时间片轮转的方式调度线程,因此线程不可能做到绝对的并行,除非线程数量小于CPU的核心数,但一般来说这是不可能的。但是在程序当中频繁创建和销毁线程显然不是高效的做法,应该采用线程池,接下来就看看Android中的线程池吧!
使用线程池的优点
- 重用线程池中的线程,避免因为线程的创建和销毁所带来的性能开销。
- 能有效控制线程池的最大并发数,避免大量的线程之间因互相抢占系统资源而导致的阻塞现象。
- 能够对线程进行简单的管理,并提供定时执行以及指定间隔循环执行等功能。
ThreadPoolExecutor
ThreadPoolExecutor
是 Java 中 Executor
框架的一部分,它实现了 Executor
接口和 ExecutorService
接口。这个类允许你创建一个线程池,并且可以控制任务的并发执行,它是线程池的核心实现类。
一共有4个构造方法,接下来我们就看看拥有最多参数的构造方法
- corePoolSize(核心线程数)
默认情况下线程池是空的,只有提交任务时才会创建线程。如果当前运行的线程数少于corePoolSize
,则会创建新的线程来处理任务;如何当前运行的线程数等于或者多于corePoolSize
,则不会创建新线程。核心线程通常不会被回收(除非设置了允许回收的核心线程数)。如果调用线程池的prestartAllcoreThread
方法,则线程池会提前创建并开启所有的核心线程来处理任务。
- maximumPoolSize(最大线程数)
这是线程池中允许的最大线程数量,包括核心线程和非核心线程。当队列满了并且正在执行的线程数少于最大线程数时,线程池会尝试创建新的线程来处理任务。如果队列满了且线程数已达到最大线程数,新提交的任务将被拒绝。
- keepAliveTime(非核心线程空闲存活时间)
这是非核心线程在终止前等待新任务的最长时间。当线程池中的线程数超过核心线程数时,这些额外的线程(非核心线程)在空闲时会等待新任务的到来。如果超过这个时间还没有新任务,线程将被回收。对于核心线程,这个参数无效,除非设置了允许回收的核心线程数(allowCoreThreadTimeOut(true)
方法来设置)。
- unit(时间单位)
这是keepAliveTime
参数的时间单位,可以是毫秒、秒、分钟等。
- workQueue(工作队列)
这是一个阻塞队列,用于存放待执行的任务。当所有核心线程都在忙碌时,新提交的任务会被放入这个队列中。如果队列满了,线程池会尝试创建新的线程来处理任务,直到达到最大线程数。
- threadFactory(线程工厂)
这是一个ThreadFactory
对象,用于创建新线程。线程工厂允许你自定义线程的创建过程,例如设置线程的名称、优先级、守护状态等。默认的线程工厂通常就足够了,但自定义线程工厂可以提供更多的控制和调试信息。
- handler(拒绝/饱和策略)
这是一个RejectedExecutionHandler
对象,用于处理当任务太多,无法被线程池及时处理时的情况,即任务队列和线程池都满了的情况。常见的拒绝策略有:
- AbortPolicy:默认策略,表示无法处理新任务,抛出
RejectedExecutionException
。 - CallerRunsPolicy:在调用者的线程中执行任务。
- DiscardPolicy:默默丢弃无法处理的任务。
- DiscardOldestPolicy:丢弃队列中最旧的任务,重新提交当前的新任务。
线程池的处理流程与原理
根据流程图我们可以看到,当我们执行ThreadPoolExecutor的execute方法,会有各种的情况:
- 如果线程池中的线程数未达到核心线程数,则创建核心线程处理任务
- 如果线程数大于或等于核心线程数,则将任务加入任务队列,线程池中的空闲线程会不断地从任务队列中取出任务进行处理
- 如果任务队列满了,并且线程数没有达到最大线程数,则创建非核心线程去处理任务
- 如果线程数超过了最大线程数,则执行饱和策略
线程池的种类
我们可以直接或者间接的通过配置来实现自己的线程池的功能特性。
FixedThreadPool
先来看看它的构造函数:
java
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
nThreads
:核心线程数和最大线程数都被设置为nThreads
,这意味着线程池的大小是固定的,不会动态变化。0L
:非核心线程的空闲存活时间被设置为0。由于所有线程都是核心线程,这个值实际上并不会影响线程池的行为。TimeUnit.MILLISECONDS
:空闲存活时间的时间单位是毫秒。new LinkedBlockingQueue<Runnable>()
:工作队列是一个无界的LinkedBlockingQueue
。由于线程池的大小是固定的,这个无界队列意味着如果所有线程都在忙碌,新提交的任务将会被放入队列中,直到队列满为止。
是可重用固定线程数的线程池,在一开始创建就已经规定了线程数,意味着只有核心线程没有非核心线程,即创建的都是核心线程,并且这些线程会一直存活直到线程池被关闭,即使它们处于空闲状态也不会被回收。它的提交任务执行示意图:
CachedThreadPool
java
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
0
:核心线程数被设置为0,这意味着线程池在初始时没有任何线程,线程池中的线程都是非核心线程。Integer.MAX_VALUE
:最大线程数被设置为Integer.MAX_VALUE
(约21亿),这意味着线程池理论上可以创建非常多的线程。但由于实际物理和操作系统资源的限制,这个数字通常不会达到。60L
:非核心线程的空闲存活时间被设置为60秒。当线程池中的线程空闲超过这个时间,它们将被回收。
CachedThreadPool
线程池,它会根据需要创建新线程,但如果线程空闲超过一定时间(默认60秒),则会被回收。这种线程池适合执行很多短期异步任务的程序。
SingleThreadExecutor
SingleThreadExecutor是使用单个线程的线程池,当当前没有运行的线程的时候,就会创建一个新线程来处理任务,如果有运行的线程就将其添加到阻塞队列当中。
java
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
1
:核心线程数和最大线程数都被设置为1,这意味着线程池始终只有一个线程。0L
:非核心线程的空闲存活时间被设置为0。由于只有一个线程,这个值实际上并不会影响线程池的行为。
ScheduledThreadPool
ScheduledThreadPool是一个能实现和定时和周期性任务的线程池。
java
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE,
DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
new DelayedWorkQueue());
}
new DelayedWorkQueue()
:工作队列,这里使用了一个延迟工作队列。这个队列可以存储待执行的任务,并按照任务的延迟时间进行排序,确保最早需要执行的任务可以被优先处理
当执行ScheduledThreadPoolExecutor
的scheduleAtFixedRate
或者scheduleWithFixedDelay
方法时,会向 DelayedWorkQueue
添加一个实现 RunnableScheduledFuture
接口的 ScheduledFutureTask
(任务的包装类),并会检查运行的线程数是否达到了corePoolSize
(核心线程数)。如果没有达到,则新建线程并启动它,但并不是立即去执行任务,而是去DelayedWorkQueue
中取ScheduledFutureTask
,然后执行任务。如果运行的线程数达到了corePoolSize时,则将任务添加到 DelayedWorkQueue
中。DelayedWorkQueue
会将任务进行排序,先要执行的任务放在队列的前面。其跟此前介绍的线程池不同的是,当执行完任务后,会将ScheduledFutureTask
中的 time变量改为下次要执行的时间并放回 DelayedWorkQueue
中。
总结
线程池类型 | 特点 | 适用场景 | 核心线程数 | 最大线程数 | 空闲线程存活时间 |
---|---|---|---|---|---|
FixedThreadPool | 拥有固定数量的线程,线程数不变。 | 负载较重的服务器,需要限制线程数量的场景。 | 固定 | 固定 | 无 |
CachedThreadPool | 根据需要创建新线程,空闲线程会被回收。 | 执行很多短期异步任务的程序。 | 0 | Integer.MAX_VALUE |
60秒 |
ScheduledThreadPool | 可以安排在给定延迟后运行命令或定期地执行。 | 需要任务在后台定期执行或重复执行的程序。 | 固定 | 固定 | 60秒 |
SingleThreadExecutor | 只有一个线程,所有任务按照提交顺序依次执行。 | 需要保证任务顺序执行的场景。 | 1 | 1 | 无 |
文章到这里就结束了!