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

36 阅读22分钟

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

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


目录

  1. 线程基础篇
  2. 线程池核心原理篇
  3. Android 特有线程机制篇
  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

源码示例

// 简单后台线程
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 内存屏障):

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

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

答案

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

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

答案

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

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

答案

CAS(Compare And Swap)
  • 无锁原子操作:比较当前值与期望值,相等则更新为新值,否则失败
  • 由 CPU 原子指令(如 x86 的 cmpxchg)保证
  • Java 通过 sun.misc.Unsafe 类提供
// 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[偏向锁<br/>Mark Word存线程ID]
    B -->|其他线程尝试获取锁| C[偏向锁撤销<br/>升级为轻量级锁]
    C -->|自旋竞争锁| D{自旋成功?}
    D -->|是| E[轻量级锁,继续持有]
    D -->|自旋失败/次数超| F[膨胀为重量级锁<br/>OS mutex]

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


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

答案

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

死锁示例

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 避免死锁

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()(强制释放锁导致数据不一致)。推荐使用中断标志

// 子线程任务
while (!Thread.currentThread().isInterrupted()) {
    // 执行任务
}

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

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

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

获取返回值的示例

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/超时
适用场景线程暂停一段时间线程间通信(生产者-消费者)

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

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 无缝集成:响应式编程友好

示例

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

二、线程池核心原理篇

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

答案

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

源码分析(execute() 方法)

// 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核数 + 1ArrayBlockingQueue(有界)图像处理、加密、复杂运算
IO 密集型大量等待(网络/磁盘)CPU核数 × 2 或更大SynchronousQueueLinkedBlockingQueue网络请求、文件读写、数据库操作
混合型部分计算 + 部分等待根据比例估算有界队列大多数业务逻辑

公式推导(IO 密集型):

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

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

Android 设备实际配置建议

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 消息)

自定义拒绝策略示例

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

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

答案

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

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正常运行
SHUTDOWN0调用了 shutdown()✅(处理完队列中的任务)
STOP1调用了 shutdownNow()❌(中断正在执行的任务)
TIDYING2所有任务已终止,workerCount=0
TERMINATED3terminated() 钩子方法执行完毕

状态转换图

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

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

答案

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

实现步骤
  1. 定义优先级接口
public interface PriorityRunnable extends Runnable {
    int getPriority();   // 数值越小优先级越高
}
  1. 创建优先级队列
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. 创建线程池
ThreadPoolExecutor executor = new ThreadPoolExecutor(
    2, 4, 30L, TimeUnit.SECONDS, queue
);
  1. 提交带优先级的任务
// 高优先级:弹幕渲染(priority=1)
executor.execute(() -> renderDanmaku());

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

注意事项

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

流程图

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

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

答案

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

// 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 的弱引用设计

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()

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 保证原子性

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

CAS 循环

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 示例
CountDownLatch latch = new CountDownLatch(3);
for (int i = 0; i < 3; i++) {
    executor.execute(() -> {
        doWork();
        latch.countDown();  // 计数器减1
    });
}
latch.await();  // 等待3个任务完成
CyclicBarrier 示例
CyclicBarrier barrier = new CyclicBarrier(3, () -> {
    System.out.println("所有人都到达,继续执行");
});
for (int i = 0; i < 3; i++) {
    executor.execute(() -> {
        doWork();
        barrier.await();  // 等待其他线程
        doNextWork();
    });
}
Semaphore 示例
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() 阻塞获取结果
  • 缺点:不能手动完成、无回调、不能链式组合
Future<String> future = executor.submit(() -> "result");
String result = future.get();  // 阻塞
CompletableFuture(Java 8+)
  • 实现 FutureCompletionStage
  • 支持回调、组合、异常处理
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 中:可与线程池结合,但注意生命周期(需手动取消)。

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

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

五、高级实践与优化篇

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

答案

方法一:自定义 ThreadPoolExecutor 添加统计
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 线程信息
adb shell kill -3 <pid>   # 生成 traces.txt

或在代码中:

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
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 级别共享,无需关闭
匿名内部类任务改为静态内部类 + 弱引用
延迟任务未取消取消所有未执行任务
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. 页面销毁时取消所有未完成的任务
// Activity 中取消任务
@Override
protected void onDestroy() {
    for (Future<?> future : pendingTasks) {
        future.cancel(true);
    }
    super.onDestroy();
}