面试官提问:Spring单例Bean是线程安全的吗?
面试现场实录
我 :(推门坐下,发现面试官眼睛突然放光)
面试官 :Spring的单例Bean是线程安全的吗?
我:(内心OS:这题看似基础实则坑多,容我慢慢给你扒皮)
这不仅仅是一个普通的技术题,更是一个典型的「照妖镜问题」!今天我们通过三个事故场景+真实线上案例,来谈谈如何解答这个高频面试问题。
真实血泪:从代码爆炸到系统崩溃的三个场景
案例一:计数器陷阱(深夜报警惊醒事件)
去年双十一零点,我们的优惠券系统突然涌进20万次请求。由于使用了如下的单例计数器:
java
@Component
public class CouponCounter {
private int count; // 危险变量!
public boolean grantCoupon() {
if(count < 10000) {
count++;
return true;
}
return false;
}
}
当两个线程同时读到 count=9999
时,导致直接多发了两千张优惠券。财务对账时发现,多支出了二十万。这就是把单例Bean当万能保险的代价。
案例二:用户数据连环车祸(订单错乱事件)
某次排查用户投诉时,发现A用户看到的居然是B用户的订单。代码中埋下了这个隐患:
typescript
@Component
public class OrderHolder {
private String currentOrderNo; // 夺命全局变量
public void setOrder(String no) {
this.currentOrderNo = no;
}
public String getOrder() {
return currentOrderNo;
}
}
在高并发情况下,线程1刚把 currentOrderNo
设置为A,还没处理完,线程2就把它改成了B,导致线程1后续操作出现错乱。
案例三:HashMap爆仓惨案(系统瘫痪事件)
我们曾经用单例Bean做本地缓存。看似简单的代码:
typescript
@Component
public class ProductCache {
private Map<String, Object> cache = new HashMap<>();
public void put(String k, Object v) {
cache.put(k, v);
}
}
当QPS达到5000时,系统频繁出现 ConcurrentModificationException
,最终导致整个缓存模块崩溃。监控数据显示,这就像是一颗突然爆炸的炸弹。
核心生存指南:三种保命手段
保命手段一:原子武器库(真实救场代码)
修改案例一的正确姿势:
java
@Component
public class SafeCouponCounter {
private final AtomicInteger count = new AtomicInteger(0);
public boolean grantCoupon() {
while(true) {
int current = count.get();
if(current >= 10000) return false;
if(count.compareAndSet(current, current + 1)) {
return true;
}
}
}
}
改用 AtomicInteger
后,TPS 直接飙升到30000+,性能提升显著。
保命手段二:线程专属集装箱(ThreadLocal妙用)
重构案例二的正确方式:
typescript
@Component
public class OrderContext {
private static final ThreadLocal<String> context = new ThreadLocal<>();
public void setOrder(String no) {
context.set(no);
}
public String getOrder() {
return context.get();
}
}
通过使用 ThreadLocal
,每个线程都获得了专属的存储空间,避免了全局变量带来的问题。
保命手段三:防爆容器升级(并发容器选择)
修改案例三的正确姿势:
typescript
@Component
public class SafeProductCache {
private final ConcurrentHashMap<String, Object> cache = new ConcurrentHashMap<>();
public void put(String k, Object v) {
cache.put(k, v);
}
}
使用 ConcurrentHashMap
后,系统稳定性大幅提升,缓存模块再也没有出现崩溃的情况。
面试高光时刻应答模板
下次遇到这个面试题时,可以按以下模板来应答:
1️⃣ 先抛观点 :"这就像问汽车能不能自动驾驶,要看驾驶员怎么操作。"
2️⃣ 举实例证 :"就像我经历过某次事故,单例计数器导致多发优惠券......"
3️⃣ 提解法 :"这时候就要用原子类或 ThreadLocal
来拆弹。"
4️⃣ 升维度:"这本质上是对象作用域与线程模型匹配的问题。"
技术人的觉醒时刻
有一天凌晨三点排查故障时,我突然在生产环境的报错堆栈中看到了自己写的单例Bean类名。那一刻,我才真正理解了什么是"面向生产编程"。
技术人的优雅,是写出来的代码能承受千万次请求,而不仅仅是在会议室讨论设计模式。下次再写 @Component
时,记得问自己:这个Bean会在什么战场上经历怎样的腥风血雨?