Android学习总结之线程池篇
一、线程池参数调优实战真题
真题 1:直播 APP 弹幕加载线程池设计
题目描述:直播 APP 需要实时加载弹幕数据(网络请求,IO 密集型),同时渲染弹幕视图(UI 操作需切主线程),现有线程池导致弹幕卡顿,如何优化?
解题思路:
- 参数调整:
corePoolSize
:对于 IO 密集型任务,设为2 * CPU核心数
,例如 8 核设备则corePoolSize = 16
。maximumPoolSize
:适当增大到3 * CPU核心数
(即 24),以应对高并发场景。workQueue
:使用有界队列ArrayBlockingQueue(100)
,防止内存溢出。keepAliveTime
:设为60秒
,允许非核心线程存活更久。
- 任务拆分:
- 网络请求任务提交至线程池。
- 通过
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 密集型任务,我会从三个层面优化。首先是 线程池参数调整:
corePoolSize
设置为2 * CPU核心数
(如 8 核设备设为 16),因为 IO 任务等待时线程不占用 CPU,更多核心线程能提升并发效率;maximumPoolSize
扩展到3 * CPU核心数
(即 24),应对弹幕突发高流量;- 使用
ArrayBlockingQueue(100)
限制任务队列长度,防止内存溢出; keepAliveTime
设为 60 秒,允许非核心线程存活,减少频繁创建销毁开销。
其次, 任务拆分:网络请求提交到线程池异步执行,通过Handler
切换到主线程渲染弹幕,避免阻塞 UI。
最后, 拒绝策略 选用CallerRunsPolicy
,将被拒绝的任务回退到调用线程(通常是主线程)。这样在高并发时,若主线程空闲可临时处理任务,同时提示开发者线程池已满载,需进一步优化参数或任务分配。”
面试追问:
- 问:为什么使用
CallerRunsPolicy
拒绝策略? - 答:该策略将被拒绝的任务交给调用者线程(通常是主线程)执行,避免任务丢弃。在弹幕高并发场景下,主线程空闲时可临时处理部分任务,同时提醒开发者线程池已满载,需优化参数或任务分配。
真题 2:图片编辑 APP 线程池优化
题目描述:图片编辑 APP 在批量处理图片时(解码、裁剪、压缩,均为 CPU 密集型),出现手机发热严重且处理速度慢的问题,如何优化?
优化方案:
- 参数调整:
corePoolSize
:设为CPU核心数 + 1
(如 9)。maximumPoolSize
:与核心线程数保持一致(9),避免过多线程上下文切换。keepAliveTime
:设为0
,快速回收空闲线程。
- 性能监控:
- 使用
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 密集型的图片处理任务,优化需兼顾效率和资源消耗。
首先调整 线程池参数:
corePoolSize
设为CPU核心数 + 1
(如 8 核设备设为 9),充分利用 CPU 资源且减少上下文切换;maximumPoolSize
与核心线程数保持一致,避免创建多余线程加剧 CPU 负载;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 中使用线程池执行网络请求,旋转屏幕后内存占用持续上升,如何解决?
解决方案:
- 正确关闭线程池:在
Activity
的onDestroy
方法中调用executor.shutdown()
优雅关闭线程池,如需立即关闭可调用executor.shutdownNow()
。 - 使用弱引用:避免
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,更多线程可提高并发效率)。
- 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 强引用,防止内存泄漏(可使用弱引用或静态内部类)。
- 第 100 个任务需在后台线程执行
- 代码示例:
// 初始化 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 面试加分项(必答点)
-
线程池生命周期管理:
- 在 Activity/Fragment 的
onDestroy()
中调用executor.shutdown()
,避免后台线程持有上下文导致内存泄漏。 - 若需立即中断任务,调用
shutdownNow()
,并处理InterruptedException
。
- 在 Activity/Fragment 的
-
避免 ANR:
- 所有耗时任务(包括
latch.await()
/Future.get()
)必须在后台线程执行,UI 操作通过Handler
切换回主线程。
- 所有耗时任务(包括
-
替代方案(高级考点):
- 若任务需与 Android 生命周期绑定,可使用
WorkManager
(适合后台任务调度,支持延迟、重试、约束条件)。 - 对于轻量任务,可使用
ExecutorService
+HandlerThread
(自定义线程消息循环)。
- 若任务需与 Android 生命周期绑定,可使用