盒子
盒子
文章目录
  1. Android线程池简介
    1. 线程池的作用和优势
  2. Executors框架概述
    1. Executors工厂方法
    2. 任务提交和执行
  3. ThreadPoolExecutor
    1. 核心参数解释
    2. 线程池的生命周期和状态转换
    3. 拒绝策略
      1. AbortPolicy
      2. CallerRunsPolicy
      3. DiscardPolicy
      4. DiscardOldestPolicy
  4. 四种内置线程池类型
    1. CachedThreadPool
    2. FixedThreadPool
    3. ScheduledThreadPool
    4. SingleThreadExecutor
  5. 阻塞队列的选择与优化
    1. LinkedBlockingQueue
    2. ArrayBlockingQueue
    3. SynchronousQueue
    4. PriorityBlockingQueue
  6. 自定义线程池
    1. 创建ThreadPoolExecutor实例
    2. 配置ThreadFactory
    3. 配置Handler
    4. 提交任务
  7. 线程池的注意事项
    1. 合理设置线程池参数
    2. 检测和处理异常
    3. 使用Callable获取任务执行结果
  8. 避免线程泄漏
  9. 监控线程池状态
  10. 总结
  11. 推荐

精通Android线程池的必备高级技巧

在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接口的带返回值的线程。

1
2
3
4
5
6
7
8
ExecutorService executorService = Executors.newFixedThreadPool(5);

executorService.submit(() -> {
// 执行任务逻辑
});

// 关闭线程池
executorService.shutdown();

线程池有一个生命周期,它包括创建、运行和关闭三个阶段。创建后线程池可以接受并执行任务,一旦不再需要,可以通过调用shutdown()shutdownNow()方法来关闭线程池。

ThreadPoolExecutor

ThreadPoolExecutorjava.util.concurrent包中的核心类之一,用于实现自定义的线程池。理解ThreadPoolExecutor的工作原理对于深入掌握线程池的使用至关重要。

核心参数解释

1
2
3
4
5
6
7
8
9
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: 用于保存等待执行的任务的阻塞队列。

线程池的生命周期和状态转换

线程池具有不同的生命周期状态,包括RUNNINGSHUTDOWNSTOPTERMINATED

  • 创建阶段: 线程池被创建时,初始状态为RUNNING。在这个阶段,线程池可以接受新的任务,并会创建核心线程来执行任务。

  • 运行阶段: 在运行阶段,线程池按照任务的到来创建和回收线程,同时任务会被放入工作队列中等待执行。

  • 关闭阶段: 当线程池不再接受新任务时,进入关闭阶段。在此阶段,线程池会停止接受新任务,但会继续执行已经在队列中的任务。线程池的状态将变为SHUTDOWN

  • 终止阶段: 在所有任务执行完成后,线程池进入终止阶段。此时,线程池的状态变为TERMINATED。在这个状态下,线程池中的所有线程都已经被销毁。

线程池的状态转换是受到任务提交、关闭和线程池终止等事件的影响的。

  • 任务提交: 在任务提交时,线程池可能会创建新的线程或者将任务放入队列中等待执行。

  • 关闭: 调用线程池的shutdown()方法将线程池切换到关闭状态。在关闭状态下,线程池不再接受新任务,但会继续执行队列中的任务。

  • 终止: 当所有任务执行完成,并且调用了shutdown()后,线程池进入终止状态。

  • 异常情况: 如果发生异常,线程池可能进入TERMINATED状态,但并不是所有的线程都已经终止。这种情况下,线程池可能需要通过适当的手段来处理未终止的线程。

拒绝策略

线程池的拒绝策略决定了当线程池无法执行提交的任务时的行为。常见的拒绝策略包括AbortPolicyCallerRunsPolicyDiscardPolicyDiscardOldestPolicy等。

AbortPolicy

AbortPolicy是默认的拒绝策略,当线程池无法接受新任务时,会抛出RejectedExecutionException异常。

1
2
3
4
5
6
7
8
9
10
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拒绝策略会直接在提交任务的线程中执行被拒绝的任务。这种策略适用于任务的负载比较轻,而且执行时间短暂的情况。

1
2
3
4
5
6
7
8
9
10
public static class CallerRunsPolicy implements RejectedExecutionHandler {

public CallerRunsPolicy() { }

public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
r.run();
}
}
}

DiscardPolicy

DiscardPolicy拒绝策略会默默地丢弃无法处理的任务,不提供任何反馈。如果任务队列已满,新提交的任务会被直接丢弃。

1
2
3
4
5
6
7
public static class DiscardPolicy implements RejectedExecutionHandler {

public DiscardPolicy() { }

public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
}
}

DiscardOldestPolicy

DiscardOldestPolicy拒绝策略会丢弃队列中最早被提交但尚未被执行的任务,然后将新任务加入队列。

1
2
3
4
5
6
7
8
9
10
11
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类中,提供了四种内置的线程池类型,分别是CachedThreadPoolFixedThreadPoolScheduledThreadPoolSingleThreadExecutor。每种线程池类型都适用于不同的场景,下面我们将详细介绍每一种类型的特点和适用情况。

CachedThreadPool

  • 特点:动态增加线程数量,如果线程池的当前线程数超过了处理任务所需的线程数,多余的空闲线程会在60秒后被终止。

  • 适用场景:处理大量短时任务,任务执行时间较短,且任务量不确定。

FixedThreadPool

  • 特点:固定线程数量的线程池,当线程池中的线程达到核心线程数时,新任务将在队列中等待。

  • 适用场景:适用于并发线程数有限的情况,可以控制资源的最大并发数。

