当前位置: 首页 > news >正文

Android学习总结之线程池篇

一、线程池参数调优实战真题

真题 1:直播 APP 弹幕加载线程池设计

题目描述:直播 APP 需要实时加载弹幕数据(网络请求,IO 密集型),同时渲染弹幕视图(UI 操作需切主线程),现有线程池导致弹幕卡顿,如何优化?

解题思路

  1. 参数调整
    • corePoolSize:对于 IO 密集型任务,设为 2 * CPU核心数,例如 8 核设备则 corePoolSize = 16
    • maximumPoolSize:适当增大到 3 * CPU核心数(即 24),以应对高并发场景。
    • workQueue:使用有界队列 ArrayBlockingQueue(100),防止内存溢出。
    • keepAliveTime:设为 60秒,允许非核心线程存活更久。
  2. 任务拆分
    • 网络请求任务提交至线程池。
    • 通过 Handler 将渲染任务切换到主线程。
int cpuCores = Runtime.getRuntime().availableProcessors();
// 创建线程池
ExecutorService executor = new ThreadPoolExecutor(2 * cpuCores, // 核心线程数,针对IO密集型任务设置为2 * CPU核心数3 * cpuCores, // 最大线程数,增大以应对高并发60L, // 非核心线程空闲时的存活时间TimeUnit.SECONDS, // 存活时间单位为秒new ArrayBlockingQueue<>(100), // 有界队列,容量为100new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略,任务被调用线程执行
);executor.submit(() -> {// 网络请求获取弹幕数据List<DanmuData> data = networkRequest(); // 通过Handler切换到主线程渲染弹幕new Handler(Looper.getMainLooper()).post(() -> { renderDanmu(data);});
});

回答话术
“针对弹幕加载的 IO 密集型任务,我会从三个层面优化。首先是 线程池参数调整

  1. corePoolSize 设置为 2 * CPU核心数(如 8 核设备设为 16),因为 IO 任务等待时线程不占用 CPU,更多核心线程能提升并发效率;
  2. maximumPoolSize 扩展到 3 * CPU核心数(即 24),应对弹幕突发高流量;
  3. 使用 ArrayBlockingQueue(100) 限制任务队列长度,防止内存溢出;
  4. keepAliveTime 设为 60 秒,允许非核心线程存活,减少频繁创建销毁开销。
    其次, 任务拆分:网络请求提交到线程池异步执行,通过 Handler 切换到主线程渲染弹幕,避免阻塞 UI。
    最后, 拒绝策略 选用 CallerRunsPolicy,将被拒绝的任务回退到调用线程(通常是主线程)。这样在高并发时,若主线程空闲可临时处理任务,同时提示开发者线程池已满载,需进一步优化参数或任务分配。”

面试追问

  • :为什么使用 CallerRunsPolicy 拒绝策略?
  • :该策略将被拒绝的任务交给调用者线程(通常是主线程)执行,避免任务丢弃。在弹幕高并发场景下,主线程空闲时可临时处理部分任务,同时提醒开发者线程池已满载,需优化参数或任务分配。
真题 2:图片编辑 APP 线程池优化

题目描述:图片编辑 APP 在批量处理图片时(解码、裁剪、压缩,均为 CPU 密集型),出现手机发热严重且处理速度慢的问题,如何优化?

优化方案

  1. 参数调整
    • corePoolSize:设为 CPU核心数 + 1(如 9)。
    • maximumPoolSize:与核心线程数保持一致(9),避免过多线程上下文切换。
    • keepAliveTime:设为 0,快速回收空闲线程。
  2. 性能监控
    • 使用 StrictMode 监控线程池使用情况。
    • 分析 CPU 使用率,动态调整任务数量。
int cpuCores = Runtime.getRuntime().availableProcessors();
// 创建线程池
ExecutorService executor = new ThreadPoolExecutor(cpuCores + 1, // 核心线程数,针对CPU密集型任务设置为CPU核心数 + 1cpuCores + 1, // 最大线程数,与核心线程数相同0L, // 非核心线程空闲时的存活时间设为0TimeUnit.MILLISECONDS, // 存活时间单位为毫秒new ArrayBlockingQueue<>(50), // 有界队列,容量为50new ThreadPoolExecutor.DiscardOldestPolicy() // 拒绝策略,丢弃队列中最老的任务
);

