← 返回 Java核心图解 目录

🌱 Spring单例深度图解

从"会用@Autowired"到"理解Spring为什么只创建1个对象" · SVG流程图 + 生活类比 + IDE源码 + 面试速答

🔗 想看单例在并发场景下的真实翻车案例?推荐搭配

本页侧重Spring单例的创建流程与原理。若需并发场景下成员变量串写的真实事故,请看:

📘 08-单例与共享与新建区分图解

1. 单例到底是什么?

公司公共打印机——全公司只有1台打印机,谁需要打印就去用这台,不会每来一个人就买一台新打印机。

Spring的单例也是这个道理:整个Spring容器只创建1个对象,谁@Autowired就拿同一个引用去用。

反面:如果不用单例——每次请求来都new一个UserService,100个请求 = 100个对象 = 内存浪费!而且对象之间状态不共享,你改了他看不到。
❌ 非单例:每人一台 员工A的打印机 员工B的打印机 员工C的打印机 员工D的打印机 4个人 = 4台打印机 = 浪费! 每次请求都 new UserService() 内存浪费,状态不共享 ✅ 单例:全公司共用1台 公共打印机 (唯一实例) A B C D 4个人共用1台 = 省资源! Spring容器只创建1个对象 谁需要就给谁用(引用传递)
图1:单例 vs 非单例 — 公共打印机 vs 每人一台

单例的关键数字

维度非单例(每次new)Spring单例(默认)
对象数量100个请求 = 100个对象100个请求 = 1个对象
内存占用大量重复对象 → GC压力大只占1份内存
创建时机每次请求时创建容器启动时创建1次
注入的依赖每个对象各注入一份所有请求共享同一份依赖
状态共享❌ 不共享,各自为政✅ 共享(但要注意线程安全)

2. GoF单例 vs Spring单例 —— 不是一个东西!