ScheduledThreadPool

  • 特点:类似于FixedThreadPool,但增加了定时执行任务的功能,可以在指定时间执行任务。

  • 适用场景:适用于需要定期执行任务的场景,例如定时任务、周期性数据同步等。

SingleThreadExecutor

  • 特点:只有一个核心线程的线程池,确保所有任务按照指定顺序执行。

  • 适用场景:适用于需要顺序执行任务的场景,例如任务之间有依赖关系的情况。

阻塞队列的选择与优化

线程池中的阻塞队列对于任务的存储和调度起着重要作用。不同的阻塞队列实现对线程池的性能和行为产生直接影响。以下是一些常见的阻塞队列以及它们的选择与优化建议。

LinkedBlockingQueue

LinkedBlockingQueue是一个基于链表的阻塞队列,可以选择不设置容量(默认为Integer.MAX_VALUE)。

  • 优势: 对于大多数场景来说,具有较高的吞吐量和性能。

  • 注意: 如果任务提交速度高于线程处理速度,队列可能会无限制增长,占用大量内存。

ArrayBlockingQueue

ArrayBlockingQueue是一个基于数组的有界阻塞队列,必须设置容量。

  • 优势: 能够限制队列的最大容量,避免无限制增长。

  • 注意: 需要根据应用场景合理设置队列容量,避免太小导致性能瓶颈,太大则可能占用过多内存。

SynchronousQueue

SynchronousQueue是一个没有容量的阻塞队列,每个插入操作必须等待另一个线程的移除操作。

  • 优势: 高效地传递任务,适用于高并发场景。

  • 注意: 当线程池的最大线程数小于队列容量时,可能导致任务被直接拒绝。

PriorityBlockingQueue

PriorityBlockingQueue是一个支持优先级的无界阻塞队列。

  • 优势: 具备任务优先级特性,可以实现任务按照优先级顺序执行。

  • 注意: 需要确保任务实现了Comparable接口或提供自定义比较器。

自定义线程池

除了使用内置的线程池类型外,ThreadPoolExecutor还允许开发者自定义线程池,以满足特定的业务需求。自定义线程池的步骤如下:

创建ThreadPoolExecutor实例

通过ThreadPoolExecutor的构造方法,设置核心线程数、最大线程数、阻塞队列等参数来创建线程池实例。

1
2
3
4
5
6
7
8
9
10
// 创建一个自定义的线程池
ThreadPoolExecutor customThreadPool = new ThreadPoolExecutor(
corePoolSize,
maximumPoolSize,
keepAliveTime,
timeUnit,
workQueue,
threadFactory,
handler
);

配置ThreadFactory

通过setThreadFactory()方法配置线程工厂,用于创建新的线程。可以使用Executors.defaultThreadFactory()创建默认线程工厂,也可以实现ThreadFactory接口自定义线程创建过程。

1
2
3
4
5
6
7
8
9
10
11
// 自定义线程工厂
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,用于处理被拒绝的任务。可以选择使用预定义的处理器,如AbortPolicyCallerRunsPolicy等,也可以实现RejectedExecutionHandler接口定义自己的处理逻辑。

1
2
3
4
5
6
7
// 自定义拒绝策略处理器
RejectedExecutionHandler customHandler = (r, executor) -> {
// 处理被拒绝的任务,例如记录日志或其他处理
System.out.println("Task Rejected: " + r.toString());
};

customThreadPool.setRejectedExecutionHandler(customHandler);

提交任务

通过调用execute()submit()方法将任务提交给自定义的线程池执行。

1
2
3
customThreadPool.execute(() -> {
// 执行任务逻辑
});

线程池的注意事项

在使用线程池时,以下注意事项可以充分发挥线程池的优势,提高应用性能和稳定性。

合理设置线程池参数

  • 核心线程数(corePoolSize): 根据应用的并发量和性能需求来设置,避免设置过高或过低。

  • 最大线程数(maximumPoolSize): 根据应用的并发峰值来设置,不要设置过多,以免占用过多系统资源。

  • 阻塞队列(workQueue): 选择合适的队列类型,如LinkedBlockingQueueArrayBlockingQueue,以及合适的队列容量,避免队列溢出。

  • 线程存活时间(keepAliveTime): 配合阻塞队列,避免线程池中的线程数量长时间维持在核心线程数之上。

检测和处理异常

在任务的run方法中要注意捕获异常,以避免异常抛出导致线程终止。可以在Thread.UncaughtExceptionHandler中处理未捕获的异常。

1
2
3
Thread.setDefaultUncaughtExceptionHandler((t, e) -> {
// 处理未捕获的异常
});

使用Callable获取任务执行结果

如果任务需要返回结果,可以使用Callable接口,通过Future对象获取任务的执行结果。

1
2
3
4
5
6
7
8
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应用中发挥着关键作用,能够有效提高应用的性能、响应速度,同时避免过度占用系统资源。

推荐

android_startup: 提供一种在应用启动时能够更加简单、高效的方式来初始化组件,优化启动速度。不仅支持Jetpack App Startup的全部功能,还提供额外的同步与异步等待、线程控制与多进程支持等功能。

AwesomeGithub: 基于Github的客户端,纯练习项目,支持组件化开发,支持账户密码与认证登陆。使用Kotlin语言进行开发,项目架构是基于JetPack\&DataBinding的MVVM;项目中使用了Arouter、Retrofit、Coroutine、Glide、Dagger与Hilt等流行开源技术。

flutter_github: 基于Flutter的跨平台版本Github客户端,与AwesomeGithub相对应。

android-api-analysis: 结合详细的Demo来全面解析Android相关的知识点, 帮助读者能够更快的掌握与理解所阐述的要点。

daily_algorithm: 每日一算法,由浅入深,欢迎加入一起共勉。

支持一下
赞赏是一门艺术