Administrator 25 阅读 面试

Java 并发高频面试题(含口述版回答)


1. 创建线程的方式有哪些?

口述版:

主要有四种:

  1. 继承 Thread 类,重写 run() 方法。
  2. 实现 Runnable 接口,把任务传给 Thread。
  3. 实现 Callable 接口,配合 FutureTask,可以拿到返回值,也能抛异常。
  4. 使用线程池(最推荐),通过 ExecutorService 提交任务。

实际项目中几乎都用线程池,而不是自己 new Thread。自己创建线程成本高、不可控,容易造成资源浪费。


2. 线程有哪些状态?生命周期是怎样的?

口述版:

Java 线程有 6 种状态(Thread.State):

状态 说明
NEW 新建,还没调用 start()
RUNNABLE 可运行(包括正在运行和就绪)
BLOCKED 阻塞,等待获取 synchronized 锁
WAITING 无限等待(wait、join、park)
TIMED_WAITING 有时限的等待(sleep、wait(timeout))
TERMINATED 终止,线程执行完毕

注意:Java 里没有把「运行中」和「就绪」严格区分,都叫 RUNNABLE。阻塞主要指等锁(BLOCKED)和主动等待(WAITING)。


3. synchronized 和 ReentrantLock 有什么区别?

口述版:

两者都能实现可重入的互斥锁,但有明显差异:

对比点 synchronized ReentrantLock
实现方式 JVM 内置关键字 JDK 实现的类(基于 AQS)
释放锁 自动释放 必须手动 unlock(建议 try-finally)
可中断 不可中断 支持 lockInterruptibly
公平锁 非公平 可选择公平/非公平
条件队列 一个(wait/notify) 多个 Condition
性能 JDK6 后优化很好 功能更丰富,性能接近

使用建议:

  • 简单同步用 synchronized 就够了,代码更简洁安全。
  • 需要尝试加锁、可中断、公平锁、多个条件队列时,用 ReentrantLock。

4. volatile 关键字的作用是什么?

口述版:

volatile 主要有两个作用:

  1. 保证可见性:一个线程修改了 volatile 变量,其他线程能立刻看到最新值(通过内存屏障强制刷新工作内存)。
  2. 禁止指令重排序:通过内存屏障防止编译器和 CPU 重排指令,保证有序性。

它不保证原子性。比如 i++ 即使加了 volatile,多线程下仍然不安全,因为 i++ 是读-改-写三步操作。

典型使用场景:

  • 状态标志位(比如 volatile boolean flag)
  • 双重检查锁定的单例(DCL)中的实例变量
  • 读写锁中的可见性保证

5. 讲讲 Java 内存模型(JMM)和 happens-before

口述版:

JMM(Java Memory Model)定义了线程如何与主内存交互,解决可见性、有序性、原子性问题。

核心概念:

  • 主内存:所有线程共享
  • 工作内存:每个线程私有,保存变量副本

happens-before 是判断两个操作是否存在可见性顺序的规则。常见的有:

  1. 程序顺序规则:同一线程内前面的操作 happens-before 后面的操作
  2. 监视器锁规则:unlock 操作 happens-before 后续的 lock
  3. volatile 规则:对 volatile 的写 happens-before 后续的读
  4. 传递性:A hb B 且 B hb C,则 A hb C
  5. 线程启动、终止、中断等规则

理解 happens-before 比死记内存屏障更重要,面试时能讲清楚可见性是怎么保证的。


6. 什么是 CAS?有什么问题?

口述版:

CAS(Compare-And-Swap)是一种乐观锁实现,包含三个操作数:内存值 V、期望值 A、要更新的值 B。只有当 V == A 时,才把 V 更新为 B,否则失败重试。

Java 中通过 Unsafe 类和原子类(AtomicInteger 等)使用 CAS。

优点:无锁,并发性能好。

三大问题:

  1. ABA 问题:值从 A 变成 B 再变回 A,CAS 以为没变。解决:加版本号(AtomicStampedReference)。
  2. 自旋开销大:长时间竞争会导致 CPU 空转。
  3. 只能保证一个变量的原子性:多个变量要用 AtomicReference 或加锁。

7. ThreadLocal 是什么?原理和内存泄漏问题?

口述版:

ThreadLocal 让每个线程都有自己独立的变量副本,互不干扰,常用于保存用户上下文、连接、Session 等。

原理: 每个 Thread 对象内部有一个 ThreadLocalMap,key 是 ThreadLocal 对象的弱引用,value 是具体值。

内存泄漏问题:

  • key 是弱引用,value 是强引用。
  • 如果 ThreadLocal 对象被回收,key 变 null,但 value 还在,造成泄漏。
  • 线程池场景更严重,线程被复用,value 一直不释放。

正确用法:用完一定要调用 remove(),尤其在线程池环境。


8. 线程池核心参数有哪些?拒绝策略是什么?

口述版:

ThreadPoolExecutor 七大参数:

  1. corePoolSize:核心线程数
  2. maximumPoolSize:最大线程数
  3. keepAliveTime:非核心线程空闲存活时间
  4. unit:时间单位
  5. workQueue:任务队列
  6. threadFactory:线程工厂
  7. handler:拒绝策略

执行流程:

  1. 线程数 < corePoolSize → 创建核心线程
  2. 核心线程满了 → 任务进队列
  3. 队列满了 → 创建非核心线程(最多到 maximumPoolSize)
  4. 还满 → 执行拒绝策略

四种拒绝策略:

  • AbortPolicy(默认):直接抛 RejectedExecutionException
  • CallerRunsPolicy:用调用线程执行任务
  • DiscardPolicy:直接丢弃
  • DiscardOldestPolicy:丢弃队列里最老的任务,再尝试提交

