目录
  1. 1. 一、基础概念
    1. 1.1. 1.1 CPU核心数和线程数的关系
    2. 1.2. 1.2 CPU时间片轮转机制
    3. 1.3. 1.3 进程和线程
    4. 1.4. 1.4 并行和并发
  2. 2. 二、Java内存模型(JMM)
    1. 2.1. 2.1 happens-before 原则
    2. 2.2. 2.2 volatile 的底层屏障
  3. 3. 三、synchronized 的锁升级过程
    1. 3.1. 3.1 synchronized 的底层实现
  4. 4. 四、高并发编程的意义
    1. 4.1. 4.1 多线程优势
    2. 4.2. 4.2 多线程开发注意事项
  5. 5. 五、AQS(AbstractQueuedSynchronizer)深度剖析
    1. 5.1. 5.1 ReentrantLock 的公平锁与非公平锁
  6. 6. 六、ThreadPoolExecutor 参数详解
  7. 7. 七、ForkJoinPool 和工作窃取
  8. 8. 八、CompletableFuture:异步编程利器
  9. 9. 九、Android 特有的线程机制
    1. 9.1. 9.1 Looper / Handler / MessageQueue
    2. 9.2. 9.2 HandlerThread
  10. 10. 十、认识Java线程
    1. 10.1. 10.1 Java里的程序天生就是多线程的
    2. 10.2. 10.2 线程的启动与中止
    3. 10.3. 10.3 线程的状态
  11. 11. 十一、死锁
    1. 11.1. 11.1 死锁概念
    2. 11.2. 11.2 死锁的预防与解决
    3. 11.3. 11.3 死锁的危害
  12. 12. 十二、面试常问题目
Java进阶之多线程与Android性能优化

一、基础概念

1.1 CPU核心数和线程数的关系

多核心:也指单芯片多处理器(Chip Multiprocessors,简称 CMP),由美国斯坦福大学提出,其思想是将大规模并行处理器中的 SMP(对称多处理器)集成到同一芯片内,各个处理器并行执行不同的进程。这种依靠多个 CPU 同时并行地运行程序是实现超高速计算的一个重要方向,称为并行处理。

多线程:Simultaneous Multithreading,简称 SMT。SMT 可通过复制处理器上的结构状态,让同一个处理器上的多个线程同步执行并共享处理器的执行资源,可最大限度地实现宽发射、乱序的超标量处理,提高处理器运算部件的利用率,缓和由于数据相关或 Cache 未命中带来的访问内存延时。

核心数、线程数:增加核心数目就是为了增加线程数,因为操作系统是通过线程来执行任务的,一般情况下它们是 1:1 对应关系。但 Intel 引入超线程技术后,使核心数与线程数形成 1:2 的关系。

1.2 CPU时间片轮转机制

我们平时在开发的时候,并没有感受到 CPU 核心数限制带来的影响,想启动线程就启动线程,哪怕是在单核 CPU 上,为什么?这是因为 CPU 提供了一种 CPU 时间片轮转机制。

时间片轮转调度是一种最古老、最简单、最公平且使用最广的算法,又称 RR 调度。每个进程被分配一个时间段,称作它的时间片,即该进程允许运行的时间。

如果在时间片结束时进程还在运行,则 CPU 将被剥夺并分配给另一个进程。如果进程在时间片结束前阻塞或结束,则 CPU 当即进行切换。调度程序所要做的就是维护一张就绪进程列表,当进程用完它的时间片后,即被移到队列的末尾。

时间片轮转调度中唯一有趣的一点是时间片的长度。从一个进程切换到另一个进程是需要定时间的,包括保存和装入寄存器值及内存映像,更新各种表格和队列等。假如进程切换(context switch),需要 5ms,再假设时间片设为 20ms,则在做完 20ms 有用的工作之后,CPU 将花费 5ms 来进行进程切换。CPU 时间的 20% 被浪费在了管理开销上。

为了提高 CPU 效率,我们可以将时间片设为 5000ms,这时浪费的时间只有 0.1%。但考虑到在一个分时系统中,如果有 10 个交互用户几乎同时按下回车键,将发生什么情况?假设所有其他进程都用足他们的时间片的话,最后一个不幸的进程不得不等待 5s 才获得运行机会,多数用户无法忍受一条简短命令要 5s 才做出响应。