“对于 CPU 密集型的图片处理任务,优化需兼顾效率和资源消耗。
首先调整 线程池参数

  1. corePoolSize 设为 CPU核心数 + 1(如 8 核设备设为 9),充分利用 CPU 资源且减少上下文切换;
  2. maximumPoolSize 与核心线程数保持一致,避免创建多余线程加剧 CPU 负载;
  3. keepAliveTime 设为 0,让非核心线程立即回收,释放资源。
    其次, 性能监控 必不可少:通过 StrictMode 监控线程池使用,例如检测耗时任务或线程泄漏;结合 CPU 使用率动态调整任务数量,比如当 CPU 负载过高时,暂停部分任务或降低并发量。
    最后, 拒绝策略 采用 DiscardOldestPolicy,丢弃队列中等待最久的任务,保证新提交的紧急任务优先执行,避免任务堆积。”

二、任务依赖与同步实战真题

真题 3:多任务顺序执行

题目描述:在文件上传功能中,需要依次完成文件压缩、加密、上传三个步骤,如何确保任务按顺序执行?

解决方案:使用 CountDownLatch 来实现任务间的同步。

// 创建CountDownLatch,初始计数值为1
CountDownLatch latch1 = new CountDownLatch(1); 
// 创建另一个CountDownLatch,初始计数值为1
CountDownLatch latch2 = new CountDownLatch(1); // 提交压缩任务到线程池
executor.submit(() -> { compressFile(); // 执行文件压缩操作latch1.countDown(); // 压缩任务完成,计数值减1
});// 提交加密任务到线程池
executor.submit(() -> { try {latch1.await(); // 等待压缩任务完成(计数值变为0)encryptFile(); // 执行文件加密操作latch2.countDown(); // 加密任务完成,计数值减1} catch (InterruptedException e) {e.printStackTrace();}
});// 提交上传任务到线程池
executor.submit(() -> { try {latch2.await(); // 等待加密任务完成(计数值变为0)uploadFile(); // 执行文件上传操作} catch (InterruptedException e) {e.printStackTrace();}
});

回答话术
“我会使用 CountDownLatch 实现任务同步。首先创建两个 CountDownLatch,初始计数值都为 1,分别控制压缩→加密、加密→上传的依赖关系。
提交任务时,压缩任务完成后调用 latch1.countDown() 释放信号;加密任务通过 latch1.await() 等待压缩完成,完成后再释放 latch2;最后上传任务等待 latch2 信号。这样通过计数器的增减,强制任务按顺序执行。
若任务失败,可在每个任务中捕获异常并记录,后续通过 CyclicBarrier 或自定义状态机实现重试逻辑。例如,若加密失败,回滚压缩结果并重新执行压缩和加密,确保流程可靠性。”

面试追问

  • :如果中间某个任务失败怎么办?
  • :可以在每个任务中捕获异常,记录失败信息。使用 CyclicBarrier 或自定义状态机来管理任务重试逻辑,确保整体流程的可靠性。
真题 4:任务优先级调度

题目描述:APP 中有高优先级的用户登录任务和低优先级的日志上报任务,如何确保高优先级任务优先执行?

解决方案:使用 PriorityBlockingQueue 来实现任务优先级调度。

// 定义一个实现Runnable和Comparable接口的任务类
class PrioritizedTask implements Runnable, Comparable<PrioritizedTask> { private final int priority; // 任务优先级private final Runnable task; // 具体任务public PrioritizedTask(int priority, Runnable task) {this.priority = priority;this.task = task;}@Overridepublic void run() {task.run(); // 执行具体任务}@Overridepublic int compareTo(PrioritizedTask other) {return Integer.compare(other.priority, this.priority); // 比较任务优先级,倒序排列,高优先级在前}
}// 创建线程池
ExecutorService executor = new ThreadPoolExecutor(5, // 核心线程数10, // 最大线程数30L, // 非核心线程空闲时的存活时间TimeUnit.SECONDS, // 存活时间单位为秒new PriorityBlockingQueue<>() // 优先级队列
);// 提交高优先级的用户登录任务
executor.submit(new PrioritizedTask(1, () -> login())); 
// 提交低优先级的日志上报任务
executor.submit(new PrioritizedTask(2, () -> uploadLog())); 

回答话术
“使用 PriorityBlockingQueue 作为线程池的任务队列即可实现。自定义 PrioritizedTask 类,实现 Comparable 接口并重写 compareTo 方法,按优先级倒序排列(高优先级在前)。线程池从队列中获取任务时,始终优先执行优先级高的任务。
相比其他方案,PriorityBlockingQueue 更灵活:它基于堆结构实现,插入和获取任务的时间复杂度为 O (log n),效率较高;同时支持动态调整任务优先级。例如,若有新的高优先级登录任务加入,能立即插队执行,而低优先级的日志上报任务会暂停等待,确保核心业务的响应速度。” 

三、线程池与 Android 生命周期管理

真题 5:Activity 中的线程池内存泄漏

题目描述:在 Activity 中使用线程池执行网络请求,旋转屏幕后内存占用持续上升,如何解决?

解决方案

  1. 正确关闭线程池:在 Activity 的 onDestroy 方法中调用 executor.shutdown() 优雅关闭线程池,如需立即关闭可调用 executor.shutdownNow()
  2. 使用弱引用:避免 Activity 被任务强引用导致无法回收。
public class MainActivity extends AppCompatActivity {private ExecutorService executor;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);// 创建固定大小的线程池executor = Executors.newFixedThreadPool(5); }@Overrideprotected void onDestroy() {super.onDestroy();executor.shutdown(); // 优雅关闭线程池// 如需立即关闭线程池,可使用executor.shutdownNow();}
}class MyTask implements Runnable {private WeakReference<Activity> activityRef; // 使用弱引用持有Activitypublic MyTask(Activity activity) {activityRef = new WeakReference<>(activity);}@Overridepublic void run() {Activity activity = activityRef.get();if (activity != null) {// 任务逻辑}}
}

回答话术
“内存泄漏主要因线程池任务持有 Activity 强引用,或线程池未正确关闭。
首先,在 Activity 的 onDestroy 方法中调用 executor.shutdown() 优雅关闭线程池,它会等待已提交任务执行完毕后释放资源;若需立即中断任务,可使用 executor.shutdownNow()
其次,避免任务强引用 Activity:将任务类中的 Activity 引用改为 WeakReference,确保 Activity 被销毁时,任务不再阻止其回收。
最后,建议将线程池与 ViewModel 或 Service 绑定,而非直接关联 Activity。例如,使用 ViewModel 管理长期运行的任务,其生命周期与 Activity 分离,能有效防止因配置变更(如旋转屏幕)导致的内存泄漏问题。” 

四、WorkManager 替代方案场景

真题 6:后台任务调度

题目描述:APP 需要在设备充电且 Wi-Fi 连接时,自动备份数据到云端,如何实现?

解决方案:使用 WorkManager 结合 Constraints 来实现后台任务调度。

// 构建任务约束条件
Constraints constraints = new Constraints.Builder() .setRequiresCharging(true) // 设置任务需要设备充电.setRequiredNetworkType(NetworkType.UNMETERED) // 设置任务需要连接非计量网络(如Wi-Fi).build();// 创建一次性工作请求,指定工作类和约束条件
OneTimeWorkRequest backupWorkRequest = new OneTimeWorkRequest.Builder(BackupWorker.class) .setConstraints(constraints).build();// 将工作请求加入WorkManager队列
WorkManager.getInstance(context).enqueue(backupWorkRequest); 

回答话术
“使用 WorkManager 结合 Constraints 能轻松实现。通过 Constraints.Builder 设置条件:setRequiresCharging(true) 确保设备充电,setRequiredNetworkType(NetworkType.UNMETERED) 限制为 Wi-Fi 网络。然后创建 OneTimeWorkRequest,将备份任务类和约束条件传入,最后加入 WorkManager 队列。
WorkManager 相比 ThreadPoolExecutor 有显著优势:它支持任务重试、延迟执行、系统级调度,且与 Android 生命周期深度集成(如设备重启后任务自动恢复)。而 ThreadPoolExecutor 更专注于任务并发执行,需开发者手动处理调度逻辑和资源管理。因此,对于这种有条件触发、需长期可靠运行的后台任务,WorkManager 是更优选择。”

面试追问

  • :WorkManager 与 ThreadPoolExecutor 的区别?
  • :WorkManager 是 Android 提供的高级任务调度框架,支持任务重试、延迟执行、约束条件等,与系统生命周期紧密集成;而 ThreadPoolExecutor 更专注于任务的并发执行,需要开发者手动处理任务调度和资源管理。

额外具体场景设计:

一、如何设计线程池让 100 个任务最快完成?

1. 核心考点:线程池参数调优与 Android 场景适配

Android 面试中,线程池设计需结合设备 CPU 核心数、任务类型(CPU 密集型 / IO 密集型)以及内存管理要求,避免 OOM 和资源浪费。

关键参数设计:
  • 核心线程数(corePoolSize):
    • CPU 密集型任务:设为 CPU 核心数 + 1(充分利用 CPU,避免线程上下文切换损耗),可通过 Runtime.getRuntime().availableProcessors() 获取核心数。
    • IO 密集型任务:设为 2 * CPU 核心数(因 IO 等待时线程不占用 CPU,更多线程可提高并发效率)。
  • 最大线程数(maximumPoolSize):
    • 对于 CPU 密集型,通常与核心线程数一致(避免无意义的线程创建);
    • 对于 IO 密集型,可适当增大,但需结合任务队列容量避免内存溢出(Android 中建议使用有界队列 ArrayBlockingQueue,而非无界队列 LinkedBlockingQueue,防止内存暴涨)。
  • 任务队列(workQueue):
    • 有界队列(如 ArrayBlockingQueue:更安全,可控制并发量,避免 OOM(Android 内存受限,无界队列可能导致后台任务堆积)。
    • 优先级队列(PriorityBlockingQueue:若任务有优先级差异,可优先执行高优先级任务。
  • 线程存活时间(keepAliveTime):
    • 非核心线程空闲时的存活时间,IO 密集型任务可设稍长(如 30s),CPU 密集型可设 0(快速回收空闲线程)。
Android 禁用默认线程池(高频考点):

避免使用 Executors.newFixedThreadPool()(无界队列导致 OOM)或 newCachedThreadPool()(最大线程数无限),必须手动创建 ThreadPoolExecutor,示例:

int cpuCores = Runtime.getRuntime().availableProcessors();
ExecutorService executor = new ThreadPoolExecutor(cpuCores + 1,          // corePoolSize(CPU 密集型)2 * cpuCores,         // maximumPoolSize(IO 密集型可增大)30L, TimeUnit.SECONDS,// keepAliveTimenew ArrayBlockingQueue<>(100), // 有界队列,容量根据任务量调整new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略:任务被调用线程执行,避免丢弃
);
性能优化点:
  • 任务轻量化: 避免在任务中执行耗时 UI 操作(需通过 Handler/runOnUiThread 切换主线程)。
  • 复用线程: 线程池复用线程避免频繁创建销毁开销,比 new Thread() 更高效。

二、如何确保前 99 个任务完成后再执行第 100 个任务?

1. 核心考点:同步机制选择(CountDownLatch 或 Future)

Android 面试中,需区分 任务依赖关系 和 线程间同步,常用方案:

方案一:CountDownLatch(最简洁方案)
  • 原理:通过计数器(初始值 99)阻塞第 100 个任务,前 99 个任务完成时调用 countDown(),计数器归零后第 100 个任务继续执行。
  • Android 注意点
    • 第 100 个任务需在后台线程执行 latch.await(),避免阻塞主线程(否则触发 ANR)。
    • 任务中避免持有 Activity 强引用,防止内存泄漏(可使用弱引用或静态内部类)。
  • 代码示例:
// 初始化 CountDownLatch(计数器 99)
private final CountDownLatch latch = new CountDownLatch(99);// 提交前 99 个任务
for (int i = 0; i < 99; i++) {executor.submit(() -> {// 执行任务...latch.countDown(); // 任务完成,计数器减一});
}// 提交第 100 个任务(依赖前 99 个)
executor.submit(() -> {try {latch.await(); // 阻塞直到计数器归零// 执行第 100 个任务...} catch (InterruptedException e) {Thread.currentThread().interrupt(); // 恢复中断状态}
});
方案二:Future 批量获取结果(适合任务有返回值场景)
  • 原理:将前 99 个任务的 Future 存入列表,通过 Future.allOf() 等待所有任务完成,再执行第 100 个任务。
  • 优势:可处理任务返回值,支持异常处理(如 Future.get() 抛 ExecutionException)。
  • 代码示例:
List<Future<?>> futures = new ArrayList<>(99);
for (int i = 0; i < 99; i++) {futures.add(executor.submit(() -> { /* 任务逻辑 */ }));
}// 提交第 100 个任务,等待前 99 个完成
executor.submit(() -> {try {Future.allOf(futures.toArray(new Future[0])).get(); // 阻塞直到所有完成// 执行第 100 个任务...} catch (InterruptedException | ExecutionException e) {// 处理异常}
});
方案对比(面试官可能追问):
方案优点缺点适用场景
CountDownLatch轻量、无返回值需求仅支持单向等待纯任务依赖场景
Future.allOf支持返回值、异常处理代码稍复杂任务有结果校验场景

三、Android 面试加分项(必答点)

  1. 线程池生命周期管理:

    • 在 Activity/Fragment 的 onDestroy() 中调用 executor.shutdown(),避免后台线程持有上下文导致内存泄漏。
    • 若需立即中断任务,调用 shutdownNow(),并处理 InterruptedException
  2. 避免 ANR:

    • 所有耗时任务(包括 latch.await()/Future.get())必须在后台线程执行,UI 操作通过 Handler 切换回主线程。
  3. 替代方案(高级考点):

    • 若任务需与 Android 生命周期绑定,可使用 WorkManager(适合后台任务调度,支持延迟、重试、约束条件)。
    • 对于轻量任务,可使用 ExecutorService + HandlerThread(自定义线程消息循环)。

相关文章:

  • 使用SSH协议克隆详细步骤
  • stm32之BKP备份寄存器和RTC时钟
  • TCPIP详解 卷1协议 八 ICMPv4和ICMPv6 Internet控制报文协议
  • 深入掌握CSS定位:构建精密布局的核心技术
  • 第二章、物理层
  • 开发环境(Development Environment)
  • 【SSM-Mybatis(一)】java持久层框架-Mybatis!本文涵盖介绍Mybatis和基本使用,分析Mybatis核心配置文件
  • 豆瓣电影Top250数据工程实践:从爬虫到智能存储的技术演进(含完整代码)
  • 【Ansible】之inventory主机清单
  • 麒麟 v10 cgroup v1 切换 cgroup v2
  • 上海海关特展:二维码讲解“外来入侵物种”的危害!
  • 小智AI客户端使用测试(Python)
  • 让 - 艾里克・德布尔与斯普林格出版公司:科技变革下的出版业探索
  • 韩国直邮新纪元:Coupang多语言支持覆盖38国市场
  • 服务网格的“解剖学” - 控制平面与数据平面
  • VIC-2D 7.0 为平面样件机械试验提供全视野位移及应变数据软件
  • 1.3 极限
  • 生成对抗网络(GAN)深度解析:理论、技术与应用全景
  • 通用RAG:通过路由模块对多源异构知识库检索生成问答思路
  • 我用Deepseek + 亮数据爬虫神器 1小时做出輿情分析器
  • 长沙查处疑似非法代孕:有人企图跳窗,有女子被麻醉躺手术台
  • 水豚“豆包”出逃已40天,扬州茱萸湾景区追加悬赏
  • 《广州大典研究》集刊发展座谈会:“广州学”的传承与创新
  • 库尔德工人党决定自行解散
  • 撤制镇如何突破困境?欢迎订阅《澎湃城市报告》第23期
  • 演员发文抵制代拍获粉丝支持,媒体:追星“正确姿势”不妨多来点