260115-并发编程面试题

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
// ✅ 支持超时,但必须 try-finally
private final ReentrantLock lock = new ReentrantLock();

public void deductStock(Long skuId, int quantity) {
try {
// 尝试获取锁,超时 3 秒快速失败
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) { // 所有 SKU 互相阻塞
// 扣减库存
}
}

正确示例

1
2
3
4
5
6
7
8
9
10
11
12
13
// ✅ 正确:按 SKU 细粒度加锁
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
// 按 SKU + 仓库维度加锁
public void deductStock(Long skuId, Long warehouseId, int quantity) {
String lockKey = "stock:" + skuId + ":" + warehouseId;
Object lock = lockMap.computeIfAbsent(lockKey, k -> new Object());

synchronized (lock) {
// 扣减指定仓库的指定 SKU 库存
}
}

1.4 分布式场景下的防超卖方案

重要提醒:JVM 锁只能防单节点内存超卖,分布式部署必须下沉到 Redis 或 DB 层。

方案 1:Redis Lua 原子操作

优势

  • ✅ 原子性保证
  • ✅ 性能高(内存操作)
  • ✅ 支持复杂逻辑

实现

1
2
3
4
5
6
7
8
9
10
11
-- deduct_stock.lua
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
// Java 调用
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 >= ?;

-- 执行结果判断
-- 影响行数 > 0:扣减成功
-- 影响行数 = 0:库存不足或已被扣减
1
2
3
4
5
6
7
8
9
10
11
12
// MyBatis 实现
@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 {
// 尝试获取锁,等待 3 秒,锁定 10 秒后自动释放
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();
}
}

// Filter 中设置
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]: ...

关键特性

  1. ThreadLocalMap

    • 每个线程私有的 Map
    • 存储在 Thread 对象的 threadLocals 字段
    • 生命周期与线程相同
  2. Entry 结构

    1
    2
    3
    4
    5
    6
    7
    8
    static class Entry extends WeakReference<ThreadLocal<?>> {
    Object value; // 强引用

    Entry(ThreadLocal<?> k, Object v) {
    super(k); // key 是弱引用
    value = v; // value 是强引用
    }
    }
  3. 引用关系

    • 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
// ✅ 正确:在 finally 中强制 remove
@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
ThreadLocal<String> context = new ThreadLocal<>();
context.set("parent-value");

executor.submit(() -> {
String value = context.get(); // null!
});

解决方案:使用阿里 TTL(TransmittableThreadLocal)

1
2
3
4
5
6
7
8
9
10
// ✅ 正确:使用 TTL
TransmittableThreadLocal<String> context = new TransmittableThreadLocal<>();
context.set("parent-value");

// 使用 TtlExecutors 包装线程池
ExecutorService ttlExecutor = TtlExecutors.getTtlExecutorService(executor);

ttlExecutor.submit(() -> {
String value = context.get(); // "parent-value" ✅
});

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
# 使用 Arthas 监控 ThreadLocal Map 大小
java -jar arthas-boot.jar

# 查看线程信息
thread

# 查看指定线程的 ThreadLocalMap
thread <thread-id>

# 监控内存使用
dashboard

2.5 响应式架构替代方案

问题:WebFlux 等响应式架构中,线程不再绑定请求

1
2
3
4
5
6
// ❌ WebFlux 中 ThreadLocal 失效
@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
// ✅ 正确:使用 Reactor Context
@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 会失效。

参考资料

#
Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×