结论可以归结如下: 时间片设的太短会导致过多的进程切换,降低了 CPU 效率;而设的太长又可能引起对短的交互请求的响应变差。将时间片设为 100ms 通常是一个比较合理的折中。

1.3 进程和线程

进程是程序运行资源分配的最小单位。进程是操作系统进行资源分配的最小单位,其中资源包括:CPU、内存空间、磁盘等等,同一进程中的多条线程共享该进程中的全部系统资源,而进程和进程之间是相互独立的。

线程是 CPU 调度的最小单位,且必须依赖于进程而存在。线程是进程的一个实体,是 CPU 调度和分派的基本单位,它是比进程更小的、能独立运行的基本单位,线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。

线程无处不在。任何一个程序都必须要创建线程,特别是 Java,不管任何程序都必须启动一个 main 函数的主线程;Java Web 开发里面的定时任务、定时器、JSP 和 Servlet、异步消息处理机制、远程访问接口等,任何一个监听事件,onClick 的触发事件等都离不开线程和并发的知识。

1.4 并行和并发

如果有条高速公路 A 上面并排有 8 条车道,那么最大的并行车辆就是 8 辆,此条高速公路 A 同时并排行走的车辆小于等于 8 辆的时候,车辆就可以并行运行。CPU 也是这个原理,一个 CPU 相当于一个高速公路 A,核心数或者线程数就相当于并排可以通行的车道;而多个 CPU 就相当于并排有多条高速公路,而每个高速公路并排有多个车道。

当谈论并发的时候一定要加个单位时间,也就是说单位时间内并发量是多少。离开了单位时间其实是没有意义的。

总结如下

  • 并发:指应用能够交替执行不同的任务,比如单 CPU 核心下执行多线程并非是同时执行多个任务,如果开启两个线程执行,其实就是在以我们几乎毫无察觉到的速度不断去切换这两个任务,以达到”同时执行效果”。
  • 并行:指应用能够同时执行不同的任务,比如,吃饭的时候可以边吃饭边打电话看电视,这几件事都可以同时进行。
  • 两者区别:一个是交替执行,一个是同时执行。

二、Java内存模型(JMM)

2.1 happens-before 原则

JMM 定义了 happens-before 关系,是多线程编程的基石:

// 1. 程序次序规则:同一个线程内,按照代码顺序执行
int a = 1; // (1)
int b = 2; // (2) — happens-before (1) happens-before (2)

// 2. volatile 变量规则:对一个 volatile 域的写 happens-before 后续读
volatile boolean flag = false;
// Thread A: flag = true; (写)
// Thread B: if (flag) {} (读) — A 的写 happens-before B 的读

// 3. 锁规则:unlock happens-before 后续的 lock
synchronized (lock) {
// 修改变量
} // unlock happens-before 下一个 synchronized(lock) 的 lock

// 4. 线程启动规则:Thread.start() happens-before 被启动线程的任意操作
// 5. 线程终止规则:线程的任意操作 happens-before 其他线程的 join() 返回
// 6. 传递性:A happens-before B,B happens-before C → A happens-before C

2.2 volatile 的底层屏障

volatile 在 JVM 层面翻译为内存屏障指令,确保可见性和有序性:

// volatile 写
// 等价于:
// StoreStore 屏障:确保之前的所有写已完成
// 写 volatile 变量
// StoreLoad 屏障:确保之后的读/写不会重排序到此写之前

// volatile 读
// 等价于:
// LoadLoad 屏障 + LoadStore 屏障
// 读 volatile 变量

// 实际应用:DCL 单例模式
public class Singleton {
private static volatile Singleton instance; // volatile 必须!

public static Singleton getInstance() {
if (instance == null) { // 1. 第一次检查(无锁)
synchronized (Singleton.class) {
if (instance == null) { // 2. 第二次检查(有锁)
instance = new Singleton(); // 3. 创建实例
// 如果没有 volatile,步骤 3 可能被重排序:
// a. 分配内存
// b. 将引用赋给 instance(此时对象尚未构造完成!)
// c. 调用构造函数
// 步骤 b 和 c 的重排序导致其他线程在步骤 1 看到非 null 但未初始化的对象
}
}
}
return instance;
}
}

