Java 并发编程面试题完全指南 目录
一、锁选型问题 1.1 库存扣减场景下的锁选择 面试官问 :库存扣减场景下,ReentrantLock 和 synchronized 如何选择?
核心原则 :
1 2 3 单机逻辑简单 → synchronized 需要超时/可中断 → ReentrantLock 分布式部署 → Redis/DB 层(JVM 锁不够)
选择策略 :
场景 1:单机简单逻辑 推荐 :synchronized
原因 :
✅ 代码简洁,自动释放锁
✅ JDK 1.6 后性能优化(偏向锁、轻量级锁)
✅ 不会漏写导致死锁
✅ 维护成本低
适用场景 :
单节点部署
锁竞争不激烈
逻辑简单,不需要超时控制
场景 2:需要超时控制 推荐 :ReentrantLock
原因 :
✅ 支持 tryLock(timeout) 超时机制
✅ 支持可中断锁获取
✅ 支持公平锁/非公平锁
✅ 支持多个 Condition
适用场景 :
大促期间锁竞争激烈
需要快速失败避免线程堆积
需要精细控制锁行为
场景 3:分布式部署 推荐 :Redis 或 DB 层(不用 JVM 锁)
原因 :
❌ JVM 锁只能防单节点内存超卖
❌ 多节点部署时无效
✅ 必须下沉到分布式层
核心方案 :
Redis Lua 原子操作
DB 乐观锁
分布式锁(Redisson/ZooKeeper)
1.2 synchronized vs ReentrantLock 对比 详细对比表格 :
对比维度
synchronized
ReentrantLock
实现层面
JVM 层面(monitorenter/monitorexit)
API 层面(java.util.concurrent)
锁释放
自动释放
必须手动释放(try-finally)
超时控制
❌ 不支持
✅ 支持 tryLock(timeout)
可中断
❌ 不支持
✅ 支持 lockInterruptibly()
公平锁
❌ 只能非公平
✅ 支持公平/非公平
条件变量
❌ 只有一个 wait/notify
✅ 支持多个 Condition
性能
JDK 1.6 后优化,差别不大
略优(高竞争场景)
代码复杂度
低(语法糖)
高(必须 try-finally)
死锁风险
低(自动释放)
高(忘记释放会死锁)
代码对比 :
synchronized 示例 1 2 3 4 5 6 7 8 public synchronized void deductStock (Long skuId, int quantity) { Stock stock = stockMapper.selectById(skuId); if (stock.getQuantity() >= quantity) { stock.setQuantity(stock.getQuantity() - quantity); stockMapper.updateById(stock); } }
ReentrantLock 示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 private final ReentrantLock lock = new ReentrantLock();public void deductStock (Long skuId, int quantity) { try { if (lock.tryLock(3 , TimeUnit.SECONDS)) { try { Stock stock = stockMapper.selectById(skuId); if (stock.getQuantity() >= quantity) { stock.setQuantity(stock.getQuantity() - quantity); stockMapper.updateById(stock); } } finally { lock.unlock(); } } else { throw new BusinessException("系统繁忙,请稍后重试" ); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new BusinessException("获取锁被中断" ); } }
1.3 锁粒度设计原则 核心原则 :绝对不锁全局!
错误示例 :
1 2 3 4 5 6 7 8 private final Object globalLock = new Object();public void deductStock (Long skuId, int quantity) { synchronized (globalLock) { } }
正确示例 :
1 2 3 4 5 6 7 8 9 10 11 12 13 private final ConcurrentHashMap<Long, Object> lockMap = new ConcurrentHashMap<>();public void deductStock (Long skuId, int quantity) { Object lock = lockMap.computeIfAbsent(skuId, k -> new Object()); synchronized (lock) { Stock stock = stockMapper.selectById(skuId); if (stock.getQuantity() >= quantity) { stock.setQuantity(stock.getQuantity() - quantity); stockMapper.updateById(stock); } } }
锁粒度设计策略 :
策略
说明
适用场景
全局锁
所有请求共用一把锁
❌ 不推荐(性能差)
对象锁
按业务对象加锁(如 SKU)
✅ 推荐(细粒度)
分段锁
将数据分段,每段一把锁
✅ 推荐(高并发)
读写锁
读共享、写独占
✅ 推荐(读多写少)
细粒度加锁示例 :
1 2 3 4 5 6 7 8 9 public void deductStock (Long skuId, Long warehouseId, int quantity) { String lockKey = "stock:" + skuId + ":" + warehouseId; Object lock = lockMap.computeIfAbsent(lockKey, k -> new Object()); synchronized (lock) { } }
1.4 分布式场景下的防超卖方案 重要提醒 :JVM 锁只能防单节点内存超卖,分布式部署必须下沉到 Redis 或 DB 层。
方案 1:Redis Lua 原子操作 优势 :
✅ 原子性保证
✅ 性能高(内存操作)
✅ 支持复杂逻辑
实现 :
1 2 3 4 5 6 7 8 9 10 11 local stock_key = KEYS[1 ]local quantity = tonumber (ARGV[1 ])local stock = tonumber (redis.call('GET' , stock_key))if stock and stock >= quantity then redis.call('DECRBY' , stock_key, quantity) return 1 else return 0 end
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 DefaultRedisScript<Long> script = new DefaultRedisScript<>(); script.setScriptSource(new ResourceScriptSource( new ClassPathResource("deduct_stock.lua" ))); script.setResultType(Long.class ) ; Long result = redisTemplate.execute( script, Collections.singletonList("stock:" + skuId), quantity ); if (result == 1 ) { } else { throw new BusinessException("库存不足" ); }
方案 2:DB 乐观锁 优势 :
实现 :
1 2 3 4 5 6 7 8 UPDATE stock SET quantity = quantity - ? WHERE id = ? AND quantity >= ?;
1 2 3 4 5 6 7 8 9 10 11 12 @Update ("UPDATE stock SET quantity = quantity - #{quantity} " + "WHERE id = #{skuId} AND quantity >= #{quantity}" ) int deductStock (@Param("skuId" ) Long skuId, @Param ("quantity" ) int quantity) ;public void deductStock (Long skuId, int quantity) { int rows = stockMapper.deductStock(skuId, quantity); if (rows == 0 ) { throw new BusinessException("库存不足" ); } }
方案 3:Redis 分布式锁 优势 :
实现 (使用 Redisson):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 @Autowired private RedissonClient redissonClient;public void deductStock (Long skuId, int quantity) { RLock lock = redissonClient.getLock("stock_lock:" + skuId); try { if (lock.tryLock(3 , 10 , TimeUnit.SECONDS)) { try { Stock stock = stockMapper.selectById(skuId); if (stock.getQuantity() >= quantity) { stock.setQuantity(stock.getQuantity() - quantity); stockMapper.updateById(stock); } else { throw new BusinessException("库存不足" ); } } finally { lock.unlock(); } } else { throw new BusinessException("系统繁忙,请稍后重试" ); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new BusinessException("获取锁被中断" ); } }
完整防超卖架构 :
1 2 3 4 5 6 7 8 9 请求到达 ↓ 本地缓存预扣减(JVM 锁,轻量防撞) ↓ Redis Lua 原子扣减(核心防超卖) ↓ DB 乐观锁最终扣减(强一致性保障) ↓ 异步更新缓存
二、ThreadLocal 问题 2.1 电商系统中的使用场景 面试官问 :ThreadLocal 在电商系统中的使用场景和内存泄漏风险?
主要使用场景 :
ThreadLocal 主要用于请求上下文的线程隔离 ,避免参数层层透传。
典型场景 :
场景
说明
示例
用户上下文
存储当前登录用户信息
userId、username、roleId
链路追踪
全链路追踪 ID
traceId、spanId
分库分表路由
动态数据源路由键
tenantId、dbIndex
国际化
当前请求的语言环境
Locale、TimeZone
事务管理
事务上下文传递
Connection、Transaction
代码示例 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 public class UserContext { private static final ThreadLocal<UserInfo> currentUser = new ThreadLocal<>(); public static void set (UserInfo user) { currentUser.set(user); } public static UserInfo get () { return currentUser.get(); } public static void remove () { currentUser.remove(); } } public class UserContextFilter implements Filter { @Override public void doFilter (ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { try { String userId = request.getHeader("X-User-Id" ); UserContext.set(new UserInfo(userId)); chain.doFilter(request, response); } finally { UserContext.remove(); } } }
2.2 底层原理分析 ThreadLocal 的底层结构 :
1 2 3 4 5 6 Thread └─ threadLocals (ThreadLocalMap) └─ Entry[] table ├─ Entry[0]: key=ThreadLocal实例(弱引用), value=对象(强引用) ├─ Entry[1]: key=ThreadLocal实例(弱引用), value=对象(强引用) └─ Entry[n]: ...
关键特性 :
ThreadLocalMap
每个线程私有的 Map
存储在 Thread 对象的 threadLocals 字段
生命周期与线程相同
Entry 结构
1 2 3 4 5 6 7 8 static class Entry extends WeakReference <ThreadLocal <?>> { Object value; Entry(ThreadLocal<?> k, Object v) { super (k); value = v; } }
引用关系
key :ThreadLocal 实例的弱引用
value :存储对象的强引用
2.3 内存泄漏风险 内存泄漏场景 :
1 线程池复用 + 未调用 remove() → 内存泄漏
泄漏原因分析 :
1 2 3 4 5 6 7 8 9 10 11 12 13 正常流程: ThreadLocal 实例 → 弱引用 → Entry.key ↓ GC 回收 Entry.key = null 问题流程: Entry.value (强引用) → 对象 ↑ Entry 仍然被 ThreadLocalMap 引用 ↑ Thread 不销毁(线程池复用) ↑ value 对象无法被 GC 回收 → 内存泄漏
图解 :
1 2 3 4 5 6 7 8 9 泄漏前: Thread → ThreadLocalMap → Entry[] ├─ key(弱引用) → ThreadLocal 实例 └─ value(强引用) → UserInfo 对象 泄漏后(ThreadLocal 实例被 GC): Thread → ThreadLocalMap → Entry[] ├─ key = null (已被 GC) └─ value(强引用) → UserInfo 对象 ← 无法回收!
电商大促风险 :
🔴 QPS 高 :大量请求创建 ThreadLocal 对象
🔴 线程池复用 :线程不销毁,Entry 持续累积
🔴 漏清理 :忘记调用 remove()
🔴 后果 :极易 OOM(内存溢出)
2.4 最佳实践规范 我们团队的规范 :
规范 1:强制封装工具类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public class UserContext { private static final ThreadLocal<UserInfo> currentUser = new ThreadLocal<>(); public static void set (UserInfo user) { currentUser.set(user); } public static UserInfo get () { return currentUser.get(); } public static void remove () { currentUser.remove(); } }
规范 2:Filter/Interceptor 中强制清理 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @Component public class UserContextFilter implements Filter { @Override public void doFilter (ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { try { String userId = request.getHeader("X-User-Id" ); UserContext.set(new UserInfo(userId)); chain.doFilter(request, response); } finally { UserContext.remove(); } } }
规范 3:异步场景使用 TTL 问题 :线程池异步执行时,ThreadLocal 无法传递
1 2 3 4 5 6 7 ThreadLocal<String> context = new ThreadLocal<>(); context.set("parent-value" ); executor.submit(() -> { String value = context.get(); });
解决方案 :使用阿里 TTL(TransmittableThreadLocal)
1 2 3 4 5 6 7 8 9 10 TransmittableThreadLocal<String> context = new TransmittableThreadLocal<>(); context.set("parent-value" ); ExecutorService ttlExecutor = TtlExecutors.getTtlExecutorService(executor); ttlExecutor.submit(() -> { String value = context.get(); });
Maven 依赖 :
1 2 3 4 5 <dependency > <groupId > com.alibaba</groupId > <artifactId > transmittable-thread-local</artifactId > <version > 2.14.2</version > </dependency >
规范 4:压测期监控 1 2 3 4 5 6 7 8 9 10 11 java -jar arthas-boot.jar thread thread <thread-id> dashboard
2.5 响应式架构替代方案 问题 :WebFlux 等响应式架构中,线程不再绑定请求
1 2 3 4 5 6 @GetMapping ("/reactive" )public Mono<String> reactiveEndpoint () { UserContext.set(new UserInfo("123" )); return Mono.just("Hello" ); }
解决方案 :使用 Reactor 的 Context
1 2 3 4 5 6 7 8 @GetMapping ("/reactive" )public Mono<String> reactiveEndpoint () { return Mono.deferContextual(ctx -> { UserInfo user = ctx.getOrDefault("user" , null ); return Mono.just("Hello, " + user.getUserId()); }).contextWrite(ctx -> ctx.put("user" , new UserInfo("123" ))); }
对比 :
特性
ThreadLocal
Reactor Context
线程绑定
✅ 线程私有
❌ 跨线程传递
响应式支持
❌ 不支持
✅ 原生支持
传递方式
隐式
显式
生命周期
线程生命周期
请求生命周期
适用场景
Servlet 架构
WebFlux/响应式
三、面试回答模板 3.1 锁选型问题回答框架 面试官问 :库存扣减场景下,ReentrantLock 和 synchronized 如何选择?
标准回答 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 在库存扣减场景下,我的选择原则是: 第一步【场景分析】: 先判断部署架构。如果是单机部署,根据具体需求选择 JVM 锁; 如果是分布式部署,JVM 锁不作为核心防超卖手段,必须下沉到 Redis 或 DB 层。 第二步【JVM 锁选型】: 具体到 JVM 锁选型,我会看三个维度: 1. 是否需要超时控制: 大促时锁竞争激烈,ReentrantLock 的 tryLock(timeout) 能在拿 不到锁时快速失败,避免线程堆积打满线程池;synchronized 只能 无限阻塞,容易引发雪崩。 2. 代码安全性与维护成本: synchronized 自动释放,不会漏写导致死锁;ReentrantLock 必须 try-finally,对团队规范要求高。如果逻辑简单且不需要高级特性, 优先 synchronized。 3. 锁粒度设计: 无论选哪个,绝对不锁全局。一定按 SKU + 仓库/门店维度细粒度加 锁,必要时结合分段锁思想进一步提升并发。 第三步【分布式方案】: 但必须强调:JVM 锁只能防单节点内存超卖。实际生产中: - 本地缓存预扣减会用 JVM 锁做轻量防撞 - 核心扣减一定走 Redis Lua 原子操作或 DB 乐观锁 - UPDATE stock = stock - ? WHERE id = ? AND stock >= ? 第四步【总结】: 锁只是手段,库存正确性靠的是分布式原子性 + 合理粒度 + 降级兜底。
3.2 ThreadLocal 问题回答框架 面试官问 :ThreadLocal 在电商系统中的使用场景和内存泄漏风险?
标准回答 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 在电商系统中,ThreadLocal 主要用于请求上下文的线程隔离。 第一部分【使用场景】: 比如用户 ID、traceId、分库分表路由键等,避免参数层层透传。 典型场景包括: - 用户上下文(userId、username) - 链路追踪(traceId、spanId) - 分库分表路由(tenantId、dbIndex) 第二部分【底层原理】: 它的底层是线程私有的 ThreadLocalMap,key 是弱引用,value 是 强引用。每个线程都有自己的 ThreadLocalMap,存储在 Thread 对象 中,生命周期与线程相同。 第三部分【内存泄漏风险】: 内存泄漏通常发生在线程池复用 + 未调用 remove() 的场景。因为 key 被 GC 回收后,value 仍被 Entry 强引用,线程不销毁就会一直 累积。电商大促 QPS 高,漏清理极易 OOM。 第四部分【最佳实践】: 我们团队的规范是: 1. 所有 ThreadLocal 必须封装工具类 2. 在 Filter/Interceptor 的 finally 中强制 remove() 3. 异步场景统一用阿里 TTL 传递上下文 4. 压测期通过 Arthas 监控线程 Map 大小 第五部分【响应式架构】: 另外在 WebFlux 等响应式架构中,我们会改用 Reactor 的 Context 替代,因为线程不再绑定请求,ThreadLocal 会失效。
参考资料