生产环境一般自己定义线程池,不推荐用 Executors 工具类(容易 OOM)。


9. ConcurrentHashMap 怎么实现线程安全的?和 Hashtable、Collections.synchronizedMap 有什么区别?

口述版:

JDK 1.7:分段锁(Segment),默认 16 个段,不同段可以并发写。

JDK 1.8 及以后:取消分段锁,改用 CAS + synchronized。

  • 数组节点用 CAS 操作
  • 链表/红黑树头节点用 synchronized 锁定
  • 锁粒度更细,并发度更高

对比:

  • Hashtable:整个表用 synchronized,并发度低
  • Collections.synchronizedMap:也是整张 map 加锁
  • ConcurrentHashMap:锁粒度细,读操作大部分无锁,性能最好

注意:ConcurrentHashMap 的 size、isEmpty 等在并发下是弱一致性的,不保证绝对准确。


10. CountDownLatch、CyclicBarrier、Semaphore 的区别?

口述版:

三个常用同步工具:

工具 作用 能否重用 典型场景
CountDownLatch 一个或多个线程等待其他线程完成 不能 主线程等所有子任务结束
CyclicBarrier 一组线程互相等待,到齐后再继续 能(可重置) 多线程分阶段计算
Semaphore 控制同时访问资源的线程数量 能 限流、资源池

简单记:

  • CountDownLatch:等人(倒计时结束就放行)
  • CyclicBarrier:互相等(人齐了一起走)
  • Semaphore:限流(只有 N 个许可证)

11. 讲讲 AQS 原理

口述版:

AQS(AbstractQueuedSynchronizer)是 Java 并发包的基础框架,ReentrantLock、CountDownLatch、Semaphore 等都基于它实现。

核心思想:

  • 用一个 volatile int state 表示同步状态
  • 用 CLH 队列(双向链表)管理等待的线程
  • 支持独占模式和共享模式

获取锁失败的线程会进入队列,通过 LockSupport.park() 挂起,释放锁时唤醒后继节点。

面试能讲到:state 的含义、独占/共享、FIFO 队列、park/unpark,就已经很好了。再深入可以提模板方法模式(tryAcquire、tryRelease 由子类实现)。


12. 什么是死锁?怎么排查和避免?

口述版:

死锁是两个或多个线程互相持有对方需要的锁,形成环形等待,永远阻塞。

产生死锁的四个必要条件:

  1. 互斥
  2. 占有且等待
  3. 不可剥夺
  4. 循环等待

排查:

  • jstack 看线程堆栈,找 waiting to lock 和 locked
  • 或用 jconsole / VisualVM 检测死锁

避免方法:

  1. 按固定顺序获取锁
  2. 使用定时锁(tryLock(timeout))
  3. 减少锁的嵌套
  4. 使用更高级的并发工具代替自己加锁

13. synchronized 的锁升级过程是怎样的?

口述版:

JDK 6 之后对 synchronized 做了大量优化,锁有四个状态:

  1. 无锁
  2. 偏向锁:只有一个线程进入,直接偏向该线程,几乎无开销
  3. 轻量级锁:有竞争但程度低,用 CAS 自旋
  4. 重量级锁:竞争激烈,升级为操作系统互斥量,线程挂起

锁可以升级,但不能降级(偏向锁在一定条件下可以批量撤销)。

理解锁升级是为了说明:synchronized 在低竞争下性能已经很好,不一定非要用 ReentrantLock。


14. Future 和 CompletableFuture 有什么区别?

口述版:

Future 是 Java 5 引入的,只能通过 get() 阻塞获取结果,功能很弱:

  • 不能方便地做链式调用
  • 不能组合多个异步任务
  • 异常处理麻烦

CompletableFuture 是 Java 8 引入的,实现了 Future 和 CompletionStage,功能强大:

  • 支持非阻塞回调(thenApply、thenAccept 等)
  • 可以组合多个任务(allOf、anyOf、thenCombine)
  • 支持异常处理(exceptionally、handle)
  • 可以手动完成(complete)

在异步编程、并行任务编排场景,强烈推荐用 CompletableFuture,而不是原始 Future。

(详细对比和阻塞问题可结合之前讨论的「Future.get() 占用 HTTP 线程」来答,很加分。)


15. 线程池为什么不推荐用 Executors 创建?

口述版:

《阿里巴巴 Java 开发手册》明确不推荐,原因是:

  1. FixedThreadPool 和 SingleThreadPool:队列是无界的 LinkedBlockingQueue,任务堆积会导致 OOM。
  2. CachedThreadPool:允许创建的线程数是 Integer.MAX_VALUE,可能创建大量线程导致 OOM。
  3. ScheduledThreadPool:同样存在线程数失控风险。

正确做法是自己通过 ThreadPoolExecutor 构造方法,明确指定核心参数和拒绝策略,让资源可控。


面试加分点

  1. 能结合项目讲线程池参数是怎么评估的、拒绝策略选了什么。
  2. 清楚 ConcurrentHashMap 在 JDK7 和 JDK8 的实现差异。
  3. 能讲清楚 volatile 和 synchronized 在可见性、原子性上的区别。
  4. 对 AQS、CAS、happens-before 有基本认知。
  5. 实际用过 CompletableFuture 做异步编排(比如之前 PDF 异步生成),能说出优势。
  6. 知道 ThreadLocal 内存泄漏的原因和正确用法。

快速记忆框架:

Java 并发面试核心围绕:

线程基础 → synchronized / Lock → volatile / JMM → CAS / 原子类 → 线程池 → 并发容器 → AQS 与同步工具 → CompletableFuture

把原理说清楚,再结合自己项目中的实际使用场景,竞争力会明显提升。