三、synchronized 的锁升级过程

HotSpot JVM 中的 synchronized 经过了多代优化,实现了锁的逐步升级:

无锁状态 (01)
↓ 第一个线程获取锁
偏向锁 (01, biased) — 在对象头记录偏向线程 ID
↓ 另一个线程竞争
轻量级锁 (00) — CAS 自旋获取(自适应自旋)
↓ 自旋超过阈值或竞争激烈
重量级锁 (10) — 操作系统 mutex,线程阻塞/唤醒
// 对象头的 Mark Word 变化(64位 JVM):
//
// 无锁: unused(25) | hashcode(31) | unused(1) | age(4) | biased_lock(1) | 01
// 偏向锁: thread(54) | epoch(2) | unused(1) | age(4) | biased_lock(1) | 01
// 轻量级锁: ptr_to_lock_record(62) | 00
// 重量级锁: ptr_to_monitor(62) | 10
// GC标记: 空(62) | 11

// 偏向锁的撤销成本很高——需要在全局安全点(SafePoint)暂停所有线程
// 因此在高竞争场景下,可以通过 -XX:-UseBiasedLocking 关闭偏向锁

3.1 synchronized 的底层实现

// 同步代码块:
// 编译为 monitorenter / monitorexit 字节码指令
synchronized (obj) {
// 临界区代码
}
// 字节码:
// aload_0 (加载 obj 引用)
// dup
// astore_1
// monitorenter (获取 obj 的 monitor)
// ... 临界区代码
// aload_1
// monitorexit (释放 obj 的 monitor)
// goto 正常返回
// astore_2 (异常处理)
// aload_1
// monitorexit (异常时也释放 monitor)

// 同步方法:
// 在方法的 access_flags 中设置 ACC_SYNCHRONIZED 标志
// JVM 根据此标志在方法调用时自动执行 monitorenter/monitorexit
public synchronized void foo() { }

四、高并发编程的意义

由于多核多线程的 CPU 的诞生,多线程、高并发的编程越来越重要,多线程可以给程序性能处理带来质的提升。

4.1 多线程优势

  • 充分利用 CPU 的资源:程序的基本调度单元是线程,并且一个线程也只能在一个 CPU 的一个核的一个线程上跑。如果你是个 i3 的 CPU 的话,最差也是双核心 4 线程的运算能力,如果是一个线程的程序的话,那是要浪费 3/4 的 CPU 性能。如果设计一个多线程的程序的话,那它就可以同时在多个 CPU 的多个核的多个线程上跑,可以充分地利用 CPU。

  • 加快响应用户的时间:比如我们经常用的迅雷下载,都喜欢多开几个线程去下载。在做程序开发的时候更应该如此,特别是做互联网项目,网页的响应时间若提升 1s,如果流量大的话,就能增加不少转换量。

  • 可以使代码模块化、异步化、简单化:例如我们在做 Android 程序开发的时候,主线程的 UI 展示部分是一块主代码程序部分,但是 UI 上的按钮响应事件的处理程序就可以做个单独的模块程序拿出来。这样既增加了异步的操作,又使程序模块化、清晰化和简单化。

4.2 多线程开发注意事项

  • 线程之间的安全性:在同一个进程里面的多线程是资源共享的,也就是都可以访问同一个内存地址当中的一个变量。若有多个线程同时执行写操作,一般都需要考虑线程同步,否则就可能影响线程安全。

  • 线程之间的死锁:为了解决线程之间的安全性引入了 Java 的锁机制,而一不小心就会产生 Java 线程死锁的多线程问题,因为不同的线程都在等待那些根本不可能被释放的锁,从而导致所有的工作都无法完成。

  • 过多线程导致耗尽服务器资源从而引起死机当机问题:线程数太多有可能造成系统创建大量线程而导致消耗完系统内存以及 CPU 的”过渡切换”,造成系统的死机。可以使用资源池来解决,如数据库连接池。

五、AQS(AbstractQueuedSynchronizer)深度剖析

