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

ExecutorService executorService = Executors.newFixedThreadPool(5);

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

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

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

ThreadPoolExecutor

ThreadPoolExecutorjava.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: 用于保存等待执行的任务的阻塞队列。

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

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

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

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

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

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

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

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

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

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

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

拒绝策略

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

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

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,用于处理被拒绝的任务。可以选择使用预定义的处理器,如AbortPolicyCallerRunsPolicy等,也可以实现RejectedExecutionHandler接口定义自己的处理逻辑。

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

customThreadPool.setRejectedExecutionHandler(customHandler);

提交任务

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

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

线程池的注意事项

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

合理设置线程池参数

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

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

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

  • 线程存活时间(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应用中发挥着关键作用,能够有效提高应用的性能、响应速度,同时避免过度占用系统资源。

最后

如果想要成为架构师或想突破20~30K薪资范畴,那就不要局限在编码,业务,要会选型、扩展,提升编程思维。此外,良好的职业规划也很重要,学习的习惯很重要,但是最重要的还是要能持之以恒,任何不能坚持落实的计划都是空谈。

如果你没有方向,这里给大家分享一套由阿里高级架构师编写的《Android八大模块进阶笔记》,帮大家将杂乱、零散、碎片化的知识进行体系化的整理,让大家系统而高效地掌握Android开发的各个知识点。
img
相对于我们平时看的碎片化内容,这份笔记的知识点更系统化,更容易理解和记忆,是严格按照知识体系编排的。

欢迎大家一键三连支持,若需要文中资料,直接扫描文末CSDN官方认证微信卡片免费领取↓↓↓(文末还有ChatGPT机器人小福利哦,大家千万不要错过)

PS:群里还设有ChatGPT机器人,可以解答大家在工作上或者是技术上的问题

图片

  • 27
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值