【面试】 Java中级开发工程师面试精选:深度问题与实战解析..
文章目录
- 🔥 问题1:HashMap与ConcurrentHashMap的线程安全差异
- ⚡ 场景题1:分布式锁下的资源竞争
- 💡 问题2:JVM内存模型与OOM实战
- 🔧 场景题2:线程池拒绝策略设计
- ✅ 总结考察点
🔥 问题1:HashMap与ConcurrentHashMap的线程安全差异
问题:
在多线程环境下,HashMap
和ConcurrentHashMap
的线程安全性有何本质区别?请结合缓存雪崩场景说明如何选择。
答案:
- 本质区别:
HashMap
非线程安全,多线程并发put
可能导致链表成环(JDK1.7)或数据覆盖(JDK1.8);
ConcurrentHashMap
通过 分段锁(JDK1.7)或 CAS + synchronized(JDK1.8)保证线程安全,锁粒度细化到桶级别。 - 缓存雪崩场景:
假设高并发查询商品库存,缓存失效后大量请求穿透到数据库。
选择ConcurrentHashMap
:
优势:private final ConcurrentHashMap<String, AtomicInteger> stockCache = new ConcurrentHashMap<>(); public void deductStock(String productId) { stockCache.compute(productId, (k, v) -> { if (v == null) v = new AtomicInteger(loadFromDB(k)); // 惰性加载 return v.decrementAndGet() >= 0 ? v : null; // 原子减库存 }); }
compute()
方法保证单键操作的原子性,避免超卖;分段锁支持高并发更新。
⚡ 场景题1:分布式锁下的资源竞争
问题:
某支付系统需保证同用户10秒内仅允许1次提现操作。若用Redis分布式锁实现,如何解决锁失效时间与业务执行时间的不确定性?
答案:
方案:锁续期(WatchDog) + 唯一令牌
public boolean tryWithdraw(String userId, double amount) { String lockKey = "lock:withdraw:" + userId; String token = UUID.randomUUID().toString(); // 唯一标识 try { // 尝试加锁(SET lockKey token NX PX 10000) if (redis.set(lockKey, token, "NX", "PX", 10000)) { // 启动WatchDog后台线程续期锁(每3秒续期10秒) startWatchDog(lockKey, token); if (checkWithdrawLimit(userId)) { // 业务逻辑 executeWithdraw(userId, amount); return true; } } return false; } finally { // 释放锁时验证token(Lua脚本保证原子性) String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; redis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(token)); }
}
关键点:
- 唯一令牌:避免误删其他线程的锁;
- 锁续期:防止业务未执行完锁过期;
- Lua原子解锁:保证判断与删除的原子性。
💡 问题2:JVM内存模型与OOM实战
问题:
线上服务频繁触发OutOfMemoryError: Metaspace
,如何定位?列举3种优化策略。
答案:
定位步骤:
jcmd <pid> VM.flags
检查Metaspace配置(-XX:MaxMetaspaceSize=256m
);jstat -gcmetacapacity <pid>
观察元空间使用趋势;- MAT工具分析
ClassLoader
泄露(如动态生成类未卸载)。
优化策略:
- 限制动态代理类:减少
CGLib
/ASM
生成的类; - 配置回收阈值:
-XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=256m
; - 重启策略:通过容器化定时重启实例释放元空间。
🔧 场景题2:线程池拒绝策略设计
问题:
订单系统需异步处理10万+/日的物流推送,线程池配置为core=5, max=20, queue=100
。
突发流量下队列满且线程数达max,如何自定义拒绝策略保证关键订单不丢失?
答案:
方案:降级写入MQ + 告警
ThreadPoolExecutor executor = new ThreadPoolExecutor( 5, 20, 30, TimeUnit.SECONDS, new ArrayBlockingQueue<>(100), new CustomRejectedExecutionHandler() // 自定义拒绝策略
); // 自定义拒绝策略
static class CustomRejectedExecutionHandler implements RejectedExecutionHandler { @Override public void rejectedExecution(Runnable task, ThreadPoolExecutor executor) { if (task instanceof LogisticsTask) { LogisticsTask logisticsTask = (LogisticsTask) task; if (logisticsTask.isHighPriority()) { // 关键订单写入RabbitMQ/Kafka mqClient.send("ORDER_FALLBACK_QUEUE", logisticsTask); } else { log.warn("丢弃普通订单: {}", logisticsTask.getOrderId()); } triggerAlert(); // 触发扩容告警 } }
}
效果:
- 关键订单通过MQ异步补偿;
- 普通订单丢弃避免拖垮服务;
- 告警触发K8s自动扩容Pod。
✅ 总结考察点
- 并发编程:线程安全容器、锁机制、原子操作;
- JVM调优:内存模型、OOM排查、GC策略;
- 分布式设计:分布式锁、降级策略、异步补偿;
- 框架实战:线程池参数化设计、资源竞争解决方案。
提示:实际面试中可延伸追问细节(如
synchronized
锁升级过程、ThreadLocal
内存泄露案例),验证候选人知识深度。