Java 并发 — 面试题篇
更新: 5/15/2026 字数: 0 字 时长: 0 分钟
Q1: synchronized 和 ReentrantLock 的区别?
考察点:锁机制对比
完整回答:
| 维度 | synchronized | ReentrantLock |
|---|---|---|
| 实现层面 | JVM 内置,monitorenter/monitorexit 字节码 | Java API,基于 AQS |
| 锁释放 | 自动释放(退出同步块/异常) | 必须手动 unlock(),通常在 finally 中 |
| 可中断 | 不可中断,线程会一直等待 | lockInterruptibly() 支持中断等待 |
| 公平性 | 只有非公平 | 构造时可选公平/非公平 |
| 条件变量 | 只有一个等待队列(wait/notify) | 可创建多个 Condition |
| 锁升级 | 偏向锁→轻量级锁→重量级锁 | 无锁升级机制 |
| 性能 | JDK 6 后大幅优化,差距不大 | 差距不大 |
选择建议:简单同步用 synchronized(代码简洁、不会忘记释放锁);需要可中断、公平锁、多条件变量等高级功能时用 ReentrantLock。
追问:synchronized 的锁升级过程?
- 无锁 → 偏向锁:第一个线程进入,在 Mark Word 中记录线程 ID,后续该线程进入只需比较 ID
- 偏向锁 → 轻量级锁:第二个线程尝试获取锁,撤销偏向,两个线程 CAS 竞争
- 轻量级锁 → 重量级锁:CAS 自旋一定次数仍失败,膨胀为重量级锁,未获取锁的线程阻塞
锁升级是单向的,不可降级。
追问:什么是锁消除和锁粗化?
锁消除:JIT 编译器通过逃逸分析发现锁对象不会逃逸到其他线程,自动去除锁。比如方法内的 StringBuffer。
锁粗化:连续对同一对象多次加锁解锁,合并为一次大范围的锁。
Q2: volatile 的原理?能保证线程安全吗?
考察点:JMM、volatile 语义
完整回答:
volatile 提供两个保证:
可见性:volatile 变量的写会立即刷新到主内存,读会从主内存加载。其他线程能立即看到最新值。
有序性:通过内存屏障禁止指令重排。volatile 写前插入 StoreStore 屏障,写后插入 StoreLoad 屏障;volatile 读后插入 LoadLoad 和 LoadStore 屏障。
但 volatile 不保证原子性。i++ 是读-改-写三步操作,两个线程可能同时读到相同值,各自加 1 后写回,丢失一次自增。
所以 volatile 不能完全保证线程安全,只适用于:
- 状态标志位(boolean flag)
- DCL 单例中防止指令重排
- 一写多读的场景
需要原子性时用 AtomicInteger(CAS)或 synchronized。
追问:DCL 单例为什么需要 volatile?
new Singleton() 分三步:1.分配内存 2.初始化 3.赋值引用。没有 volatile,步骤 2 和 3 可能重排序,另一个线程看到非 null 的引用但对象还没初始化完成。volatile 禁止这个重排序。
Q3: 线程池的核心参数?execute 的执行流程?
考察点:ThreadPoolExecutor 原理
完整回答:
7 个核心参数:
corePoolSize:核心线程数,即使空闲也不回收maximumPoolSize:最大线程数keepAliveTime+unit:非核心线程空闲存活时间workQueue:任务队列(LinkedBlockingQueue/ArrayBlockingQueue/SynchronousQueue)threadFactory:线程工厂,可自定义线程名handler:拒绝策略
execute 流程:
- 当前线程数 < corePoolSize → 创建核心线程执行任务
- 核心线程满 → 任务放入 workQueue
- 队列满且线程数 < maximumPoolSize → 创建非核心线程
- 队列满且线程数 = maximumPoolSize → 执行拒绝策略
四种拒绝策略:
- AbortPolicy(默认):抛 RejectedExecutionException
- CallerRunsPolicy:调用者线程执行任务(降级)
- DiscardPolicy:静默丢弃
- DiscardOldestPolicy:丢弃队列头部最老的任务
追问:常见线程池的问题?
Executors.newFixedThreadPool:LinkedBlockingQueue 无界队列,任务堆积可能 OOMExecutors.newCachedThreadPool:maximumPoolSize = Integer.MAX_VALUE,可能创建大量线程- 阿里规范建议手动创建 ThreadPoolExecutor,明确指定参数
追问:核心线程能回收吗?
可以,调用 allowCoreThreadTimeOut(true) 后,核心线程空闲超过 keepAliveTime 也会被回收。
加分点:提到 Worker 继承了 AQS 实现不可重入锁,用于判断线程是否空闲(shutdown 时只中断空闲线程)。
Q4: ThreadLocal 的原理?为什么会内存泄漏?
考察点:ThreadLocal 实现机制
完整回答:
每个 Thread 内部持有一个 ThreadLocalMap,key 是 ThreadLocal 对象(弱引用),value 是存储的值。
Thread
└── ThreadLocalMap
├── Entry(WeakReference<ThreadLocal>, value1)
├── Entry(WeakReference<ThreadLocal>, value2)
└── ...get() 流程:获取当前线程的 ThreadLocalMap → 以 this(ThreadLocal 对象)为 key 查找 Entry → 返回 value。
内存泄漏原因:Entry 的 key 是弱引用,当 ThreadLocal 对象没有外部强引用时,GC 会回收 key,但 value 是强引用仍然存在。如果线程是线程池中的长期存活线程,这些 value 就永远不会被回收。
解决方案:使用完毕后调用 threadLocal.remove()。ThreadLocalMap 在 get/set 时也会清理 key 为 null 的 Entry(expungeStaleEntry),但不能完全依赖这个机制。
追问:InheritableThreadLocal 是什么?
普通 ThreadLocal 父子线程不共享数据。InheritableThreadLocal 在创建子线程时会将父线程的值拷贝到子线程的 inheritableThreadLocals 中。但注意线程池中线程是复用的,不会每次都拷贝,需要用阿里的 TransmittableThreadLocal 解决。
Q5: AQS 的原理?ReentrantLock 怎么实现的?
考察点:AQS 框架
完整回答:
AQS(AbstractQueuedSynchronizer)是 Java 并发包的基础框架,核心是:
volatile int state:同步状态(ReentrantLock 中 0=未锁,>0=重入次数)- CLH 双向队列:存放等待获取锁的线程节点
加锁流程(acquire):
tryAcquire:CAS 尝试将 state 从 0 改为 1,成功则获取锁- 如果当前线程已持有锁(重入),state++
- 失败则
addWaiter创建节点加入 CLH 队列尾部 acquireQueued:在队列中自旋,如果前驱是 head 则再次 tryAcquire- 否则
LockSupport.park()阻塞
解锁流程(release):
tryRelease:state--,state == 0 时真正释放LockSupport.unpark()唤醒队列中下一个等待线程
公平 vs 非公平:
- 非公平(默认):新线程直接 CAS 抢锁,不管队列
- 公平:tryAcquire 先检查
hasQueuedPredecessors(),队列中有等待线程则不抢
追问:基于 AQS 的其他实现?
- Semaphore:state 表示许可数量
- CountDownLatch:state 表示计数,countDown 减 1,await 等待 state 变为 0
- ReentrantReadWriteLock:state 高 16 位读锁计数,低 16 位写锁计数
Q6: CAS 是什么?ABA 问题怎么解决?
考察点:无锁编程
完整回答:
CAS(Compare And Swap)是一种无锁原子操作:比较内存值与预期值,相等则更新为新值,否则不操作。底层依赖 CPU 的 cmpxchg 指令。
Java 中通过 Unsafe.compareAndSwapInt 实现,AtomicInteger 等原子类都基于 CAS。
ABA 问题:线程1 读到值 A,线程2 将 A 改为 B 再改回 A,线程1 CAS 检查发现还是 A 就成功了,但实际上值已经被修改过。
解决方案:
AtomicStampedReference:加版本号(stamp),每次修改版本号+1,CAS 同时比较值和版本号AtomicMarkableReference:加布尔标记
大多数业务场景 ABA 问题不会造成实际影响,只有在涉及链表等数据结构的无锁算法中才需要关注。
加分点:CAS 的缺点——自旋开销(竞争激烈时 CPU 空转)、只能保证单个变量的原子性。LongAdder 通过分散热点(Cell 数组)减少 CAS 竞争,比 AtomicLong 在高并发下性能更好。
Q7: CountDownLatch 和 CyclicBarrier 的区别?
考察点:并发工具类
完整回答:
| 维度 | CountDownLatch | CyclicBarrier |
|---|---|---|
| 等待方式 | 一个线程等待其他 N 个线程 | N 个线程互相等待 |
| 可重用 | 一次性,计数到 0 不可重置 | 可重用(reset) |
| 底层 | AQS 的共享模式 | ReentrantLock + Condition |
| 回调 | 无 | 到达屏障时可执行 barrierAction |
使用场景:
- CountDownLatch:主线程等待多个子任务完成(如启动优化中等待多个 SDK 初始化完成)
- CyclicBarrier:多个线程分阶段执行,每阶段结束时同步(如并行计算后合并结果)
追问:Semaphore 呢?
Semaphore 控制并发访问数量。acquire() 获取许可(-1),release() 释放许可(+1)。许可为 0 时阻塞。适合限流场景(如数据库连接池最多 10 个连接)。
Q8: wait/notify 和 Condition 的区别?wait 为什么必须在 synchronized 中?
考察点:线程通信
完整回答:
wait/notify 是 Object 的方法,配合 synchronized 使用。Condition 是 Lock 的条件变量,配合 ReentrantLock 使用。
| 维度 | wait/notify | Condition |
|---|---|---|
| 配合 | synchronized | ReentrantLock |
| 条件队列 | 只有一个 | 可以有多个(精确唤醒) |
| 唤醒 | notify 随机唤醒一个 | signal 唤醒指定条件的线程 |
wait 必须在 synchronized 中的原因:wait 会释放锁并进入等待队列。如果不在 synchronized 中,没有锁可以释放,会抛 IllegalMonitorStateException。更重要的是,如果不加锁,check-then-wait 之间可能被其他线程插入 notify,导致信号丢失(lost wake-up)。
// 经典的生产者-消费者
synchronized (lock) {
while (queue.isEmpty()) { // 用 while 不用 if,防止虚假唤醒
lock.wait();
}
item = queue.poll();
}Q9: 死锁的条件?怎么排查和避免?
考察点:并发问题排查
完整回答:
死锁四个必要条件:
- 互斥:资源同一时刻只能被一个线程持有
- 持有并等待:持有一个资源的同时等待另一个资源
- 不可剥夺:已获取的资源不能被强制释放
- 循环等待:线程 A 等 B 的锁,B 等 A 的锁
排查方法:
jstack <pid>:打印线程堆栈,查找BLOCKED状态和waiting to lock信息- Android Studio Profiler → CPU → Thread dump
- ANR traces.txt 中查找死锁信息
避免方法:
- 固定加锁顺序(所有线程按相同顺序获取锁)
- 使用
tryLock(timeout)替代lock(),超时则放弃 - 减少锁的粒度和持有时间
- 使用无锁数据结构(ConcurrentHashMap、AtomicInteger)
Q10: Java 线程有哪些状态?状态之间怎么转换?
考察点:线程生命周期
完整回答:
6 种状态(Thread.State 枚举):
NEW → RUNNABLE → BLOCKED / WAITING / TIMED_WAITING → TERMINATED- NEW:创建但未 start
- RUNNABLE:调用 start 后,包括就绪和运行中(Java 不区分)
- BLOCKED:等待获取 synchronized 锁
- WAITING:调用 wait()、join()、LockSupport.park(),无限等待
- TIMED_WAITING:调用 sleep(ms)、wait(ms)、join(ms),有超时的等待
- TERMINATED:执行完毕或异常退出
追问:sleep 和 wait 的区别?
| 维度 | sleep | wait |
|---|---|---|
| 所属 | Thread 静态方法 | Object 实例方法 |
| 锁 | 不释放锁 | 释放锁 |
| 唤醒 | 超时自动唤醒 | 需要 notify/notifyAll |
| 使用条件 | 任何地方 | 必须在 synchronized 中 |
实习面试补充:线程与主线程通信高频题
实习面试通常先确认你是否理解“主线程负责 UI,耗时任务放子线程”,再逐步追问 Handler、线程安全和协程。
Q11: Android 为什么不能在子线程直接更新 UI?
考察点:主线程模型、线程安全
完整回答:
Android 的 View 体系不是线程安全的。UI 的测量、布局、绘制和事件分发都在主线程中按消息队列顺序执行,如果多个线程同时修改 View 状态,会产生竞态条件,导致显示错乱甚至崩溃。
因此耗时任务应该放到子线程执行,拿到结果后切回主线程更新 UI。常见方式:
Activity.runOnUiThreadView.postHandler(Looper.getMainLooper()).post- Kotlin 协程中的
withContext(Dispatchers.Main)
追问:子线程一定不能创建 Handler 吗?
可以,但这个线程必须先调用 Looper.prepare() 创建 Looper,再调用 Looper.loop() 开启消息循环。普通子线程默认没有 Looper。
Q12: 线程和进程有什么区别?
考察点:操作系统基础
完整回答:
- 进程是系统分配资源的基本单位,有独立的内存空间。
- 线程是 CPU 调度的基本单位,同一个进程内的线程共享进程资源。
Android 中每个 App 通常运行在独立进程中,主线程也叫 UI 线程。网络请求、数据库读写、图片解码等耗时任务如果放在主线程,可能造成卡顿或 ANR。
加分点:多个线程共享内存,所以要注意线程安全;多个进程之间内存隔离,通信通常需要 IPC,比如 Binder。
Q13: 什么情况下会出现线程安全问题?
考察点:共享变量、并发修改
完整回答:
线程安全问题通常出现在多个线程同时读写同一份可变数据时。例如多个线程同时执行 count++,它不是原子操作,实际包含读取、加一、写回三个步骤,可能导致结果丢失。
解决方式:
- 使用
synchronized或Lock保护临界区。 - 使用原子类,比如
AtomicInteger。 - 尽量使用不可变对象,减少共享可变状态。
- 在 Android 中把 UI 状态收敛到主线程更新。
追问:volatile 能保证 count++ 线程安全吗?
不能。volatile 主要保证可见性和禁止部分重排序,不保证复合操作的原子性。
