1. Java并发面试题100道解析:从基础到实战
作为一名经历过上百场技术面试的Java老兵,我深知并发编程在面试中的分量。这份精心整理的100道Java并发面试题,不仅覆盖了面试高频考点,更融入了我多年实战中积累的经验和教训。无论你是准备跳槽涨薪,还是想系统提升并发编程能力,这份指南都能帮你少走弯路。
2. 线程基础:理解并发编程的基石
2.1 线程与进程的本质区别
很多面试者能背出"进程是资源分配单位,线程是CPU调度单位"的标准答案,但真正理解其内涵的人不多。在实际开发中,我经常看到因为混淆两者概念导致的性能问题。
进程拥有独立的地址空间,这意味着:
- 进程间通信(IPC)成本高(需要管道、共享内存等机制)
- 进程切换开销大(需要保存/恢复完整的上下文)
- 进程崩溃不会影响其他进程(天然的隔离性)
而线程共享进程资源:
- 通信简单(直接读写共享变量即可)
- 切换成本低(只需保存程序计数器等少量寄存器)
- 一个线程崩溃可能导致整个进程退出(缺乏隔离性)
实际案例:我曾优化过一个日志服务,原设计为每个客户端连接创建独立进程,导致服务器在300连接时就资源耗尽。改为线程模型后,轻松支持5000+并发连接。
2.2 四种线程创建方式深度对比
继承Thread类
java复制class MyThread extends Thread {
@Override
public void run() {
System.out.println("Thread running");
}
}
// 启动
new MyThread().start();
- 优点:写法简单
- 缺点:Java单继承特性导致扩展性差
实现Runnable接口
java复制class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("Runnable running");
}
}
// 启动
new Thread(new MyRunnable()).start();
- 优点:可继承其他类,适合多线程共享同一任务
- 缺点:无法直接获取执行结果
实现Callable接口
java复制class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
return "Callable result";
}
}
// 启动
FutureTask<String> future = new FutureTask<>(new MyCallable());
new Thread(future).start();
System.out.println(future.get()); // 获取结果
- 优点:可返回结果,可抛出异常
- 缺点:使用稍复杂
线程池方式(推荐)
java复制ExecutorService executor = Executors.newFixedThreadPool(4);
executor.submit(() -> System.out.println("Pool running"));
executor.shutdown();
- 优点:资源可控,避免频繁创建销毁线程
- 缺点:需要理解线程池参数配置
踩坑记录:早期项目我曾大量直接new Thread(),导致线上出现数千线程竞争CPU,系统完全卡死。改用线程池后性能提升300%。
2.3 线程生命周期与状态转换
线程状态流转图:
code复制新建(NEW) → 就绪(RUNNABLE) ↔ 运行(RUNNING)
↓
阻塞(BLOCKED)
↓
等待(WAITING)
↓
超时等待(TIMED_WAITING)
↓
终止(TERMINATED)
关键状态解析:
- BLOCKED:等待获取监视器锁(如synchronized)
- WAITING:无限期等待(如object.wait())
- TIMED_WAITING:有限期等待(如Thread.sleep())
查看线程状态的方法:
java复制Thread thread = new Thread(()->{...});
thread.start();
System.out.println(thread.getState()); // 获取当前状态
2.4 sleep()与wait()的九大区别
| 特性 | sleep() | wait() |
|---|---|---|
| 所属类 | Thread | Object |
| 锁释放 | 不释放 | 释放 |
| 唤醒条件 | 时间到期 | notify()/notifyAll() |
| 使用场景 | 定时任务 | 线程间协调 |
| 异常 | InterruptedException | InterruptedException |
| 静态方法 | 是 | 否 |
| 调用前提 | 任何情况 | 必须持有锁 |
| 精度控制 | 支持纳秒 | 只支持毫秒 |
| JVM实现 | 原生支持 | 依赖监视器 |
典型错误案例:
java复制synchronized(lock) {
Thread.sleep(1000); // 错误!持有锁睡觉会导致其他线程阻塞
lock.wait(1000); // 正确!等待时会释放锁
}
3. 锁机制:高并发环境下的守护者
3.1 synchronized的底层实现原理
每个Java对象都有个隐藏的Monitor(监视器),synchronized正是通过它实现同步:
- 字节码层面:通过monitorenter和monitorexit指令实现
- JVM层面:依赖操作系统的互斥锁(Mutex Lock)
- 内存语义:遵循happens-before原则,保证可见性
对象头结构(64位JVM):
code复制|------------------------------------------------------------------|
| Mark Word (64bits) | Klass Word (64bits) |
|------------------------------------------------------------------|
| unused:25 | identity_hashcode:31 | unused:1 | age:4 | biased_lock:1 | lock:2 | → Normal
| thread:54 | epoch:2 | unused:1 | age:4 | biased_lock:1 | lock:2 | → Biased
| ptr_to_lock_record:62 | lock:2 | → Lightweight
| ptr_to_heavyweight_monitor:62 | lock:2 | → Heavyweight
| | lock:2 | → GC
3.2 锁升级的全过程解析
JDK1.6后的优化:无锁 → 偏向锁 → 轻量级锁 → 重量级锁
-
偏向锁(Biased Locking)
- 假设只有一个线程访问
- 在对象头记录线程ID
- 优点:零成本加锁
- 适用场景:单线程重复访问
-
轻量级锁(Lightweight Locking)
- 多个线程交替访问
- 通过CAS竞争锁
- 优点:避免OS级阻塞
- 失败后升级为重量级锁
-
重量级锁(Heavyweight Locking)
- 真正的互斥锁
- 线程阻塞进入等待队列
- 优点:保证严格互斥
- 缺点:上下文切换开销大
查看锁状态的方法:
java复制// 添加JVM参数:-XX:+PrintFlagsFinal
// 查找BiasedLocking相关参数
3.3 死锁的四大必要条件与破解之道
必要条件:
- 互斥条件
- 请求与保持
- 不可剥夺
- 循环等待
诊断工具:
bash复制jstack <pid> # 查看线程堆栈
jconsole # 图形化监控
破解方案对比:
| 方案 | 实现方式 | 优点 | 缺点 |
|---|---|---|---|
| 顺序加锁 | 统一获取锁的顺序 | 彻底预防 | 设计复杂度高 |
| 超时锁 | tryLock(timeout) | 简单易实现 | 可能活锁 |
| 死锁检测 | 定期扫描锁依赖图 | 可发现潜在死锁 | 实现复杂 |
| 事务回滚 | 引入重试机制 | 通用性强 | 业务逻辑需支持 |
实战案例:银行转账问题
java复制// 错误实现:可能死锁
void transfer(Account from, Account to, int amount) {
synchronized(from) {
synchronized(to) {
// 转账逻辑
}
}
}
// 正确实现:按hash排序
void transfer(Account from, Account to, int amount) {
Account first = from.hashCode() < to.hashCode() ? from : to;
Account second = first == from ? to : from;
synchronized(first) {
synchronized(second) {
// 转账逻辑
}
}
}
4. volatile与JMM内存模型
4.1 可见性问题的本质
现代CPU架构导致的内存可见性问题:
- CPU多级缓存(L1/L2/L3)
- 写缓冲区(Store Buffer)
- 无效化队列(Invalidate Queue)
volatile的底层实现:
- 写操作:插入StoreLoad屏障(如x86的lock前缀指令)
- 读操作:强制刷新CPU缓存
JMM规范下的happens-before规则:
- 程序顺序规则
- 监视器锁规则
- volatile规则
- 线程启动规则
- 线程终止规则
- 中断规则
- 终结器规则
4.2 单例模式的双重检查锁实现
典型错误实现:
java复制class Singleton {
private static Singleton instance;
public static Singleton getInstance() {
if (instance == null) { // 第一次检查
synchronized (Singleton.class) {
if (instance == null) { // 第二次检查
instance = new Singleton();
}
}
}
return instance;
}
}
问题在于new操作可能被重排序:
- 分配内存空间
- 初始化对象
- 将引用指向内存地址
正确实现(JDK5+):
java复制class Singleton {
private static volatile Singleton instance;
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
4.3 volatile的典型使用场景
- 状态标志
java复制volatile boolean shutdownRequested;
public void shutdown() { shutdownRequested = true; }
public void doWork() {
while (!shutdownRequested) {
// 业务逻辑
}
}
- 一次性发布
java复制class ConfigLoader {
private volatile static Config config;
public static Config getConfig() {
if (config == null) {
synchronized (ConfigLoader.class) {
if (config == null) {
config = loadConfig();
}
}
}
return config;
}
}
- 独立观察
java复制volatile long lastLoginTime;
public void onLogin() {
lastLoginTime = System.currentTimeMillis();
// 其他操作
}
性能测试:在百万次读写的测试中,volatile变量访问比普通变量慢约20%,但比synchronized快10倍以上。
5. 线程池:并发编程的工业级解决方案
5.1 七大核心参数详解
-
corePoolSize(核心线程数)
- 池中常驻线程数量
- 即使空闲也不会被回收
- 设置建议:CPU密集型=Ncpu+1,IO密集型=2*Ncpu
-
maximumPoolSize(最大线程数)
- 池中允许的最大线程数
- 当队列满时创建新线程
- 设置建议:根据业务峰值设置
-
keepAliveTime(空闲时间)
- 非核心线程空闲存活时间
- 设置建议:根据任务频次调整
-
unit(时间单位)
- keepAliveTime的时间单位
- 常用TimeUnit.SECONDS等
-
workQueue(工作队列)
- 任务排队策略
- 常见实现:
- ArrayBlockingQueue(有界)
- LinkedBlockingQueue(无界)
- SynchronousQueue(直接交接)
-
threadFactory(线程工厂)
- 创建新线程的方式
- 可自定义线程名、优先级等
-
handler(拒绝策略)
- 当队列和线程池都满时的处理策略
- 内置策略:
- AbortPolicy(默认,抛出异常)
- CallerRunsPolicy(调用者运行)
- DiscardPolicy(静默丢弃)
- DiscardOldestPolicy(丢弃最老任务)
5.2 四种常见线程池对比
| 类型 | 核心线程数 | 最大线程数 | 工作队列 | 特点 |
|---|---|---|---|---|
| FixedThreadPool | 固定 | 同核心 | LinkedBlockingQueue | 固定大小,无界队列 |
| CachedThreadPool | 0 | Integer.MAX_VALUE | SynchronousQueue | 自动扩容,适合短时任务 |
| SingleThreadExecutor | 1 | 1 | LinkedBlockingQueue | 单线程顺序执行 |
| ScheduledThreadPool | 指定 | Integer.MAX_VALUE | DelayedWorkQueue | 支持定时/周期性任务 |
5.3 线程池最佳实践
-
不要使用Executors快捷方法
- 问题:FixedThreadPool和SingleThreadExecutor使用无界队列,可能OOM
- 正确做法:手动创建ThreadPoolExecutor
-
合理设置队列容量
java复制new ThreadPoolExecutor( 4, 8, 30, TimeUnit.SECONDS, new ArrayBlockingQueue<>(1000), // 有界队列 new ThreadPoolExecutor.CallerRunsPolicy() ); -
给线程池命名
java复制ThreadFactory namedThreadFactory = new ThreadFactoryBuilder() .setNameFormat("my-pool-%d") .build(); -
监控线程池状态
java复制// 实现自己的RejectedExecutionHandler class MonitorRejectHandler implements RejectedExecutionHandler { @Override public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) { // 记录报警 } } -
优雅关闭
java复制executor.shutdown(); // 平缓关闭 if (!executor.awaitTermination(60, TimeUnit.SECONDS)) { executor.shutdownNow(); // 强制关闭 }
血泪教训:曾因使用无界队列导致百万任务堆积,最终OOM。改用有界队列+CallerRunsPolicy后系统稳定性大幅提升。
6. 并发工具类:JUC的强大武器库
6.1 CountDownLatch vs CyclicBarrier
对比表格:
| 特性 | CountDownLatch | CyclicBarrier |
|---|---|---|
| 计数器 | 不可重置 | 可重置 |
| 等待机制 | 主线程等待子线程 | 线程互相等待 |
| 异常处理 | 不影响其他线程 | 会传播异常 |
| 使用场景 | 启动准备/结束统计 | 分阶段任务 |
| 构造方法 | 指定计数 | 指定计数+可选Runnable |
典型用例:
java复制// CountDownLatch:多任务并行执行后汇总
CountDownLatch latch = new CountDownLatch(3);
for (int i = 0; i < 3; i++) {
new Thread(() -> {
doWork();
latch.countDown();
}).start();
}
latch.await(); // 等待所有任务完成
// CyclicBarrier:多阶段并行计算
CyclicBarrier barrier = new CyclicBarrier(3, () -> {
System.out.println("All threads reached barrier");
});
for (int i = 0; i < 3; i++) {
new Thread(() -> {
phase1();
barrier.await();
phase2();
}).start();
}
6.2 ConcurrentHashMap的演进之路
JDK7实现:
- 分段锁(Segment)
- 默认16个段
- 段内使用HashEntry数组
- get操作无锁
- put操作只锁对应段
JDK8改进:
- 取消分段锁
- 改用Node数组+CAS+synchronized
- 链表转红黑树(阈值=8)
- 扩容时多线程协助
关键代码片段:
java复制// JDK8 putVal实现(简化版)
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode());
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0)
tab = initTable();
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value)))
break;
}
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
synchronized (f) {
// 链表/红黑树插入逻辑
}
}
}
addCount(1L, binCount);
return null;
}
6.3 CompletableFuture的异步编排艺术
常见操作链:
java复制CompletableFuture.supplyAsync(() -> queryDatabase())
.thenApply(result -> transformData(result))
.thenAccept(transformed -> sendToAPI(transformed))
.exceptionally(ex -> {
log.error("Error occurred", ex);
return null;
});
组合多个Future:
java复制CompletableFuture<String> future1 = CompletableFuture.supplyAsync(...);
CompletableFuture<String> future2 = CompletableFuture.supplyAsync(...);
// 全部完成
CompletableFuture<Void> all = CompletableFuture.allOf(future1, future2);
// 任一完成
CompletableFuture<Object> any = CompletableFuture.anyOf(future1, future2);
// 合并结果
CompletableFuture<String> combined = future1.thenCombine(future2,
(result1, result2) -> result1 + result2);
超时控制(JDK9+):
java复制future.orTimeout(1, TimeUnit.SECONDS)
.exceptionally(ex -> "Fallback value");
性能对比:相比传统Future.get(),CompletableFuture回调方式可提升吞吐量3-5倍,特别适合IO密集型服务。
7. 并发编程实战经验总结
7.1 性能优化五大黄金法则
-
减少锁粒度:
- 用ConcurrentHashMap代替Collections.synchronizedMap
- 用LongAdder代替AtomicLong
-
降低锁竞争:
- 读写分离(CopyOnWriteArrayList)
- 数据分片(如数据库分库分表思想)
-
避免锁嵌套:
- 使用开放调用(调用外部方法时不持有锁)
- 重构为原子操作
-
使用无锁结构:
- ConcurrentLinkedQueue
- AtomicReference
-
合理使用线程池:
- 根据任务类型选择队列
- 设置合适的拒绝策略
7.2 常见并发Bug排查技巧
-
死锁检测:
bash复制
jstack <pid> | grep -A 10 deadlock -
线程阻塞分析:
bash复制
jstack <pid> | grep BLOCKED -A 5 -
CPU高负载排查:
bash复制top -Hp <pid> # 找出高CPU线程 printf "%x\n" <tid> # 转16进制 jstack <pid> | grep <hex-tid> -A 20 -
内存泄漏检查:
bash复制jmap -histo:live <pid> | head -20
7.3 并发测试工具推荐
-
JMH(Java Microbenchmark Harness)
- 官方微基准测试工具
- 避免JIT优化干扰
- 示例:
java复制@Benchmark @Threads(4) public void testConcurrentHashMap() { map.get(key); }
-
JUnit5并行测试
java复制@Execution(ExecutionMode.CONCURRENT) class MyConcurrentTest { @RepeatedTest(1000) void testThreadSafety() { ... } } -
Stress Testing
java复制// 使用CountDownLatch模拟并发 CountDownLatch start = new CountDownLatch(1); CountDownLatch end = new CountDownLatch(THREAD_COUNT); for (int i = 0; i < THREAD_COUNT; i++) { new Thread(() -> { start.await(); try { testMethod(); } finally { end.countDown(); } }).start(); } start.countDown(); // 同时释放所有线程 end.await(); // 等待所有线程完成
8. 面试实战:高频问题深度解析
8.1 "谈谈你对AQS的理解"
回答要点:
- AQS(AbstractQueuedSynchronizer)是JUC的核心基础组件
- 采用CLH队列变体管理等待线程
- 通过state变量表示同步状态
- 提供tryAcquire/tryRelease等模板方法
- 应用案例:
- ReentrantLock
- Semaphore
- CountDownLatch
- 公平与非公平实现差异
加分回答:
java复制// 自定义同步器示例
class MyLock extends AbstractQueuedSynchronizer {
protected boolean tryAcquire(int arg) {
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
protected boolean tryRelease(int arg) {
if (getState() == 0) throw new IllegalMonitorStateException();
setExclusiveOwnerThread(null);
setState(0);
return true;
}
}
8.2 "如何设计一个高并发秒杀系统"
分层解决方案:
-
前端层:
- 静态资源CDN加速
- 按钮防重复点击
- 随机化请求时间
-
接入层:
- Nginx限流(漏桶算法)
- 验证码过滤
- 请求队列削峰
-
服务层:
- 缓存预热(Redis)
- 库存扣减(Lua脚本保证原子性)
- 异步下单(MQ)
-
数据层:
- 数据库分库分表
- 乐观锁更新
- 柔性事务补偿
关键代码示例(Redis库存扣减):
lua复制-- KEYS[1]: 库存key
-- ARGV[1]: 扣减数量
local stock = tonumber(redis.call('GET', KEYS[1]))
if stock >= tonumber(ARGV[1]) then
return redis.call('DECRBY', KEYS[1], ARGV[1])
else
return -1
end
8.3 "ThreadLocal原理与内存泄漏防范"
实现原理:
- 每个Thread维护ThreadLocalMap
- Map的key是弱引用ThreadLocal实例
- set/get操作基于当前线程的map
内存泄漏风险:
- key被回收但value强引用
- 线程池场景下线程长期存活
正确使用方式:
java复制// 使用static final修饰
private static final ThreadLocal<SimpleDateFormat> formatter =
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));
// 显式remove
try {
formatter.get().format(...);
} finally {
formatter.remove(); // 必须清理
}
优化方案(FastThreadLocal):
- Netty提供的增强版
- 使用数组代替Map
- 索引变量定位value
- 自动清理机制
9. Java并发编程的未来趋势
9.1 Project Loom与虚拟线程
核心特性:
- 轻量级虚拟线程(协程)
- 百万级线程支持
- 兼容现有代码
- 结构化并发
示例对比:
java复制// 传统线程
Thread.ofPlatform().start(() -> {
doBlockingIO();
});
// 虚拟线程
Thread.ofVirtual().start(() -> {
doBlockingIO(); // 自动yield
});
9.2 Java并发编程的学习路线建议
-
基础阶段:
- 线程生命周期
- synchronized/volatile
- 基本线程池使用
-
进阶阶段:
- AQS原理
- 并发容器实现
- 锁优化技巧
-
高级阶段:
- 无锁编程
- 性能调优
- 分布式并发控制
推荐学习资源:
- 《Java并发编程实战》
- 《并发编程的艺术》
- JEP文档(如JEP-425)
- OpenJDK源码
10. 写在最后:我的并发编程心得
在多年的Java开发生涯中,我总结出三条并发编程的黄金法则:
-
简单即美:能用无锁就不用锁,能用单线程就不多线程。我曾为了"高性能"设计复杂并发结构,结果反而引入难以排查的Bug。
-
理解重于记忆:死记硬背synchronized语法不如理解Monitor工作原理,明白原理后各种锁问题都能迎刃而解。
-
工具要趁手:熟练使用jstack、Arthas、JMH等工具,好的工具能让并发问题无所遁形。
最后分享一个真实案例:某金融系统出现偶发性的余额错误,经过两周排查发现是因为在HashMap上误用了Collections.synchronizedMap()包装,而迭代操作没有整体加锁。改用ConcurrentHashMap后问题彻底解决。这个教训让我明白:在并发领域,选择正确的工具比编写复杂的代码更重要。