Skip to content

Java 并发 — 面试题篇

更新: 5/15/2026 字数: 0 字 时长: 0 分钟


Q1: synchronized 和 ReentrantLock 的区别?

考察点:锁机制对比

完整回答

维度synchronizedReentrantLock
实现层面JVM 内置,monitorenter/monitorexit 字节码Java API,基于 AQS
锁释放自动释放(退出同步块/异常)必须手动 unlock(),通常在 finally 中
可中断不可中断,线程会一直等待lockInterruptibly() 支持中断等待
公平性只有非公平构造时可选公平/非公平
条件变量只有一个等待队列(wait/notify)可创建多个 Condition
锁升级偏向锁→轻量级锁→重量级锁无锁升级机制
性能JDK 6 后大幅优化,差距不大差距不大

选择建议:简单同步用 synchronized(代码简洁、不会忘记释放锁);需要可中断、公平锁、多条件变量等高级功能时用 ReentrantLock。

追问:synchronized 的锁升级过程?

  1. 无锁 → 偏向锁:第一个线程进入,在 Mark Word 中记录线程 ID,后续该线程进入只需比较 ID
  2. 偏向锁 → 轻量级锁:第二个线程尝试获取锁,撤销偏向,两个线程 CAS 竞争
  3. 轻量级锁 → 重量级锁:CAS 自旋一定次数仍失败,膨胀为重量级锁,未获取锁的线程阻塞

锁升级是单向的,不可降级。

追问:什么是锁消除和锁粗化?

锁消除:JIT 编译器通过逃逸分析发现锁对象不会逃逸到其他线程,自动去除锁。比如方法内的 StringBuffer。

锁粗化:连续对同一对象多次加锁解锁,合并为一次大范围的锁。


Q2: volatile 的原理?能保证线程安全吗?

考察点:JMM、volatile 语义

完整回答

volatile 提供两个保证:

  1. 可见性:volatile 变量的写会立即刷新到主内存,读会从主内存加载。其他线程能立即看到最新值。

  2. 有序性:通过内存屏障禁止指令重排。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 流程:

  1. 当前线程数 < corePoolSize → 创建核心线程执行任务
  2. 核心线程满 → 任务放入 workQueue
  3. 队列满且线程数 < maximumPoolSize → 创建非核心线程
  4. 队列满且线程数 = maximumPoolSize → 执行拒绝策略

四种拒绝策略:

  • AbortPolicy(默认):抛 RejectedExecutionException
  • CallerRunsPolicy:调用者线程执行任务(降级)
  • DiscardPolicy:静默丢弃
  • DiscardOldestPolicy:丢弃队列头部最老的任务

追问:常见线程池的问题?

  • Executors.newFixedThreadPool:LinkedBlockingQueue 无界队列,任务堆积可能 OOM
  • Executors.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)

  1. tryAcquire:CAS 尝试将 state 从 0 改为 1,成功则获取锁
  2. 如果当前线程已持有锁(重入),state++
  3. 失败则 addWaiter 创建节点加入 CLH 队列尾部
  4. acquireQueued:在队列中自旋,如果前驱是 head 则再次 tryAcquire
  5. 否则 LockSupport.park() 阻塞

解锁流程(release)

  1. tryRelease:state--,state == 0 时真正释放
  2. 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 的区别?

考察点:并发工具类

完整回答

维度CountDownLatchCyclicBarrier
等待方式一个线程等待其他 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/notifyCondition
配合synchronizedReentrantLock
条件队列只有一个可以有多个(精确唤醒)
唤醒notify 随机唤醒一个signal 唤醒指定条件的线程

wait 必须在 synchronized 中的原因:wait 会释放锁并进入等待队列。如果不在 synchronized 中,没有锁可以释放,会抛 IllegalMonitorStateException。更重要的是,如果不加锁,check-then-wait 之间可能被其他线程插入 notify,导致信号丢失(lost wake-up)。

java
// 经典的生产者-消费者
synchronized (lock) {
    while (queue.isEmpty()) { // 用 while 不用 if,防止虚假唤醒
        lock.wait();
    }
    item = queue.poll();
}

Q9: 死锁的条件?怎么排查和避免?

考察点:并发问题排查

完整回答

死锁四个必要条件:

  1. 互斥:资源同一时刻只能被一个线程持有
  2. 持有并等待:持有一个资源的同时等待另一个资源
  3. 不可剥夺:已获取的资源不能被强制释放
  4. 循环等待:线程 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 的区别?

维度sleepwait
所属Thread 静态方法Object 实例方法
不释放锁释放锁
唤醒超时自动唤醒需要 notify/notifyAll
使用条件任何地方必须在 synchronized 中

实习面试补充:线程与主线程通信高频题

实习面试通常先确认你是否理解“主线程负责 UI,耗时任务放子线程”,再逐步追问 Handler、线程安全和协程。

Q11: Android 为什么不能在子线程直接更新 UI?

考察点:主线程模型、线程安全

完整回答

Android 的 View 体系不是线程安全的。UI 的测量、布局、绘制和事件分发都在主线程中按消息队列顺序执行,如果多个线程同时修改 View 状态,会产生竞态条件,导致显示错乱甚至崩溃。

因此耗时任务应该放到子线程执行,拿到结果后切回主线程更新 UI。常见方式:

  • Activity.runOnUiThread
  • View.post
  • Handler(Looper.getMainLooper()).post
  • Kotlin 协程中的 withContext(Dispatchers.Main)

追问:子线程一定不能创建 Handler 吗?

可以,但这个线程必须先调用 Looper.prepare() 创建 Looper,再调用 Looper.loop() 开启消息循环。普通子线程默认没有 Looper。


Q12: 线程和进程有什么区别?

考察点:操作系统基础

完整回答

  • 进程是系统分配资源的基本单位,有独立的内存空间。
  • 线程是 CPU 调度的基本单位,同一个进程内的线程共享进程资源。

Android 中每个 App 通常运行在独立进程中,主线程也叫 UI 线程。网络请求、数据库读写、图片解码等耗时任务如果放在主线程,可能造成卡顿或 ANR。

加分点:多个线程共享内存,所以要注意线程安全;多个进程之间内存隔离,通信通常需要 IPC,比如 Binder。


Q13: 什么情况下会出现线程安全问题?

考察点:共享变量、并发修改

完整回答

线程安全问题通常出现在多个线程同时读写同一份可变数据时。例如多个线程同时执行 count++,它不是原子操作,实际包含读取、加一、写回三个步骤,可能导致结果丢失。

解决方式:

  • 使用 synchronizedLock 保护临界区。
  • 使用原子类,比如 AtomicInteger
  • 尽量使用不可变对象,减少共享可变状态。
  • 在 Android 中把 UI 状态收敛到主线程更新。

追问:volatile 能保证 count++ 线程安全吗?

不能。volatile 主要保证可见性和禁止部分重排序,不保证复合操作的原子性。