原文来自于:zha-ge.cn/java/103
从 IOC 到多线程:Spring 单例 Bean 的并发安全性全解析
春天到了,Spring 的故事又要开讲。你有没有和我一样,第一次用 Spring 的时候,有点怵单例 Bean?心里总嘀咕:单例不会有并发问题吗?要是两个线程一起用同一个 Bean,不就打架了吗?结果看了三天源码,差点没在 XML 配置里睡着。哎,踩坑的路,就是这么波澜不惊地温柔。
那些年我追过的单例 Bean
单例在 Spring 里啥都好用,写起来不用操心,各种自动装配,@Autowired 一打,配好就行。配置里 scope="singleton",不写其实也是 singleton,Spring 就帮咱省事。说人话就是:
- 全容器只有一个这个 Bean 对象
- 不管注入几次,用的其实就那一个
- Bean 默认是线程安全的?
你品一品,线程安全?真的安全吗?我那会儿还信了......
那天阳光很好,我和并发聊了一会天
场景:有个计数器 Bean,里头就一个 int 计数。然后两个线程一起操作,大家都懂得:
java
@Component
public class Counter {
private int count = 0;
public void increment() { count++; }
public int get() { return count; }
}
没啥花活,就是纯粹为了让坑掉出来。线程一边加,线程一边读,我抱着"Spring 会帮我做好"的想法,放心地写了下面这段测试:
java
@Autowired
private Counter counter;
// 两个线程轮流 counter.increment()
没过两分钟,count 就不对了。这是"你有你的张良计,Spring 有它的背水一战"------原来单例 Bean 并不是线程安全的!
踩坑瞬间
正式进大坑时,心情是这样的------
- 以为 Spring 单例 Bean 是万能的,能包治百病,结果它只是全局唯一
- 想用加锁,纠结死------好像丑了点,还憋屈
- 问 ChatGPT(没错,就是你),它说"用 volatile 或者原子类"
- 菜鸟我试了下 AtomicInteger,哎,还真好使!
这波下来,我悟了:Spring 只保证 Bean 是单例,不包线程安全。咱但凡 Bean 里有"状态"(尤其是可变字段),多线程就可能捅漏子。别问为啥,单例就是一家公司只有一个马云,几个员工抢话筒谁说了算?
贴个关键代码,妙用 Java 并发:
java
@Component
public class AtomicCounter {
private final AtomicInteger count = new AtomicInteger(0);
public void increment() { count.incrementAndGet(); }
public int get() { return count.get(); }
}
这样就再也不怕多线程抓头发了。
经验启示
Spring 单例只管"唯一",不管"安全" 单例的本质,是容器里只放一份对象;线程安全性得靠你自己兜底。
无状态服务天然安全 如果方法里只处理输入参数,不依赖成员变量,也不修改 Bean 的内部字段,那么这个 Bean 再多人抢着用都没问题。典型的就是各种工具类、DAO 层的接口代理。
有状态 Bean 要小心 一旦有可变成员变量(比如计数器、缓存、上下文记录),并发读写就有可能出错。要么加锁、要么用并发包里的原子类(AtomicInteger、ConcurrentHashMap 等)。
实在绕不开共享状态 那就乖乖用 synchronized、Lock、ThreadLocal 之类的手段来隔离或者保护。比如用户上下文就常用 ThreadLocal 来保证每个线程拿到自己的副本。
面试官杀手锏问题
问:Spring 的单例 Bean 是线程安全的吗?
答法:
Spring 单例只保证全局唯一实例,不自动保证线程安全。 如果 Bean 是无状态的(纯粹的方法调用),线程安全。 如果 Bean 内部持有可变状态,就需要开发者自己保证安全。 常用手段有原子类、并发容器、ThreadLocal,或者直接把 Bean 设计为无状态。 这么答,既简洁又能点出关键,面试官大概率会满意。
写在最后
Spring 单例 Bean 的并发安全性,说白了就是一句话: Spring 管你"有没有",不管你"安不安全"。
想偷懒:尽量写无状态 Bean。 想稳妥:用并发工具类兜底。 想装逼:顺手再抛出 IOC + AOP 的设计哲学,面试官立刻觉得你"格局打开了"。
等你踩过一次"计数器翻车"的坑,就再也不会天真地以为"单例=线程安全"了。