3. 2026金三银四 Android 背完这 23 道题,Android 线程面试横着走

Android 线程与线程池面试题深度解析(资深版)

本文基于 Android 14 / Java 17 源码,面向资深 Android 开发工程师,涵盖线程基础、线程池原理、Binder 线程池等核心知识点,深入源码层面解析设计思想与实现细节。


目录

  1. 线程基础篇
  2. 线程池核心原理篇
  3. [Android 特有线程机制篇](#Android 特有线程机制篇 "#%E4%B8%89android-%E7%89%B9%E6%9C%89%E7%BA%BF%E7%A8%8B%E6%9C%BA%E5%88%B6%E7%AF%87")
  4. 线程同步与安全篇
  5. 高级实践与优化篇

一、线程基础篇

Q1:Android 为什么需要多线程?

答案

Android 应用的主线程(UI 线程)负责界面绘制(每 16ms 一次,保证 60fps)和用户交互(点击、滑动等)。任何耗时操作(网络请求、数据库读写、文件 I/O、大量计算)都会阻塞主线程,导致掉帧、卡顿,甚至 ANR(Application Not Responding,Activity 5 秒无响应 / BroadcastReceiver 10 秒无响应)。多线程将耗时任务移到后台线程,保证主线程的响应性。同时多线程可充分利用多核 CPU 提升计算效率。

流程图

flowchart LR A[主线程] --> B{任务类型} B -->|UI 操作| C[立即执行] B -->|耗时操作| D[子线程执行] D --> E[结果通过 runOnUiThread / View.post 回传] E --> C

源码示例

java 复制代码
// 简单后台线程
new Thread(() -> {
    Bitmap bitmap = loadImageFromNetwork();
    runOnUiThread(() -> imageView.setImageBitmap(bitmap));
}).start();

Q2:多线程为什么会有并发问题?

答案

多线程并发问题的根源是 共享资源的非原子性操作缓存一致性指令重排序

  • 可见性:一个线程修改了共享变量,其他线程不能立即看到(每个线程有自己的工作内存/缓存)
  • 原子性 :对变量的读-改-写操作(如 count++)可能被线程调度打断,分为读、改、写三步
  • 有序性:编译器/CPU 可能重排序指令,导致多线程环境下代码执行顺序与预期不符

JMM 模型图

flowchart LR subgraph 主内存 M[共享变量 count=0] end subgraph 线程A CA[工作内存 副本=0] end subgraph 线程B CB[工作内存 副本=0] end M -->|read| CA M -->|read| CB CA -->|write| M CB -->|write| M

示例 :线程 A 和 B 同时读取 count=0,各自计算为 1 并写回,最终结果为 1 而非期望的 2 → 丢失更新


Q3:JVM 内存模型(JMM)详细说明

答案

JMM(Java Memory Model)定义了线程与主内存之间的抽象关系,是理解并发问题的理论基础:

  • 主内存:所有线程共享,存储变量
  • 工作内存:每个线程私有,存储变量的副本(缓存、寄存器)

JMM 核心三大特性:

特性 说明 保证手段
原子性 一个或多个操作要么全部执行且不被中断,要么全不执行 synchronizedLock、原子类
可见性 一个线程对共享变量的修改,其他线程能够立即看到 volatilesynchronizedLock
有序性 禁止指令重排序,保证代码执行顺序与程序顺序一致 volatilehappens-before 规则

Happens-Before 规则(JSR-133):

  1. 程序次序规则:一个线程内,书写在前的代码 happens-before 后面的代码
  2. volatile 规则:对 volatile 变量的写 happens-before 后续对该变量的读
  3. 锁规则:解锁 happens-before 后续的加锁
  4. 传递性:A happens-before B,B happens-before C ⇒ A happens-before C

源码(Unsafe 内存屏障):

java 复制代码
// JVM 底层插入内存屏障
public native void loadFence();  // LoadLoad
public native void storeFence(); // StoreStore
public native void fullFence();  // StoreLoad

Q4:线程的同步有哪些方式?

答案

方式 核心原理 适用场景
synchronized JVM 内置监视器锁(Monitor),自动加锁/释放 简单同步,方法或代码块级别
ReentrantLock AQS(AbstractQueuedSynchronizer),手动 lock/unlock 需要可中断、超时、公平锁的复杂场景
volatile 内存屏障,保证可见性和有序性,不保证原子性 布尔标志位、状态变量
原子类(AtomicInteger CAS(Compare And Swap)无锁机制 计数器、累加器
ThreadLocal 每个线程独立副本,空间换时间 避免共享(如 SimpleDateFormat
读写锁(ReadWriteLock 读共享、写互斥 读多写少场景
信号量(Semaphore 控制并发数量 限流、连接池

Q5:synchronized、ReentrantLock、volatile 的底层原理与比较

答案

synchronized 底层原理
  • 对象头(Mark Word)存储锁信息(偏向锁、轻量级锁、重量级锁标识)
  • 编译为 monitorenter / monitorexit 指令
  • 依赖操作系统的 mutex(重量级锁时)
java 复制代码
// 源码对应字节码
public void syncMethod() {
    synchronized (this) { count++; }
}
// 字节码:monitorenter → count++ → monitorexit
ReentrantLock 底层原理
  • 基于 AQS(AbstractQueuedSynchronizer)
  • 内部维护 volatile int stateCLH 等待队列
  • CAS 实现快速加锁
java 复制代码
// ReentrantLock 的非公平锁实现
final void lock() {
    if (compareAndSetState(0, 1))  // CAS 尝试获取锁
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);                // 失败则入队阻塞
}
volatile 底层原理
  • 写入 volatile 变量时,汇编层面增加 lock 前缀指令
  • 触发 缓存一致性协议(MESI),强制将当前缓存行写回主内存,并让其他 CPU 缓存行失效
  • 禁止重排序:在 volatile 写前后插入内存屏障
三者对比表
特性 synchronized ReentrantLock volatile
实现层级 JVM 内置 Java API(AQS) JVM + CPU
锁性质 可重入、非公平(默认) 可重入、公平/非公平可选 无锁
原子性保证
可见性保证
有序性保证 ✅(代码块内) ✅(禁止重排)
能否响应中断 ✅(lockInterruptibly() N/A
超时尝试 ✅(tryLock(timeout) N/A
条件变量 wait/notify Condition(可多个) N/A
性能(现代 JVM) 优化后相近 相近 非常轻量

Q6:CAS 机制、偏向锁、锁升级机制(重点)

答案

CAS(Compare And Swap)
  • 无锁原子操作:比较当前值与期望值,相等则更新为新值,否则失败
  • 由 CPU 原子指令(如 x86 的 cmpxchg)保证
  • Java 通过 sun.misc.Unsafe 类提供
java 复制代码
// AtomicInteger.incrementAndGet() 源码
public final int incrementAndGet() {
    return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
// unsafe 实现循环 CAS
do {
    old = getIntVolatile(object, offset);
} while (!compareAndSwapInt(object, offset, old, old + delta));

CAS 的问题

  • ABA 问题 :值从 A→B→A,CAS 会误认为未改变。解决:AtomicStampedReference(带版本号)
  • 自旋开销:高并发下大量失败重试消耗 CPU
锁升级机制(synchronized 优化)

JVM 为了减少锁竞争开销,对 synchronized 做了优化:

锁状态 触发条件 原理 优势
无锁 对象刚创建 Mark Word 为普通 hashCode 无竞争
偏向锁 只有一个线程访问同步块 Mark Word 存储线程 ID,每次进入无需 CAS 消除无竞争时的同步开销
轻量级锁 第二个线程开始竞争(但未自旋成功) 栈帧中创建 Lock Record,CAS 替换 Mark Word,自旋等待 避免操作系统内核态切换
重量级锁 自旋超过阈值(默认 10 次)或竞争激烈 升级为操作系统的 mutex,线程阻塞 保证公平但开销大

锁升级流程图

flowchart TD A[无锁状态] -->|第一个线程进入| B[偏向锁
Mark Word存线程ID] B -->|其他线程尝试获取锁| C[偏向锁撤销
升级为轻量级锁] C -->|自旋竞争锁| D{自旋成功?} D -->|是| E[轻量级锁,继续持有] D -->|自旋失败/次数超| F[膨胀为重量级锁
OS mutex]

源码位置hotspot/src/share/vm/runtime/synchronizer.cpp 中的 ObjectSynchronizer::slow_enter


Q7:死锁的产生条件与避免方法

答案

死锁的四个必要条件
  1. 互斥:资源一次只能被一个线程占用
  2. 持有并等待:线程持有至少一个资源,同时等待其他资源
  3. 不可剥夺:资源只能由持有者主动释放
  4. 循环等待:线程之间形成循环等待链

死锁示例

java 复制代码
Object lockA = new Object();
Object lockB = new Object();

// 线程 1
new Thread(() -> {
    synchronized (lockA) {
        sleep(100);
        synchronized (lockB) { /* 操作 */ }
    }
}).start();

// 线程 2
new Thread(() -> {
    synchronized (lockB) {
        sleep(100);
        synchronized (lockA) { /* 操作 */ }
    }
}).start();

死锁流程图

flowchart LR subgraph 死锁 T1[线程1] -- 持有锁A --> A[锁A] T1 -- 等待锁B --> B[锁B] T2[线程2] -- 持有锁B --> B T2 -- 等待锁A --> A end
避免死锁的方法
方法 说明
统一加锁顺序 所有线程按相同的顺序获取锁(先 lockA 后 lockB)
使用超时锁 tryLock(timeout),超时后释放已持有的锁
降低锁粒度 减少持有锁的时间,避免嵌套锁
使用无锁结构 原子类、ConcurrentHashMap
死锁检测 JVM 工具 jstack 可检测死锁,或使用 ThreadMXBean 编程检测

使用 tryLock 避免死锁

java 复制代码
ReentrantLock lock1 = new ReentrantLock();
ReentrantLock lock2 = new ReentrantLock();

while (true) {
    if (lock1.tryLock()) {
        try {
            if (lock2.tryLock(100, TimeUnit.MILLISECONDS)) {
                try { /* work */ } finally { lock2.unlock(); }
                break;
            }
        } finally { lock1.unlock(); }
    }
}

Q8:线程的状态、如何终止、创建形式与返回值

答案

线程的 6 种状态
状态 说明 进入方式 退出方式
NEW 线程创建但未启动 new Thread() start()
RUNNABLE 就绪或运行中 start() 时间片用完或主动让出
BLOCKED 阻塞等待锁 进入 synchronized 块且锁被占用 获取到锁
WAITING 无限期等待 wait()/join()/LockSupport.park() notify()/notifyAll()/unpark()
TIMED_WAITING 限期等待 sleep(time)/wait(time)/join(time) 超时或被唤醒
TERMINATED 已终止 run() 结束或异常 -

状态转换图

stateDiagram-v2 [*] --> NEW : new Thread() NEW --> RUNNABLE : start() RUNNABLE --> BLOCKED : 等待synchronized锁 RUNNABLE --> WAITING : wait()/join() RUNNABLE --> TIMED_WAITING : sleep(time)/wait(time) BLOCKED --> RUNNABLE : 获取到锁 WAITING --> RUNNABLE : notify()/notifyAll() TIMED_WAITING --> RUNNABLE : 超时或被唤醒 RUNNABLE --> TERMINATED : run()结束/异常 TERMINATED --> [*]
线程如何终止(正确方式)

禁止使用 Thread.stop() (强制释放锁导致数据不一致)。推荐使用中断标志

java 复制代码
// 子线程任务
while (!Thread.currentThread().isInterrupted()) {
    // 执行任务
}

// 主线程中发起中断
workerThread.interrupt();

处理 InterruptedException:捕获后可以重新设置中断标志或直接退出。

线程的创建形式及返回值
形式 有无返回值 异常抛出 使用方式
Thread 子类 无(run() void) 不支持 new MyThread().start()
Runnable 不支持 new Thread(runnable).start()
Callable 有(call() 返回 V) 支持 FutureTask + Thread 或线程池

获取返回值的示例

java 复制代码
Callable<String> callable = () -> "result";
FutureTask<String> task = new FutureTask<>(callable);
new Thread(task).start();
String result = task.get();  // 阻塞直到完成

Q9:wait()、notify()、notifyAll() 的作用?与 sleep() 的区别?

答案
wait()notify()notifyAll()Object 类的 native 方法,用于线程间协作,必须在同步块(synchronized)内调用

方法 作用
wait() 释放当前对象的锁,线程进入 WAITING 状态,等待被 notify/notifyAll 唤醒
wait(timeout) 带超时的等待,超时后自动唤醒
notify() 随机唤醒一个在该对象上等待的线程
notifyAll() 唤醒所有在该对象上等待的线程
sleep() vs wait() 对比
对比项 sleep() wait()
所属类 Thread 静态方法 Object 实例方法
是否释放锁 ❌ 不释放 ✅ 释放
是否需要同步块 ❌ 不需要 ✅ 必须(否则抛 IllegalMonitorStateException
唤醒方式 时间到或 interrupt() notify/notifyAll/超时
适用场景 线程暂停一段时间 线程间通信(生产者-消费者)

示例(生产者-消费者简化):

java 复制代码
synchronized (queue) {
    while (queue.isEmpty()) {
        queue.wait();   // 释放锁,等待数据
    }
    Object item = queue.poll();
}

Q10:进程与线程的核心区别?线程、线程池、协程的区别?为什么协程是最优解?

答案

进程 vs 线程
维度 进程 线程
资源分配 系统资源分配的基本单位 不独立拥有资源,共享进程资源
地址空间 独立的地址空间 共享进程地址空间
切换开销 大(涉及地址空间切换、刷新 TLB) 小(仅保存/恢复 PC、寄存器)
通信方式 IPC(Binder、Socket、共享内存等) 直接读写共享变量(需同步)
健壮性 进程崩溃不影响其他进程 线程崩溃导致整个进程崩溃
线程 vs 线程池 vs 协程
对比项 线程(Thread) 线程池(ThreadPool) 协程(Coroutine)
创建开销 高(~1MB 栈内存) 复用线程,减少创建 极低(~几 KB 栈)
调度方式 操作系统内核调度 内核调度 用户态调度(程序控制)
并发数量 受限(几千) 受限(线程池大小) 可达数十万
阻塞影响 阻塞时线程挂起,浪费资源 同线程 挂起不阻塞线程,可继续执行其他协程
取消机制 interrupt() 较麻烦 同线程 结构化并发,自动取消
生命周期管理 手动 手动关闭 自动绑定作用域(如 lifecycleScope
为什么协程是最优解(在 Android 中)?
  1. 轻量:一个线程可运行成千上万个协程,避免 OOM
  2. 非阻塞挂起:遇到 IO 等待时自动挂起,释放底层线程去执行其他协程,提高吞吐量
  3. 结构化并发:父协程取消时自动取消所有子协程,避免内存泄漏
  4. 简洁异步代码 :用同步风格写异步逻辑(suspend 函数),告别回调地狱
  5. 与 LiveData/Flow 无缝集成:响应式编程友好

示例

kotlin 复制代码
lifecycleScope.launch(Dispatchers.IO) {
    val data = fetchData()   // 挂起函数,不阻塞线程
    withContext(Dispatchers.Main) {
        textView.text = data
    }
}

二、线程池核心原理篇

Q11:ThreadPoolExecutor 的 7 个核心参数及工作流程

答案

java 复制代码
public ThreadPoolExecutor(
    int corePoolSize,           // 核心线程数(常驻存活)
    int maximumPoolSize,        // 最大线程数(核心 + 临时)
    long keepAliveTime,         // 临时线程空闲存活时间
    TimeUnit unit,              // 时间单位
    BlockingQueue<Runnable> workQueue,  // 任务阻塞队列
    ThreadFactory threadFactory,        // 线程工厂
    RejectedExecutionHandler handler    // 拒绝策略
)
参数 含义
corePoolSize 核心线程数,即使空闲也不会被回收(除非设置 allowCoreThreadTimeOut(true)
maximumPoolSize 最大线程数 = 核心线程 + 非核心线程
keepAliveTime 非核心线程空闲存活时间,超过则回收
unit 存活时间单位(TimeUnit.SECONDS 等)
workQueue 任务阻塞队列,用于暂存等待执行的任务
threadFactory 线程工厂,可自定义线程名称、优先级、是否为守护线程
handler 拒绝策略,当队列满且线程数达最大时触发

队列选择

队列类型 特点 适用
ArrayBlockingQueue 有界,FIFO 控制资源使用
LinkedBlockingQueue 可选有界/无界,吞吐量高 默认无界(注意 OOM)
SynchronousQueue 容量为 0,必须立即有线程处理 CachedThreadPool
PriorityBlockingQueue 优先级队列,无界 任务分优先级

工作流程图

flowchart TD A[提交任务] --> B{当前线程数
< corePoolSize?} B -->|是| C[创建核心线程
执行任务] B -->|否| D{任务入队成功?} D -->|是| E[等待核心线程空闲
从队列取任务执行] D -->|否| F{当前线程数
< maximumPoolSize?} F -->|是| G[创建非核心线程
执行任务] F -->|否| H[触发拒绝策略]

源码分析(execute() 方法)

java 复制代码
// java.util.concurrent.ThreadPoolExecutor#execute
public void execute(Runnable command) {
    if (command == null) throw new NullPointerException();
    int c = ctl.get();
    
    // 1. 当前线程数 < corePoolSize → 创建核心线程
    if (workerCountOf(c) < corePoolSize) {
        if (addWorker(command, true))
            return;
        c = ctl.get();
    }
    
    // 2. 任务入队
    if (isRunning(c) && workQueue.offer(command)) {
        int recheck = ctl.get();
        if (!isRunning(recheck) && remove(command))
            reject(command);
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false);  // 创建非核心线程处理队列
    }
    // 3. 队列已满 → 创建非核心线程
    else if (!addWorker(command, false))
        reject(command);  // 队列满且线程已达最大 → 拒绝
}

Q12:为什么 Executors 工具类创建的线程池存在风险?(生产环境禁止使用)

答案

Executors 的四种工厂方法都存在隐患:

方法 默认配置 隐患
newFixedThreadPool(n) LinkedBlockingQueue 无界队列 任务堆积可能 OOM
newSingleThreadExecutor() LinkedBlockingQueue 无界队列 同上
newCachedThreadPool() maxPoolSize=Integer.MAX_VALUE 突发大量任务创建海量线程,CPU 过载
newScheduledThreadPool(n) DelayedWorkQueue 无界队列 任务堆积可能 OOM

正确做法 :直接使用 ThreadPoolExecutor 构造函数,根据业务场景配置有界队列和合理的线程数。


Q13:CPU 密集型 vs IO 密集型任务的线程池参数如何设计?

答案

任务类型 特征 核心线程数建议 队列选择 典型场景
CPU 密集型 大量计算,CPU 占用高 CPU核数 + 1 ArrayBlockingQueue(有界) 图像处理、加密、复杂运算
IO 密集型 大量等待(网络/磁盘) CPU核数 × 2 或更大 SynchronousQueueLinkedBlockingQueue 网络请求、文件读写、数据库操作
混合型 部分计算 + 部分等待 根据比例估算 有界队列 大多数业务逻辑

公式推导(IO 密集型):

scss 复制代码
最佳线程数 = CPU核数 / (1 - 阻塞系数)
阻塞系数 = IO等待时间 / (CPU计算时间 + IO等待时间)

例如:阻塞系数 0.9(90% 时间在等待),4 核 → 4 / (1-0.9) = 40 线程。

Android 设备实际配置建议

java 复制代码
int cpuCores = Runtime.getRuntime().availableProcessors();

// CPU 密集型(图片压缩)
ThreadPoolExecutor cpuPool = new ThreadPoolExecutor(
    cpuCores + 1, cpuCores + 1, 60L, TimeUnit.SECONDS,
    new ArrayBlockingQueue<>(20),
    new ThreadPoolExecutor.CallerRunsPolicy()
);

// IO 密集型(网络请求)
ThreadPoolExecutor ioPool = new ThreadPoolExecutor(
    cpuCores * 2, cpuCores * 4, 30L, TimeUnit.SECONDS,
    new LinkedBlockingQueue<>(100),
    new ThreadPoolExecutor.DiscardPolicy()
);

Q14:线程池的 4 种拒绝策略及其适用场景

答案

策略 行为 适用场景
AbortPolicy(默认) 直接抛出 RejectedExecutionException 严格要求任务不丢失,失败需立即感知
CallerRunsPolicy 由调用者线程执行任务 降低任务提交速度,防止雪崩
DiscardPolicy 静默丢弃新任务 非关键任务,可接受丢失
DiscardOldestPolicy 丢弃队列头部的任务,重试提交新任务 实时性要求高的场景(如 IM 消息)

自定义拒绝策略示例

java 复制代码
RejectedExecutionHandler customHandler = (r, executor) -> {
    Log.e("ThreadPool", "Task rejected, queue size: " + executor.getQueue().size());
    saveToLocalStorage(r);  // 降级:存入本地数据库稍后重试
};

Q15:线程池的状态及其转换(面试进阶题)

答案

ThreadPoolExecutor 使用一个 AtomicInteger ctl 同时存储运行状态(高 3 位)和线程数(低 29 位):

java 复制代码
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
private static final int COUNT_BITS = Integer.SIZE - 3;  // 29
private static final int CAPACITY = (1 << COUNT_BITS) - 1;

// 5 种状态(高 3 位)
private static final int RUNNING    = -1 << COUNT_BITS;  // 接受新任务并处理队列
private static final int SHUTDOWN   =  0 << COUNT_BITS;  // 不接受新任务,处理队列
private static final int STOP       =  1 << COUNT_BITS;  // 不接受新任务,不处理队列,中断执行中的线程
private static final int TIDYING    =  2 << COUNT_BITS;  // 所有任务终止,workerCount=0
private static final int TERMINATED =  3 << COUNT_BITS;  // terminated() 执行完成
状态 高3位值 描述 是否接受新任务 是否处理队列任务
RUNNING -1 正常运行
SHUTDOWN 0 调用了 shutdown() ✅(处理完队列中的任务)
STOP 1 调用了 shutdownNow() ❌(中断正在执行的任务)
TIDYING 2 所有任务已终止,workerCount=0
TERMINATED 3 terminated() 钩子方法执行完毕

状态转换图

flowchart LR RUNNING -->|shutdown| SHUTDOWN RUNNING -->|shutdownNow| STOP SHUTDOWN -->|队列为空且所有任务完成| TIDYING STOP -->|所有工作线程退出| TIDYING TIDYING -->|terminated执行完成| TERMINATED

Q16:线程池如何实现任务优先级排序?实战场景(直播弹幕 > 日志上报)

答案

使用 PriorityBlockingQueue 作为任务队列,队列根据任务的优先级排序。

实现步骤
  1. 定义优先级接口
java 复制代码
public interface PriorityRunnable extends Runnable {
    int getPriority();   // 数值越小优先级越高
}
  1. 创建优先级队列
java 复制代码
BlockingQueue<Runnable> queue = new PriorityBlockingQueue<>(11, 
    (r1, r2) -> {
        int p1 = (r1 instanceof PriorityRunnable) ? ((PriorityRunnable) r1).getPriority() : Integer.MAX_VALUE;
        int p2 = (r2 instanceof PriorityRunnable) ? ((PriorityRunnable) r2).getPriority() : Integer.MAX_VALUE;
        return Integer.compare(p1, p2);
    }
);
  1. 创建线程池
java 复制代码
ThreadPoolExecutor executor = new ThreadPoolExecutor(
    2, 4, 30L, TimeUnit.SECONDS, queue
);
  1. 提交带优先级的任务
java 复制代码
// 高优先级:弹幕渲染(priority=1)
executor.execute(() -> renderDanmaku());

// 低优先级:日志上报(priority=10)
executor.execute(() -> uploadLog());
直播场景实战
  • 弹幕渲染任务 :必须快速处理,否则用户感觉卡顿 → priority = 1
  • 日志上报任务 :可以延迟,不影响体验 → priority = 10
  • 还可结合 优先级动态调整:当队列长度超过阈值时,自动提升某些任务的优先级。

注意事项

  • PriorityBlockingQueue无界队列,必须配合其他措施防止 OOM
  • 优先级相同的任务不保证 FIFO,可结合 AtomicLong 添加序列号实现二次排序

流程图

text 复制代码
提交高优先级弹幕任务 -> 入队 -> 排在队头
提交低优先级日志任务 -> 入队 -> 排在队尾
Worker线程从队列take() -> 获取队头(高优先级) -> 执行

Q17:线程池如何调优,如何优化?

答案

调优步骤
  1. 确定任务类型 :CPU 密集型还是 IO 密集型?决定 corePoolSize 和队列
  2. 设置合理的队列容量:无界队列可能导致 OOM,有界队列需要根据任务峰值设置
  3. 选择合适的拒绝策略
    • 关键任务 → AbortPolicy(抛出异常,上层重试)
    • 允许降级 → CallerRunsPolicy(调用者线程执行,减缓提交速度)
    • 丢弃非关键任务 → DiscardPolicy / DiscardOldestPolicy
  4. 监控线程池状态:活跃线程数、队列长度、拒绝次数
  5. 动态调整参数 (高级):setCorePoolSize()setMaximumPoolSize() 可以在运行时修改
  6. 线程命名 :通过 ThreadFactory 给线程设置有意义的名称,便于 jstack 调试
  7. 避免任务泄漏 :确保 Runnable 不会因为异常导致线程意外终止
优化技巧
java 复制代码
// 动态调整核心线程数(根据当前负载)
public void adjustPoolSize(int targetCoreSize) {
    int current = executor.getCorePoolSize();
    if (targetCoreSize > current) {
        executor.setCorePoolSize(targetCoreSize);
        executor.setMaximumPoolSize(targetCoreSize * 2);
    } else if (targetCoreSize < current) {
        executor.setCorePoolSize(targetCoreSize);
        executor.setMaximumPoolSize(targetCoreSize * 2);
    }
}

// 线程命名工厂
ThreadFactory namedFactory = new ThreadFactoryBuilder()
    .setNameFormat("MyPool-%d")
    .setDaemon(false)
    .setPriority(Thread.NORM_PRIORITY)
    .build();

调优闭环流程图

flowchart LR A[压测] --> B[监控指标] B --> C{CPU/队列深度/拒绝次数} C -->|异常| D[调整参数] D --> A C -->|正常| E[保持配置]

三、Android 特有线程机制篇

Q18:Binder 线程池的工作原理(高级 Framework 题)

答案

Binder 线程池是 Android Framework 层面实现跨进程通信的核心机制,与传统 Java 线程池设计完全不同。

核心特点
  1. 非传统线程池结构 :每个进程只有一个 PoolThread 类继承自 Thread
  2. 线程创建和管理由 Binder Driver 控制,而非应用层
  3. 主线程 + 非主线程:每个 binder 线程池只有一个主线程,启动时创建;非主线程由 binder driver 按需创建
  4. 默认大小:16 (1 个主线程 + 15 个非主线程),可通过 ProcessState::setMaxThreads() 修改
源码分析(C++ 层)
cpp 复制代码
// frameworks/native/libs/binder/ProcessState.cpp
void ProcessState::startThreadPool() {
    AutoMutex _l(mLock);
    if (!mThreadPoolStarted) {
        mThreadPoolStarted = true;
        spawnPooledThread(true);  // 创建主线程,isMain=true
    }
}

void ProcessState::spawnPooledThread(bool isMain) {
    if (mThreadPoolStarted) {
        String8 name = makeBinderThreadName();  // 格式 "Binder:x"
        sp<Thread> t = new PoolThread(isMain);
        t->run(name.string());
    }
}

// PoolThread 的 run 最终会调用 IPCThreadState::joinThreadPool
void IPCThreadState::joinThreadPool(bool isMain) {
    // 进入循环,从 Binder Driver 读取命令并处理
    do {
        result = talkWithDriver();   // 阻塞等待请求
        executeCommand(cmd);          // 执行跨进程调用
    } while (result != -ECONNREFUSED && result != -EBADF);
}
非主线程的创建时机
  • Client 进程向 Binder Driver 发送 IPC 请求时
  • 当 Client 进程发起 IPC 请求,且 Server 进程当前所有 Binder 线程都处于忙碌状态时,Binder Driver 会通知 Server 进程 新创建一条 Binder 线程(除非已达到最大线程数)
验证 Binder 线程数
bash 复制代码
# adb shell 查看进程的 binder 线程
ps -T | grep binder | grep <your_pid>
# 输出类似:
# u0_a123  12345  12350  binder:12345_1
# u0_a123  12345  12351  binder:12345_2
应用层注意事项
  • 普通应用开发者通常不需要直接操作 Binder 线程池,但需要知道:大量同步 Binder 调用可能导致 Binder 线程耗尽,引发 ANR
  • 避免在主线程做长时间的 Binder 调用(如跨进程查询数据库)

Binder IPC 流程图

sequenceDiagram participant Client as Client进程 participant Driver as Binder Driver participant Server as Server进程 participant Pool as Binder线程池 Client->>Driver: 发起IPC请求 Driver->>Pool: 分配空闲线程 Pool->>Server: 执行onTransact() Server-->>Pool: 返回结果 Pool-->>Driver: 传递结果 Driver-->>Client: 返回结果

四、线程同步与安全篇

Q19:ThreadLocal 的原理及内存泄漏问题

答案

原理

每个 Thread 内部维护一个 ThreadLocalMap,以 ThreadLocal 的弱引用为 key,存储线程隔离的值。

java 复制代码
// Thread.java
ThreadLocal.ThreadLocalMap threadLocals = null;

// ThreadLocal.java
public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) return (T) e.value;
    }
    return setInitialValue();
}

ThreadLocalMap.Entry 的弱引用设计

java 复制代码
static class Entry extends WeakReference<ThreadLocal<?>> {
    Object value;
    Entry(ThreadLocal<?> k, Object v) {
        super(k);  // key 是弱引用
        value = v;
    }
}
内存泄漏问题分析
场景 是否泄漏 原因
Thread 存活,ThreadLocal 被 GC ✅ 泄漏 value 被 Entry 强引用,无法回收
Thread 销毁 ❌ 不泄漏 ThreadLocalMap 随之销毁
主动调用 remove() ❌ 不泄漏 手动清除 Entry

解决方案 :使用后调用 remove()

java 复制代码
ThreadLocal<SimpleDateFormat> dateFormat = ThreadLocal.withInitial(
    () -> new SimpleDateFormat("yyyy-MM-dd")
);
try {
    dateFormat.get().format(new Date());
} finally {
    dateFormat.remove();  // 防止内存泄漏
}
应用场景
  • 避免 SimpleDateFormat 多线程共享
  • 存储每个线程的 Looper(Handler 机制)
  • 存储每个线程的数据库连接、Session 等

Q20:原子类(AtomicXXX)的实现原理(CAS + volatile)

答案

原子类(如 AtomicInteger)内部使用 volatile 保证可见性 + CAS 保证原子性

java 复制代码
public class AtomicInteger {
    private volatile int value;
    
    public final int incrementAndGet() {
        return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
    }
}

CAS 循环

java 复制代码
do {
    old = this.get();
    new = old + delta;
} while (!compareAndSwap(old, new));

常用原子类

原子类 用途
AtomicInteger / AtomicLong 整数/长整型原子操作
AtomicBoolean 布尔值原子操作
AtomicReference<V> 引用类型原子操作
AtomicIntegerArray 整数数组原子操作
LongAdder 高并发累加器,分段计数,性能优于 AtomicLong
AtomicStampedReference 带版本号的引用,解决 ABA 问题

适用场景

  • 计数器(请求计数、统计)
  • 标识位(开关状态)
  • 累加器(性能统计)

Q21:CountDownLatch、CyclicBarrier、Semaphore 的使用场景与原理

答案

同步器 原理 使用场景
CountDownLatch 计数器递减,归零时释放等待线程 一个线程等待多个任务完成
CyclicBarrier 计数器递减到零后重置,所有线程同时继续 多线程相互等待到齐
Semaphore 许可证机制,控制同时访问数量 限流、连接池
CountDownLatch 示例
java 复制代码
CountDownLatch latch = new CountDownLatch(3);
for (int i = 0; i < 3; i++) {
    executor.execute(() -> {
        doWork();
        latch.countDown();  // 计数器减1
    });
}
latch.await();  // 等待3个任务完成
CyclicBarrier 示例
java 复制代码
CyclicBarrier barrier = new CyclicBarrier(3, () -> {
    System.out.println("所有人都到达,继续执行");
});
for (int i = 0; i < 3; i++) {
    executor.execute(() -> {
        doWork();
        barrier.await();  // 等待其他线程
        doNextWork();
    });
}
Semaphore 示例
java 复制代码
Semaphore semaphore = new Semaphore(10); // 最多10个并发
semaphore.acquire();
try {
    doLimitedTask();
} finally {
    semaphore.release();
}

流程图(CountDownLatch)

sequenceDiagram participant Main as 主线程 participant T1 as 线程1 participant T2 as 线程2 participant T3 as 线程3 Main->>T1: 启动 Main->>T2: 启动 Main->>T3: 启动 Main->>Main: latch.await() 阻塞 T1->>T1: doWork() T1->>Main: countDown() T2->>T2: doWork() T2->>Main: countDown() T3->>T3: doWork() T3->>Main: countDown() Main->>Main: 被唤醒,继续执行

Q22:Future 和 CompletableFuture 的异步编程

答案

Future
  • 表示异步计算结果
  • 通过 get() 阻塞获取结果
  • 缺点:不能手动完成、无回调、不能链式组合
java 复制代码
Future<String> future = executor.submit(() -> "result");
String result = future.get();  // 阻塞
CompletableFuture(Java 8+)
  • 实现 FutureCompletionStage
  • 支持回调、组合、异常处理
java 复制代码
CompletableFuture.supplyAsync(() -> fetchData())
    .thenApply(data -> process(data))
    .thenAccept(result -> updateUI(result))
    .exceptionally(e -> { log(e); return null; });

CompletableFuture 常用方法

方法 作用
supplyAsync() 异步执行有返回值的任务
thenApply() 转换结果
thenAccept() 消费结果(无返回值)
thenRun() 执行 Runnable
thenCompose() 链式组合多个异步任务
thenCombine() 组合两个独立任务的结果
allOf() 等待所有任务完成
anyOf() 等待任意一个任务完成
exceptionally() 异常处理

在 Android 中:可与线程池结合,但注意生命周期(需手动取消)。

java 复制代码
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
    // 耗时操作
}, executor);

// Activity 销毁时取消
@Override
protected void onDestroy() {
    future.cancel(true);
    super.onDestroy();
}

五、高级实践与优化篇

Q23:Android 中如何监控线程池的状态?

答案

方法一:自定义 ThreadPoolExecutor 添加统计
java 复制代码
public class MonitoredThreadPoolExecutor extends ThreadPoolExecutor {
    private final AtomicLong totalTaskCount = new AtomicLong(0);
    private final AtomicLong completedTaskCount = new AtomicLong(0);
    private final AtomicLong rejectedTaskCount = new AtomicLong(0);
    private final AtomicLong maxQueueSize = new AtomicLong(0);
    
    public MonitoredThreadPoolExecutor(...) { super(...); }
    
    @Override
    protected void beforeExecute(Thread t, Runnable r) {
        super.beforeExecute(t, r);
        // 记录任务开始时间
        ((MonitoredRunnable) r).setStartTime(System.currentTimeMillis());
    }
    
    @Override
    protected void afterExecute(Runnable r, Throwable t) {
        super.afterExecute(r, t);
        completedTaskCount.incrementAndGet();
        long cost = System.currentTimeMillis() - ((MonitoredRunnable) r).getStartTime();
        if (cost > 1000) {
            Log.w("ThreadPool", "Slow task: " + cost + "ms");
        }
    }
    
    @Override
    public void execute(Runnable command) {
        totalTaskCount.incrementAndGet();
        super.execute(command);
        long currentQueueSize = getQueue().size();
        maxQueueSize.updateAndGet(old -> Math.max(old, currentQueueSize));
    }
    
    @Override
    protected void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        rejectedTaskCount.incrementAndGet();
        super.rejectedExecution(r, e);
    }
    
    public void logStatus() {
        Log.d("ThreadPool", String.format(
            "PoolSize=%d, Active=%d, Queue=%d, Completed=%d, Rejected=%d, MaxQueue=%d",
            getPoolSize(), getActiveCount(), getQueue().size(),
            completedTaskCount.get(), rejectedTaskCount.get(), maxQueueSize.get()
        ));
    }
}
方法二:使用 Android Profiler
  • CPU Profiler:实时查看线程数量、线程状态(运行/等待/阻塞)
  • Memory Profiler:检测线程导致的内存泄漏
方法三:定期 dump 线程信息
bash 复制代码
adb shell kill -3 <pid>   # 生成 traces.txt

或在代码中:

java 复制代码
ThreadMXBean threadBean = ManagementFactory.getThreadMXBean();
long[] threadIds = threadBean.getAllThreadIds();
for (long id : threadIds) {
    ThreadInfo info = threadBean.getThreadInfo(id, Integer.MAX_VALUE);
    Log.d("ThreadDump", info.toString());
}
方法四:集成 LeakCanary

LeakCanary 可以检测 Activity 泄漏,间接发现因线程池任务持有 Activity 引用导致的泄漏。

方法五:使用 StrictMode
java 复制代码
StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
    .detectDiskReads()
    .detectDiskWrites()
    .detectNetwork()
    .penaltyLog()
    .build());

监控流程图

flowchart LR A[定时任务每30s] --> B[调用getActiveCount/getQueue.size] B --> C[上报到监控平台] C --> D{告警规则} D -->|队列深度>1000| E[发送告警] D -->|active>corePoolSize*1.5| E

Q24:如何避免使用线程池导致的内存泄漏?

答案

常见泄漏场景与解决方案
泄漏场景 解决方案
任务持有 Activity 引用 使用 WeakReference 或传入 ApplicationContext
线程池未关闭 Application 级别共享,无需关闭
匿名内部类任务 改为静态内部类 + 弱引用
延迟任务未取消 取消所有未执行任务
java 复制代码
public class ImageLoader {
    private static final ExecutorService sExecutor = 
        new MonitoredThreadPoolExecutor(...);  // 单例
    
    public void loadImage(String url, ImageView imageView) {
        sExecutor.submit(new ImageLoadTask(url, new WeakReference<>(imageView)));
    }
    
    private static class ImageLoadTask implements Runnable {
        private final String url;
        private final WeakReference<ImageView> imageViewRef;
        
        ImageLoadTask(String url, WeakReference<ImageView> ref) {
            this.url = url;
            this.imageViewRef = ref;
        }
        
        @Override
        public void run() {
            Bitmap bitmap = loadFromNetwork(url);
            // 切换到主线程更新 UI
            mainHandler.post(() -> {
                ImageView iv = imageViewRef.get();
                if (iv != null && iv.getContext() != null) {
                    iv.setImageBitmap(bitmap);
                }
            });
        }
    }
}
最佳实践
  1. Application 级别维护统一的线程池实例,避免重复创建
  2. 使用自定义线程池而非 Executors 工具类
  3. 结合 LeakCanary + Android Profiler 监控线程和内存状态
  4. 新项目优先使用 Kotlin 协程,与现有线程池可以混合使用
  5. 页面销毁时取消所有未完成的任务
java 复制代码
// Activity 中取消任务
@Override
protected void onDestroy() {
    for (Future<?> future : pendingTasks) {
        future.cancel(true);
    }
    super.onDestroy();
}

相关推荐
AI人工智能+电脑小能手2 小时前
【大白话说Java面试题】【Java基础篇】第3题:ArrayList和LinkedList有什么区别
java·开发语言·后端·面试·list
AI人工智能+电脑小能手2 小时前
【大白话说Java面试题】【Java基础篇】第4题:LinkedList是单向链表还是双向链表
java·开发语言·数据结构·后端·链表·面试·list
测试199812 小时前
2026最新软件测试面试八股文【附文档】
自动化测试·软件测试·python·测试工具·面试·职场和发展·测试用例
zmsofts12 小时前
java面试必问13:MyBatis 一级缓存、二级缓存:从原理到脏数据,一篇讲透
java·面试·mybatis
冬奇Lab12 小时前
Android 开发要变天了:Google 专为 Agent 重建工具链,Token 减少 70%、速度提升 3 倍
android·人工智能·ai编程
我叫黑大帅12 小时前
为什么map查找时间复杂度是O(1)?
后端·算法·面试
M ? A13 小时前
Vue 动态组件在 React 中,VuReact 会如何实现?
前端·javascript·vue.js·经验分享·react.js·面试·vureact
imuliuliang15 小时前
存储过程(SQL)
android·数据库·sql
AgCl2316 小时前
MYSQL-6-函数与约束-3/17
android·数据库·mysql