GoF单例 = 自己锁门,你自己写private构造器,自己控制只能创建1个。
Spring单例 = 物业管钥匙,你不管构造器是不是public,Spring容器帮你管理,只创建1个放池子里,谁要给谁引用。
GoF 设计模式单例 (你自己写代码保证) public class President { private static President inst; private President() {} // 私有! public static President getInstance() { if(inst==null) inst = new President(); return inst; } 1. 构造器private,外部不能new 2. 全JVM进程只有1个 3. 你自己保证(代码控制) 4. 类比:国家只有一个总统 Spring 单例 (Spring容器帮你保证) Spring 容器(singleton pool) userService orderService controller 1. 构造器public也行 2. 每个Spring容器里只有1个 3. Spring帮你保证(容器控制) 4. 类比:公司只有一台打印机 5. @Autowired拿到的都是同一个
图2:GoF单例 vs Spring单例 — 自己锁门 vs 物业管钥匙
面试高频坑点:Spring的单例和GoF设计模式的Singleton不是同一个概念
- GoF单例:一个JVM里某个类只有1个实例,靠private构造器+static保证
- Spring单例:一个Spring IoC容器里某个Bean只有1个实例,靠容器管理保证
- 如果你有2个Spring容器(比如父子上下文),同一个Bean可以有2个实例!

3. Spring创建单例的6步流程

公司入职流程:1. 收到offer(扫描@Component)→ 2. 登记花名册(BeanDefinition)→ 3. 人来了但还没配电脑(实例化空壳)→ 4. 配电脑/门禁卡(属性注入)→ 5. 岗前培训+发工牌(初始化+AOP代理)→ 6. 登记到员工系统(放入单例池)
1. 扫描 @Component Spring扫描包路径,发现所有@Service/@Controller/@Repository的类定义 2. 生成 BeanDefinition(类的设计图) 记录类名、scope=singleton、依赖关系、init方法等,还不创建对象! 3. 实例化 new UserService() —— 空壳对象 UserService us = new UserService(); 此时对象已创建,但属性都是null!userMapper=null, redisTemplate=null 4. 属性注入 @Autowired —— 填充依赖 us.userMapper = 容器里的userMapperBean; 把容器中已创建好的其他单例,塞进这个对象的属性里 5. 初始化 @PostConstruct + AOP代理 调用@PostConstruct方法 → BeanPostProcessor前后置处理 → 生成AOP代理对象 如果有@Transactional,这里会替换成CGLIB代理对象! 6. 放入 singletonObjects(单例池) singletonObjects.put("userService", 代理对象); 以后谁@Autowired,就从这里拿,永远是同一个!
图3:Spring创建单例Bean的6步完整流程
DefaultSingletonBeanRegistry.java
📂 spring-beans
📂 support
DefaultListableBeanFactory
DefaultSingletonBeanRegistry
AbstractBeanFactory
// Spring单例池的核心数据结构 — 就是一个ConcurrentHashMap public class DefaultSingletonBeanRegistry { // 🔑 单例池!所有单例Bean都存在这里 private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256); // 获取单例:名字 → 对象 protected Object getSingleton(String beanName) { return this.singletonObjects.get(beanName); } // 注册单例:创建好之后放进去 protected void addSingleton(String beanName, Object singleton) { this.singletonObjects.put(beanName, singleton); } }
源码 singletonObjects 是一个 ConcurrentHashMap<String, Object>,key是Bean名字(如"userService"),value是Bean实例对象。Spring启动时把所有单例Bean创建好放进去,后续@Autowired就从这里get,所以永远是同一个对象

4. 单例的线程安全问题 —— 面试必问

无状态 = 每个人自带纸笔:方法里的局部变量是每个线程栈里的,别人看不见,安全!
有状态 = 桌上共享的笔:成员变量是堆上的,所有线程都能改,两个人同时拿就抢!
UserService(单例) userMapper(注入的,只读)✅ count(成员变量,可变!)❌ 线程1 Tomcat 线程2 Tomcat 线程3 Tomcat ✅ 安全:无状态Bean 方法内局部变量 → 每个线程 在自己的栈里有自己的副本 注入的userMapper → 只读引用 不同线程调用同一方法 各自有自己的局部变量,互不干扰 ❌ 危险:有可变状态 private int count; // 成员变量 线程1: count++ → 读0 改1 线程2: count++ → 也读0 改1 两个线程同时改同一个变量 结果不确定!= 竞态条件
图4:Spring单例的线程安全问题 — 无状态安全,有状态危险
UserService.java
📂 com.example
📂 service
UserService
OrderService
PaymentService
@Service public class UserService { @Autowired private UserMapper userMapper; // ✅ 安全:注入的引用,只读 private int count = 0; // ❌ 危险!可变成员变量 public User getUser(int id) { // ✅ 安全:局部变量在栈上,线程独立 User result = userMapper.selectById(id); return result; } public synchronized void increment() { count++; // 加synchronized可以解决,但影响性能 } }
面试必背:Spring的Bean默认是单例的,不要定义可变的成员变量
- ✅ 安全的:注入的其他Bean引用(@Autowired)、常量(static final)
- ❌ 危险的:private int count、private List<String> cache 等可变成员变量
- 解决方案:用ThreadLocal、方法局部变量、或改为@Scope("prototype")

5. 请求如何使用单例 —— 全链路视角

去公司打印:1. 你拿着文件去打印(HTTP请求)→ 2. 前台指引你去3楼(DispatcherServlet路由)→ 3. 你找到打印机(从单例池取Bean)→ 4. 按打印按钮(调用方法)→ 5. 打印完成(返回响应)。
关键:不管谁来打印,用的都是同一台打印机,打印机不会因为人多就多出来。
HTTP 请求 /api/user/1 Tomcat线程池分配 Thread-3 DispatcherServlet → 路由到 UserController Spring 容器 singletonObjects 单例池 "userController" → 对象@A1B2 "userService" → 对象@C3D4 "userMapper" → 对象@E5F6 所有Bean只创建1次,谁用给谁引用 调用过程 1. 从池中取出 userController@A1B2 2. 调用方法 controller.getUser(1) 3. 方法内this.userService → 指向 userService@C3D4 4. 同一个对象!
图5:HTTP请求 → Tomcat线程 → Spring单例池 → 方法调用全链路

6. @Scope 五种作用域

singleton = 公司1台打印机(默认)
prototype = 每人发1台(每次@Autowired都new)
request = 每个快递单配1台(Web专用,一个请求一个)
session = 每个客户配1台(Web专用,一个会话一个)
application = 整栋楼1台(ServletContext级别)
Scope创建时机数量类比使用场景
singleton容器启动时1个/容器公司公共打印机无状态Service/Controller/Mapper
prototype每次获取时N个每人发一台有状态的Bean
request每次HTTP请求1个/请求每个快递单配一台请求上下文信息
session每次HTTP会话1个/会话每个客户配一台购物车、登录信息
applicationServletContext创建时1个/ServletContext整栋楼1台全局共享配置
OrderService.java — prototype示例
📂 com.example
📂 service
UserService
OrderService
PaymentService
@Service @Scope("prototype") // 每次@Autowired都创建新对象 public class OrderService { // 这个Bean有可变状态,不能用单例! private List<OrderItem> items = new ArrayList<>(); public void addItem(OrderItem item) { items.add(item); } // 每次获取都是全新的对象,互不干扰 // 但注意:prototype Bean不会被Spring自动销毁! // 需要手动管理生命周期 }
singleton中注入prototype会怎样?只会在singleton创建时获取1次prototype!之后prototype不会重新创建。解决方案:
1. ObjectFactory/ObjectProvider:每次调用getObject()获取新实例
2. @Lookup方法:Spring用CGLIB重写方法,每次调用返回新Bean
3. 改为方法参数注入:不@Autowired,改为方法参数让Spring每次传入

7. @Lazy 延迟加载

默认(eager) = 公司开业前就买好打印机放着 → 启动慢,但首次请求快
@Lazy = 等有人要打印才去买 → 启动快,但第一次请求稍慢
LazyConfig.java
📂 com.example
📂 config
LazyConfig
AppConfig
@Configuration public class LazyConfig { @Lazy // 不在启动时创建,第一次使用时才创建 @Bean public HeavyService heavyService() { // 这个Bean初始化很慢(比如连接外部系统) return new HeavyService(); } } @Service public class NormalService { @Lazy // 也可以加在注入点 @Autowired private HeavyService heavyService; // 注入的是代理对象,首次调用方法时才真正创建 }

8. 面试速答

Spring的Bean默认是单例的吗?为什么?
是的,默认scope=singleton。因为大部分Bean是无状态的Service/Controller/Mapper,不需要每次请求都创建新对象,单例可以节省内存、提高性能。
Spring单例和GoF设计模式的单例有什么区别?
GoF单例靠private构造器+static自己保证全JVM只有1个;Spring单例靠IoC容器管理,每个Spring容器里只有1个。如果有2个Spring容器(如父子上下文),同一个Bean可以有2个实例。Spring单例的构造器可以是public的。
Spring单例Bean线程安全吗?
取决于Bean是否有可变状态。无状态Bean(只有方法内局部变量和注入的只读引用)是线程安全的;有可变成员变量的Bean不是线程安全的。解决方案:①不定义可变成员变量 ②使用ThreadLocal ③改为prototype scope ④加synchronized(不推荐,影响并发)
singleton Bean里注入prototype Bean会怎样?
prototype Bean只会在singleton创建时注入1次,之后不会重新创建。因为singleton只创建1次,@Autowired也只执行1次。解决方案:①用ObjectProvider<T>,每次getObject()获取新实例 ②用@Lookup方法注入 ③改为方法参数注入
Spring什么时候创建单例Bean?
默认在容器启动时(ApplicationContext.refresh()阶段)就创建所有单例Bean(pre-instantiation),不是第一次请求时。这样启动慢但运行快。加@Lazy可以改为首次使用时才创建。
为什么Controller/Service/Mapper都可以用单例?
因为它们都是无状态的:Controller调用Service,Service调用Mapper,方法内的局部变量在栈上是线程独立的,注入的依赖引用是只读的。真正有状态的数据(请求参数、数据库连接)不在Bean的成员变量里,而是通过方法参数传递或由MyBatis的SqlSession管理。
Spring怎么保证单例只创建1个?
DefaultSingletonBeanRegistry内部维护一个ConcurrentHashMap<String, Object>叫singletonObjects。创建Bean前先get检查是否存在,不存在才创建,创建后put进去。而且创建过程用synchronized加锁,保证不会并发创建。
@Lazy有什么用?有什么坑?
@Lazy让Bean在首次使用时才创建,而不是启动时。好处是加快启动速度,特别是有些Bean初始化很重(连接外部系统)。坑:①首次请求会慢 ②如果Bean有配置错误,启动时不会报错,到运行时才暴露 ③循环依赖时@Lazy可以打破循环(注入代理对象)
单例Bean销毁时会怎样?
Spring容器关闭时会调用所有单例Bean的@PreDestroy方法和DisposableBean.destroy(),释放资源(关闭连接池、清理缓存等)。但prototype Bean Spring不负责销毁,需要自己管理。
循环依赖和单例有什么关系?
Spring只对singleton Bean解决循环依赖(三级缓存),prototype Bean循环依赖会直接报错。因为singleton是容器管理完整生命周期,可以通过"提前暴露半成品"来打破循环;而prototype每次都创建新的,无法缓存。

📋 速记口诀

Spring单例 = 公司公共打印机
🔹 只创建1个,放singletonObjects池子里
🔹 谁用给谁引用(@Autowired),永远是同一个
🔹 无状态Bean安全(局部变量+只读引用)
🔹 有可变成员变量 = 危险!多线程抢改
🔹 GoF单例 ≠ Spring单例(private构造器 vs 容器管理)
🔹 singleton里注入prototype = 只注入1次!
🔹 @Lazy = 等有人用才创建(加快启动)
🔹 循环依赖只对singleton解决(三级缓存)

核心三原则速记

Spring三原则:① Bean默认单例,不要在Bean中存请求级状态 → ② @Transactional不能在类内部调用中生效 → ③ 循环依赖能用但不推荐