AQS 是 Java 并发包的核心框架,ReentrantLock、Semaphore、CountDownLatch、ReentrantReadWriteLock 等都基于 AQS 构建。

// AQS 核心数据结构
// 一个 volatile int state(同步状态)
// 一个 CLH 变体的 FIFO 双向链表(等待队列)

// ReentrantLock 中 state 的含义:
// state = 0: 锁未被持有
// state = 1: 锁被持有(未重入)
// state = N: 锁被持有者重入了 N 次

// AQS 获取锁的简化流程:
// 1. tryAcquire(arg) — 尝试获取(子类实现)
// 2. 如果获取成功 → 返回
// 3. 如果获取失败 → 加入等待队列(CAS 入队)
// 4. 在队列中自旋或阻塞(park)直到前驱节点释放锁并唤醒(unpark)

// AQS 的独占模式 vs 共享模式:
// 独占 (Exclusive): ReentrantLock 的写锁——一次只有一个线程能获取
// 共享 (Shared): Semaphore, CountDownLatch, ReadLock——多个线程可同时获取

// Condition 的实现基于 AQS
// Condition.await() → 释放锁 → 进入 Condition 等待队列 → park
// Condition.signal() → 将等待队列的头节点移到同步队列 → unpark

5.1 ReentrantLock 的公平锁与非公平锁

