在Android开发中,我们经常会遇到需要执行耗时操作的情况,例如网络请求、数据库读写、图片加载等。为了避免这些操作阻塞主线程,我们通常会使用线程池来管理并发执行任务。而Android Executors是一个非常常用的线程池管理工具。本文将深入解析Android Executors的原理,帮助大家更好地理解和使用这个工具。
Android线程池简介
在移动应用中,频繁地创建和销毁线程可能导致系统资源的浪费。线程池通过维护一组可重用的线程,降低了线程创建和销毁的开销。这种机制使得线程的使用更加高效,同时能够控制并发线程的数量,防止系统资源被过度占用。
线程池的作用和优势
-
任务队列管理: 线程池通过任务队列管理待执行的任务,确保它们按照一定的顺序执行,提高了任务的执行效率。
-
资源控制: 可以限制并发执行的线程数量,避免资源耗尽和竞争条件的发生。
-
线程复用: 线程池维护一组可复用的线程,减少了线程的创建和销毁,提高了系统的性能。
Executors框架概述
java.util.concurrent.Executors
是Java中用于创建线程池的工厂类。它提供了一系列静态方法,可以方便地创建不同类型的线程池。这些线程池类型包括CachedThreadPool、FixedThreadPool、ScheduledThreadPool和SingleThreadExecutor等。
通过使用Executors
,可以简化线程池的创建过程,专注于任务的实现和调度。
Executors工厂方法
Executors
类提供了几个常用的工厂方法:
-
newCachedThreadPool(): 创建一个可根据需要创建新线程的线程池,但在可用时将重用先前构造的线程。
-
newFixedThreadPool(int n): 创建一个具有固定线程数的线程池,超过的任务会在队列中等待。
-
newScheduledThreadPool(int corePoolSize): 创建一个线程池,它可调度在给定的延迟之后运行命令,或者定期执行命令。
-
newSingleThreadExecutor(): 创建一个使用单个 worker 线程的线程池,以无界队列方式来运行该线程。
任务提交和执行
一旦线程池创建完成,可以通过将任务提交给线程池来执行。任务可以是实现了Runnable
接口的普通线程,也可以是实现了Callable
接口的带返回值的线程。
ExecutorService executorService = Executors.newFixedThreadPool(5);
executorService.submit(() -> {
// 执行任务逻辑
});
// 关闭线程池
executorService.shutdown();
线程池有一个生命周期,它包括创建、运行和关闭三个阶段。创建后线程池可以接受并执行任务,一旦不再需要,可以通过调用shutdown()
或shutdownNow()
方法来关闭线程池。
ThreadPoolExecutor
ThreadPoolExecutor
是java.util.concurrent
包中的核心类之一,用于实现自定义的线程池。理解ThreadPoolExecutor
的工作原理对于深入掌握线程池的使用至关重要。
核心参数解释
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
threadFactory, defaultHandler);
}
ThreadPoolExecutor
的构造方法包括多个参数,其中一些是线程池的核心参数:
-
corePoolSize: 线程池的核心线程数,即线程池维护的最小线程数。
-
maximumPoolSize: 线程池的最大线程数,即线程池允许的最大线程数。
-
keepAliveTime: 当线程池线程数量超过核心线程数时,多余的空闲线程在终止前等待新任务的最长时间。
-
workQueue: 用于保存等待执行的任务的阻塞队列。
线程池的生命周期和状态转换
线程池具有不同的生命周期状态,包括RUNNING
、SHUTDOWN
、STOP
和TERMINATED
。
-
创建阶段: 线程池被创建时,初始状态为
RUNNING
。在这个阶段,线程池可以接受新的任务,并会创建核心线程来执行任务。 -
运行阶段: 在运行阶段,线程池按照任务的到来创建和回收线程,同时任务会被放入工作队列中等待执行。
-
关闭阶段: 当线程池不再接受新任务时,进入关闭阶段。在此阶段,线程池会停止接受新任务,但会继续执行已经在队列中的任务。线程池的状态将变为
SHUTDOWN
。 -
终止阶段: 在所有任务执行完成后,线程池进入终止阶段。此时,线程池的状态变为
TERMINATED
。在这个状态下,线程池中的所有线程都已经被销毁。
线程池的状态转换是受到任务提交、关闭和线程池终止等事件的影响的。
-
任务提交: 在任务提交时,线程池可能会创建新的线程或者将任务放入队列中等待执行。
-
关闭: 调用线程池的
shutdown()
方法将线程池切换到关闭状态。在关闭状态下,线程池不再接受新任务,但会继续执行队列中的任务。 -
终止: 当所有任务执行完成,并且调用了
shutdown()
后,线程池进入终止状态。 -
异常情况: 如果发生异常,线程池可能进入
TERMINATED
状态,但并不是所有的线程都已经终止。这种情况下,线程池可能需要通过适当的手段来处理未终止的线程。
拒绝策略
线程池的拒绝策略决定了当线程池无法执行提交的任务时的行为。常见的拒绝策略包括AbortPolicy
、CallerRunsPolicy
、DiscardPolicy
和DiscardOldestPolicy
等。
AbortPolicy
AbortPolicy
是默认的拒绝策略,当线程池无法接受新任务时,会抛出RejectedExecutionException
异常。
public static class AbortPolicy implements RejectedExecutionHandler {
public AbortPolicy() { }
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
throw new RejectedExecutionException("Task " + r.toString() +
" rejected from " +
e.toString());
}
}
CallerRunsPolicy
CallerRunsPolicy
拒绝策略会直接在提交任务的线程中执行被拒绝的任务。这种策略适用于任务的负载比较轻,而且执行时间短暂的情况。
public static class CallerRunsPolicy implements RejectedExecutionHandler {
public CallerRunsPolicy() { }
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
r.run();
}
}
}
DiscardPolicy
DiscardPolicy
拒绝策略会默默地丢弃无法处理的任务,不提供任何反馈。如果任务队列已满,新提交的任务会被直接丢弃。
public static class DiscardPolicy implements RejectedExecutionHandler {
public DiscardPolicy() { }
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
}
}
DiscardOldestPolicy
DiscardOldestPolicy
拒绝策略会丢弃队列中最早被提交但尚未被执行的任务,然后将新任务加入队列。
public static class DiscardOldestPolicy implements RejectedExecutionHandler {
public DiscardOldestPolicy() { }
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
e.getQueue().poll();
e.execute(r);
}
}
}
也可以使用setRejectedExecutionHandler()
方法设置拒绝策略,处理线程池无法接受新任务时的行为。或者实现RejectedExecutionHandler
接口定义自己的策略。
四种内置线程池类型
在Executors
类中,提供了四种内置的线程池类型,分别是CachedThreadPool
、FixedThreadPool
、ScheduledThreadPool
和SingleThreadExecutor
。每种线程池类型都适用于不同的场景,下面我们将详细介绍每一种类型的特点和适用情况。
CachedThreadPool
-
**特点:**动态增加线程数量,如果线程池的当前线程数超过了处理任务所需的线程数,多余的空闲线程会在60秒后被终止。
-
**适用场景:**处理大量短时任务,任务执行时间较短,且任务量不确定。
FixedThreadPool
-
**特点:**固定线程数量的线程池,当线程池中的线程达到核心线程数时,新任务将在队列中等待。
-
**适用场景:**适用于并发线程数有限的情况,可以控制资源的最大并发数。
ScheduledThreadPool
-
**特点:**类似于
FixedThreadPool
,但增加了定时执行任务的功能,可以在指定时间执行任务。 -
**适用场景:**适用于需要定期执行任务的场景,例如定时任务、周期性数据同步等。
SingleThreadExecutor
-
**特点:**只有一个核心线程的线程池,确保所有任务按照指定顺序执行。
-
**适用场景:**适用于需要顺序执行任务的场景,例如任务之间有依赖关系的情况。
阻塞队列的选择与优化
线程池中的阻塞队列对于任务的存储和调度起着重要作用。不同的阻塞队列实现对线程池的性能和行为产生直接影响。以下是一些常见的阻塞队列以及它们的选择与优化建议。
LinkedBlockingQueue
LinkedBlockingQueue
是一个基于链表的阻塞队列,可以选择不设置容量(默认为Integer.MAX_VALUE
)。
-
优势: 对于大多数场景来说,具有较高的吞吐量和性能。
-
注意: 如果任务提交速度高于线程处理速度,队列可能会无限制增长,占用大量内存。
ArrayBlockingQueue
ArrayBlockingQueue
是一个基于数组的有界阻塞队列,必须设置容量。
-
优势: 能够限制队列的最大容量,避免无限制增长。
-
注意: 需要根据应用场景合理设置队列容量,避免太小导致性能瓶颈,太大则可能占用过多内存。
SynchronousQueue
SynchronousQueue
是一个没有容量的阻塞队列,每个插入操作必须等待另一个线程的移除操作。
-
优势: 高效地传递任务,适用于高并发场景。
-
注意: 当线程池的最大线程数小于队列容量时,可能导致任务被直接拒绝。
PriorityBlockingQueue
PriorityBlockingQueue
是一个支持优先级的无界阻塞队列。
-
优势: 具备任务优先级特性,可以实现任务按照优先级顺序执行。
-
注意: 需要确保任务实现了
Comparable
接口或提供自定义比较器。
自定义线程池
除了使用内置的线程池类型外,ThreadPoolExecutor
还允许开发者自定义线程池,以满足特定的业务需求。自定义线程池的步骤如下:
创建ThreadPoolExecutor实例
通过ThreadPoolExecutor
的构造方法,设置核心线程数、最大线程数、阻塞队列等参数来创建线程池实例。
// 创建一个自定义的线程池
ThreadPoolExecutor customThreadPool = new ThreadPoolExecutor(
corePoolSize,
maximumPoolSize,
keepAliveTime,
timeUnit,
workQueue,
threadFactory,
handler
);
配置ThreadFactory
通过setThreadFactory()
方法配置线程工厂,用于创建新的线程。可以使用Executors.defaultThreadFactory()
创建默认线程工厂,也可以实现ThreadFactory
接口自定义线程创建过程。
// 自定义线程工厂
ThreadFactory customThreadFactory = new ThreadFactory() {
private final AtomicInteger threadNumber = new AtomicInteger(1);
@Override
public Thread newThread(Runnable r) {
return new Thread(r, "CustomThread-" + threadNumber.getAndIncrement());
}
};
customThreadPool.setThreadFactory(customThreadFactory);
配置Handler
通过setThreadFactory()
方法配置RejectedExecutionHandler
,用于处理被拒绝的任务。可以选择使用预定义的处理器,如AbortPolicy
、CallerRunsPolicy
等,也可以实现RejectedExecutionHandler
接口定义自己的处理逻辑。
// 自定义拒绝策略处理器
RejectedExecutionHandler customHandler = (r, executor) -> {
// 处理被拒绝的任务,例如记录日志或其他处理
System.out.println("Task Rejected: " + r.toString());
};
customThreadPool.setRejectedExecutionHandler(customHandler);
提交任务
通过调用execute()
或submit()
方法将任务提交给自定义的线程池执行。
customThreadPool.execute(() -> {
// 执行任务逻辑
});
线程池的注意事项
在使用线程池时,以下注意事项可以充分发挥线程池的优势,提高应用性能和稳定性。
合理设置线程池参数
-
核心线程数(corePoolSize): 根据应用的并发量和性能需求来设置,避免设置过高或过低。
-
最大线程数(maximumPoolSize): 根据应用的并发峰值来设置,不要设置过多,以免占用过多系统资源。
-
阻塞队列(workQueue): 选择合适的队列类型,如
LinkedBlockingQueue
或ArrayBlockingQueue
,以及合适的队列容量,避免队列溢出。 -
线程存活时间(keepAliveTime): 配合阻塞队列,避免线程池中的线程数量长时间维持在核心线程数之上。
检测和处理异常
在任务的run
方法中要注意捕获异常,以避免异常抛出导致线程终止。可以在Thread.UncaughtExceptionHandler
中处理未捕获的异常。
Thread.setDefaultUncaughtExceptionHandler((t, e) -> {
// 处理未捕获的异常
});
使用Callable获取任务执行结果
如果任务需要返回结果,可以使用Callable
接口,通过Future
对象获取任务的执行结果。
ExecutorService executorService = Executors.newFixedThreadPool(5);
Future<Integer> future = executorService.submit(() -> {
// 执行任务逻辑
return 42;
});
// 获取任务执行结果
int result = future.get();
避免线程泄漏
线程泄漏是在使用线程池时需要注意的一个问题。下面是一些避免线程泄漏的方法:
-
及时释放线程资源:在任务完成后,务必及时关闭线程,释放线程资源。可以使用
shutdown()
或shutdownNow()
方法来关闭线程池。 -
使用WeakReference:如果线程需要引用外部对象,可以使用WeakReference来持有引用。这样,在任务完成后,即使线程还在执行,外部对象也能被垃圾回收器回收。
-
处理任务取消:如果任务被取消,需要确保线程能够正确地响应取消操作,并释放相关资源。
-
使用适当的线程池大小:如果线程池的大小设置过大,可能会导致线程资源的浪费。因此,需要根据应用程序的需求和系统的性能来合理地设置线程池的大小。
-
使用线程池监控工具:可以使用线程池监控工具来检测线程池的状态和资源利用情况,及时发现潜在的线程泄漏问题。
监控线程池状态
监控线程池的状态可以帮助我们及时发现线程池的异常和性能问题。下面是一些方法来监控线程池的状态:
-
使用ThreadPoolExecutor提供的方法:
getActiveCount()
方法可以获取线程池中正在执行任务的线程数;getTaskCount()
方法可以获取线程池中已经执行和正在执行的任务总数;getCompletedTaskCount()
方法可以获取线程池中已经完成的任务总数。通过这些方法,可以了解线程池的活动情况。 -
使用ThreadPoolExecutor提供的getQueue()方法:这个方法可以获取线程池中的任务队列。通过检查任务队列的长度,可以了解等待执行的任务数。
-
使用定时任务:可以定时记录线程池的状态,比如每隔一段时间输出线程池的活动线程数、任务队列的长度等指标。这样可以及时发现线程池的异常情况。
-
使用性能分析工具:可以使用性能分析工具,如
Android Profiler
,来监测线程池的CPU使用率、内存消耗和线程执行时间等指标。这些工具可以提供更详细的线程池性能信息。 -
自定义监控工具:根据应用程序的需求,可以自定义监控工具来监测线程池的状态。这些工具可以根据具体情况收集和展示线程池的相关指标。
总结
通过本文的学习,我们深入了解了Android中线程池Executors的重要性和灵活性。线程池作为一种多线程管理机制,在Android应用中发挥着关键作用,能够有效提高应用的性能、响应速度,同时避免过度占用系统资源。
网友评论