// 非公平锁(默认):新来的线程可能直接抢到锁,跳过排队的线程
// tryAcquire 中直接 CAS 尝试获取(如果 state == 0)
class NonfairSync extends Sync {
final boolean initialTryLock() {
if (compareAndSetState(0, 1)) { // 直接抢!
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
// ...
}
}

// 公平锁:必须排队,先到先得
// tryAcquire 中先检查队列中是否有等待的前驱节点
class FairSync extends Sync {
final boolean initialTryLock() {
if (getState() == 0 && !hasQueuedPredecessors() // 先检查是否有排队!
&& compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
// ...
}
}

// 选择建议:
// 非公平锁:吞吐量高(减少上下文切换),但可能造成线程饥饿
// 公平锁:公平性好,但性能较低(频繁的上下文切换)
// 默认使用非公平锁,除非有严格的公平性要求

六、ThreadPoolExecutor 参数详解

// ThreadPoolExecutor 完整构造
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2, // corePoolSize: 核心线程数
5, // maximumPoolSize: 最大线程数
60, TimeUnit.SECONDS, // keepAliveTime: 空闲非核心线程存活时间
new LinkedBlockingQueue<>(100), // workQueue: 任务队列
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy() // 拒绝策略
);

// 任务执行流程:
// 1. 如果线程数 < corePoolSize → 创建新线程执行任务
// 2. 如果线程数 >= corePoolSize → 任务入队(workQueue)
// 3. 如果队列满 → 创建新线程(但不超过 maximumPoolSize)
// 4. 如果线程数 == maximumPoolSize 且队列满 → 执行拒绝策略

// 四种拒绝策略:
// AbortPolicy: 抛 RejectedExecutionException(默认)
// CallerRunsPolicy: 由调用者线程执行(降低提交速度)
// DiscardPolicy: 静默丢弃任务
// DiscardOldestPolicy:丢弃最旧的任务并重试

// 队列选择:
// SynchronousQueue: 没有容量,每个任务直接交给线程(适合 CPU 密集型)
// LinkedBlockingQueue: 无界队列(可能导致 OOM,慎用!)
// ArrayBlockingQueue: 有界队列(推荐,限制资源使用)
// PriorityBlockingQueue: 优先级队列

// 最佳实践:
// CPU 密集型: corePoolSize = CPU 核数 + 1
// IO 密集型: corePoolSize = CPU 核数 * 2 (或更高)
// 使用有界队列 + CallerRunsPolicy 提供自然的背压(back-pressure)

七、ForkJoinPool 和工作窃取

// ForkJoinPool 适用于递归分解的任务(分治算法)
// 核心特性:工作窃取(Work Stealing)
// 每个线程有自己的双端队列
// 线程从自己队列的尾部取任务(LIFO)
// 空闲线程从其他线程队列的头部"窃取"任务(FIFO)

// 示例:并行求和
class SumTask extends RecursiveTask<Long> {
private final int[] array;
private final int start, end;
private static final int THRESHOLD = 1000;

@Override
protected Long compute() {
if (end - start <= THRESHOLD) {
long sum = 0;
for (int i = start; i < end; i++) sum += array[i];
return sum;
}
int mid = (start + end) / 2;
SumTask left = new SumTask(array, start, mid);
SumTask right = new SumTask(array, mid, end);
left.fork(); // 异步执行左半
long rightResult = right.compute(); // 同步执行右半
long leftResult = left.join(); // 等待左半结果
return leftResult + rightResult;
}
}

// ForkJoinPool commonPool = ForkJoinPool.commonPool();
// = Runtime.getRuntime().availableProcessors() - 1 个线程(默认)
// 用于 CompletableFuture、parallelStream 等

八、CompletableFuture:异步编程利器

// CompletableFuture 链式异步编程
CompletableFuture.supplyAsync(() -> fetchUserFromDb(userId))
.thenApplyAsync(user -> enrichWithApi(user)) // 转换
.thenAcceptAsync(enriched -> updateCache(enriched)) // 消费
.exceptionally(throwable -> { // 异常处理
log.error("Failed", throwable);
return null;
});

// 组合多个异步任务
CompletableFuture<String> userFuture =
CompletableFuture.supplyAsync(() -> fetchUser(userId));
CompletableFuture<Integer> scoreFuture =
CompletableFuture.supplyAsync(() -> fetchScore(userId));

// 等待两个都完成
userFuture.thenCombine(scoreFuture, (user, score) ->
new UserProfile(user, score));

// 任一完成
CompletableFuture.anyOf(userFuture, scoreFuture)
.thenAccept(result -> log.info("First result: {}", result));

// 全部完成
CompletableFuture.allOf(userFuture, scoreFuture)
.thenRun(() -> log.info("All done"));

九、Android 特有的线程机制

9.1 Looper / Handler / MessageQueue

// Android 消息机制的核心是事件驱动模型
// Looper: 每个线程的消息循环(主线程的 Looper.prepareMainLooper())
// MessageQueue: 消息队列(native 层基于 epoll 实现)
// Handler: 发送和处理消息的接口

// Handler 发送消息的流程:
// Handler.sendMessage(msg)
// → MessageQueue.enqueueMessage(msg, when)
// → (可能唤醒 Looper)
// Looper.loop()
// → MessageQueue.next() (可能阻塞,等待有消息或定时器到期)
// → msg.target.dispatchMessage(msg)
// → Handler.handleMessage(msg)

// 源码关键路径:
// Looper.loop → MessageQueue.next → nativePollOnce → epoll_wait
// nativePollOnce 是可中断的,当 enqueueMessage 时通过 eventfd 唤醒

// Handler 内存泄漏的原因:
// Handler 作为非静态内部类持有外部 Activity 的隐式引用
// MessageQueue 中的 Message 持有 Handler 引用
// Message 延迟发送时,引用链导致 Activity 无法被 GC
// 解决:使用静态内部类 + WeakReference<Activity>

9.2 HandlerThread

// HandlerThread 是一个自带 Looper 的后台线程
HandlerThread handlerThread = new HandlerThread("MyWorkerThread");
handlerThread.start();

Handler workerHandler = new Handler(handlerThread.getLooper()) {
@Override
public void handleMessage(Message msg) {
// 在后台线程中执行
}
};

workerHandler.post(() -> {
// 在后台线程中执行
});

// 使用完毕后必须退出
handlerThread.quitSafely(); // 安全退出(处理完队列中所有消息)

十、认识Java线程

10.1 Java里的程序天生就是多线程的

一个 Java 程序从 main() 方法开始执行,然后按照既定的代码逻辑执行,看似没有其他线程参与,但实际上,Java 程序天生就是多线程程序,因为执行 Main() 方法的是一个名称为 main 的线程。同时还有以下线程在运行:

  1. main – main 线程,用户程序入口
  2. Reference Handler – 清除 Reference 线程
  3. Finalizer – 调用对象的 finalizer 方法的线程
  4. Signal Dispatcher – 分发处理发送给 JVM 信号的线程
  5. Attach Listener – 内存 dump,线程 dump,类信息统计,获取系统属性等
  6. Monitor Ctrl-Break – 监控 Ctrl-Break 中断信号

10.2 线程的启动与中止

启动线程的方式有(但其实真正意义上只有前面两种):

  1. X extends Thread,然后 X.run
  2. X implements Runnable;然后交给 Thread 运行
  3. X implements Callable;然后交给 FutureTask 运行

Callable、Future 和 FutureTask

  • Runnable 是一个接口,run() 方法返回值为 void 类型,所以在执行完任务之后无法返回任何结果。
  • Callable 是泛型接口,call() 方法返回 V 类型。
  • Future 对于具体的 Runnable 或 Callable 任务的执行结果进行取消、查询是否完成、获取结果。必要时可以通过 get 方法获取执行结果,该方法会阻塞直到任务返回结果。
  • FutureTask 实现了 RunnableFuture 接口,RunnableFuture 继承了 Runnable 和 Future 接口,所以它既可以作为 Runnable 被线程执行,又可以作为 Future 得到 Callable 的返回值。

手动中止
暂停、恢复和停止操作对应在线程 Thread 的 API 就是 suspend()、resume() 和 stop()。但是这些 API 是过期的,不建议使用的。以 suspend() 方法为例,在调用后,线程不会释放已经占有的资源(比如锁),而是占有着资源进入睡眠状态,这样非常容易引发死锁。

安全的中止则是其他线程通过调用某个线程的 interrupt() 方法对其进行中断操作,中断好比其他线程对该线程打了个招呼,”你要中断了”,但这并不代表线程会立即停止自己的工作,因为 Java 里的线程是协作式的,不是抢占式的。线程通过检查自身中断标志位是否被置为 true 来进行响应。

如果一个线程处于阻塞状态(通过调用 thread.sleep、thread.join、thread.wait …),则在线程检查到自己的中断标识位为 true 时,会在这些阻塞方法调用处抛 InterruptedException 异常,并且在抛出异常后会立即将线程的中断标识位清除。

10.3 线程的状态

Java 中线程的状态分为 6 种:

  1. **初始(NEW)**:新创建一个线程对象,但还没有调用 start() 方法。
  2. **运行(RUNNABLE)**:Java 线程中将就绪(ready)和运行中(running)两种状态统称为”运行”。线程对象创建后,其他线程调用了该对象的 start() 方法,该状态的线程位于可运行线程池中,等待被线程调度选中。
  3. **阻塞(BLOCKED)**:表示线程阻塞于锁(等待获取 monitor 锁)。
  4. **等待(WAITING)**:进入该状态的线程需要等待其他线程做出一些动作(通知或中断),如 Object.wait()、Thread.join()、LockSupport.park()。
  5. **超时等待(TIMED_WAITING)**:该状态不同于 WAITING,它可以在指定的时间后自行返回线程队列,如 Thread.sleep()、Object.wait(timeout)、LockSupport.parkNanos()。
  6. **终止(TERMINATED)**:表示该线程已经执行完毕。

十一、死锁

11.1 死锁概念

死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成一种阻塞线程,若无外力作用,它们都将无法推进下去,此时称系统处于死锁状态或者系统产生了死锁。

死锁的发生必须具备以下四个必要条件:

  • 互斥条件:指进程对所分配到的资源进行排他性使用,即在某一段时间内某资源只由一个进程占用。
  • 请求和保持条件:指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其他进程占有。
  • 不剥夺条件:指进程已获得资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。
  • 环路等待条件:指在发生死锁时,必然存在一个进程 - 资源的环形链。

11.2 死锁的预防与解决

打破四个必要条件之一,就可以有效预防死锁:

  • 打破互斥条件:改造独占性资源为虚拟资源
  • 打破不可抢占条件:当无法满足时退出原占有的资源
  • 打破占有且申请条件:采用资源预先分配策略
  • 打破循环等待条件:实现资源有序分配策略

避免死锁的常见算法:有序资源分配法银行家算法

解决方法

  1. 内部通过顺序比较,确定拿锁的顺序(所有线程按固定顺序获取锁)
  2. 采用尝试拿锁的机制(tryLock(timeout)

11.3 死锁的危害

  1. 线程不工作了,但是整个程序还是活着的
  2. 没有任何的异常信息可供检查
  3. 一旦程序发生了死锁,是没有任何办法恢复的,除非重启应用程序

十二、面试常问题目

Q1: 什么是 happens-before 原则?volatile 是如何保证可见性和有序性的?

happens-before 是 JMM 定义的偏序关系,用于判断数据是否存在竞争。如果操作 A happens-before 操作 B,那么 A 的结果对 B 可见,且 A 的执行顺序在 B 之前。volatile 通过内存屏障保证:(1) 可见性——对 volatile 变量的修改会被立即刷新到主内存,并且使其他 CPU 缓存行失效;(2) 有序性——volatile 写之前和之后的指令不会被重排序。volatile 不保证原子性(如 i++ 仍然是线程不安全的)。适用于状态标志、DCL 单例、独立观察等场景。

Q2: synchronized 的锁升级过程是怎样的?为什么需要这么设计?

锁升级过程:无锁 → 偏向锁 → 轻量级锁 → 重量级锁。偏向锁将对象头标记为偏向特定线程,该线程再次进入同步块时无需 CAS 操作——适用于大多数时间只有一个线程访问的场景。轻量级锁通过 CAS 自旋获取,避免操作系统层面的阻塞——适用于线程交替执行的场景。重量级锁使用 OS mutex,线程阻塞/唤醒——适用于竞争激烈的场景。这种逐步升级的设计是基于”大多数情况下锁的竞争不激烈”的经验假设,避免了不必要的重量级操作。

Q3: ThreadPoolExecutor 的执行流程是什么?核心线程数和最大线程数有什么区别?

执行流程:(1) 线程数 < corePoolSize → 创建新线程;(2) 线程数 >= corePoolSize → 任务入队;(3) 队列满 → 创建新线程(不超过 maximumPoolSize);(4) 达到 maximumPoolSize → 执行拒绝策略。核心线程默认不会回收(除非 allowCoreThreadTimeOut=true),空闲的非核心线程在 keepAliveTime 后回收。核心线程数应该根据 CPU 核数和任务类型设定:CPU 密集型约为核心数+1,IO 密集型约为核心数*2。

Q4: Handler 的内存泄漏是怎么产生的?如何避免?

Handler 作为 Activity 的非静态内部类(或匿名内部类),隐式持有外部 Activity 的引用。当发送延迟消息(如 postDelayed 10 分钟),消息在 MessageQueue 中持有 Handler 引用,形成引用链:MessageQueue → Message → Handler → Activity。在消息处理前 Activity 被销毁,由于这条引用链,GC 无法回收 Activity。解决方案:(1) 使用静态内部类 + WeakReference;(2) 在 onDestroy 中调用 handler.removeCallbacksAndMessages(null) 移除所有消息;(3) 使用 Lifecycle 感知组件(如 LiveData)。

Q5: CompletableFuture 和传统的 Future 相比有什么优势?

Future 只能通过 get() 阻塞等待结果或 isDone() 轮询,不支持回调、链式处理和组合。CompletableFuture 实现了 Future 和 CompletionStage 接口,支持:(1) 链式处理:thenApply / thenAccept / thenRun 等;(2) 组合操作:thenCombine / thenCompose / allOf / anyOf;(3) 异常处理:exceptionally / handle;(4) 异步回调:thenApplyAsync 等(使用 ForkJoinPool.commonPool());(5) 手动完成:complete / completeExceptionally。这使得异步代码可以以声明式方式表达,避免了回调地狱。


参考源码路径:

  • Android Looper:frameworks/base/core/java/android/os/Looper.java
  • Android Handler:frameworks/base/core/java/android/os/Handler.java
  • AQS 源码:java.util.concurrent.locks.AbstractQueuedSynchronizer
  • ThreadPoolExecutor 源码:java.util.concurrent.ThreadPoolExecutor
  • ART Monitor:art/runtime/monitor.cc
打赏
  • 微信
  • 支付宝

评论