恒美微站 Logo 恒美微站
  • 首页
  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心
  • 联系我们

Spring Singleton与单例模式的区别及线程安全实战

  • 首页
  • 资讯中心
  • /
  • Spring Singleton与单例模式的区别及线程安全实战

相关资讯

并查集判环经典题:P1347格子游戏的图论原理与C++实现 2026/10/6 17:03:22
基于Claude Code的营销自动化:SEO与CRO技能化实战指南 2026/10/6 17:03:22
兼顾查重率与AIGC率:Paperzz论文降重方案全解析 2026/10/6 17:03:22

最新资讯

context-mode 实战:Neovim 滚动阅读不迷路的上下文显示方案
AI编码代理实操:让大模型操控GUI并支持MCP,打包单文件
AI小说创作助手实战:智能拆书、起名与润色提示词工程
大模型智能体如何安全落地?沙箱隔离与纵深防御实战
Context-Mode 实战:AI 应用上下文管理与优化全方案
Windows本地部署MinerU 4.0:RAG文档预处理与批量解析实战

今日推荐

2026 AI 开发全家桶落地指南:TaoToken 统一 Key 打通 IDE 插件、Agent 与自动化代码审查全链路配置实测
MR25H40CDF+STM32F031C6工业级高可靠数据存储方案
MRAM+STM32工业断电数据保全实战指南

本周热门

MR25H40CDF + PIC18F65K40:工业记录仪高可靠存储实战
基于STM32的数控恒压恒流电源设计:从硬件到PID调参全解析
LT9211 MIPI重定时器原理与双路扇出实战指南

本月精选

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证
2026 大模型集体涨价:用 Python 做企业 Token 成本测算与选型避坑(附配置)

Spring Singleton与单例模式的区别及线程安全实战

发布时间:2026/10/6 17:03:22
Spring Singleton与单例模式的区别及线程安全实战 讲个真实经历。有次面试候选人简历上写着“熟悉Spring”我顺手问了一句“Spring里默认的bean是singleton那这个singleton是单例模式吗线程安全吗”对方愣了几秒然后开始背单例模式的双重检查锁。那一刻我就知道很多人把这两件事混在一起了。后来我自己带团队做项目评审发现不少开发者在Service里写了一个HashMap当缓存结果生产环境偶尔出现数据错乱排查半天却发现Bean明明只有一个实例怎么还会互相干扰其实答案早就藏在Spring的设计里只是我们没仔细琢磨。这篇东西写给谁刚接触Spring的Java新人或者做了几年开发但没深究过容器原理的同行。你能搞清楚三件事Spring的singleton到底是什么意思它和经典单例模式差在哪以及遇到线程安全问题到底该怎么办。不需要你背源码但我会把关键源码位置和常见坑都点出来。1. 先搞清楚Spring的singleton到底是个什么东西1.1 不是GoF单例而是“作用域”先明确一个概念Spring里说的singleton和《设计模式》里那本GoF书上的单例模式完全不是一回事。GoF单例模式的核心是“类本身只允许有一个实例”。它通常把构造方法设为private然后通过一个静态方法返回同一个实例。这个控制权在类自己手里谁也绕不过去。Spring的singleton说的是“Bean的作用域”。一个Bean定义好之后Spring容器启动时或者第一次获取时会创建这个Bean的实例然后把它放在容器内部的一个Map里。以后每次通过getBean或者依赖注入获取拿到的都是同一个对象。这个控制权在容器手里类本身根本不需要做任何事——不需要私有化构造器不需要静态方法就是一个普通的Java类照样可以被Spring当成单例Bean来管理。两者最大的区别我用大白话说就是GoF单例是“我这一辈子就只有一个我”谁来要都一样。Spring singleton是“容器里只有一个我”但如果再起一个容器那就是另一个我了。如果你在代码里手动new了两个Spring容器每个容器都可以各自创建一个同一个类的singleton bean。这在GoF单例模式下是不可能的同一个ClassLoader下但Spring完全允许。1.2 从源码角度理解singleton bean的实例化很多人一听到源码就头大其实Spring的Bean存储机制特别直白。默认的Bean工厂叫DefaultSingletonBeanRegistry它里面有个字段private final MapString, Object singletonObjects new ConcurrentHashMap(256);这个Map的key是bean名字value就是那个单例对象。你每次getBean(userService)Spring会先去这个Map里查。如果存在直接返回如果不存在就创建并放进去。这就是“singleton”最朴素的实现。不过光说这个不够因为Spring还处理了循环依赖的问题。面试题里常说的“三级缓存”其实是在这个类里定义了另外几个MapsingletonObjects一级缓存存放完整的单例Bean。earlySingletonObjects二级缓存存放提前暴露的早期Bean引用还没完成属性填充。singletonFactories三级缓存存放ObjectFactory可以生成早期Bean。这套机制解决的是A依赖B、B又依赖A的情况。但注意三级缓存的线程安全经常被忽略。它在并发获取Bean时有特殊的处理比如getSingleton方法里用了synchronized块防止两个线程同时创建同一个Bean导致重复实例化。但这是“Bean创建过程”的线程安全跟你Bean内部状态的线程安全是两码事。2. Spring singleton线程安全吗——先给结论再说为什么2.1 直接结论不等于线程安全我们用一句话回答面试题Spring的singleton只保证一个容器里Bean实例是唯一的不保证这个Bean是线程安全的。“唯一”和“安全”是两个维度。线程安全指的是在多线程访问下对象的行为始终正确不会出现数据错乱。一个Bean无论是不是单例只要它内部有可变的共享状态在多线程下都有可能出问题。举个最典型的例子。假设有个计算器BeanComponent public class CounterService { private int count 0; public void increment() { count; } public int getCount() { return count; } }count看着简单但它实际上包含三步读取count的值加1写回。多线程同时执行时可能两个线程都读到了0然后各自加1最后写回1而不是期望的2。这就是线程不安全。你可以做个实验用100个线程每个线程循环调1000次increment()理论上最终结果应该是100000但实际跑出来经常是九万多。这就是Spring单例Bean在有状态时的天然问题。2.2 为什么很多人误以为安全因为无状态Bean才是主流我见过很多开发者理直气壮地说“Spring的单例Bean一直都是这么用的从来没出过线程问题。”他说得没错但那是因为他写的Bean大多是无状态的。什么是无状态就是Bean内部没有可变的成员变量。比如一个典型的ServiceService public class OrderService { private final OrderMapper orderMapper; // 依赖注入本身线程安全 public Order createOrder(OrderDto dto) { // 局部变量都在方法内部不会跨线程共享 return orderMapper.insert(dto); } }这个Bean里只有一个orderMapper引用它本身是Spring管理的另一个singleton Bean而且通常没有内部状态Mapper一般是接口代理。所有数据都通过参数传递或者通过局部变量计算不写入Bean自己的字段。这种Bean天然线程安全因为多线程调用同一个方法时各自独立的变量不会互相干扰。Spring生态里大部分Service、Controller、Repository都是这种无状态Bean。大家用得顺手了自然觉得“Spring单例很安全”。但实际上安全的是你的写法不是Spring的单例机制。2.3 有状态Bean该怎么处理如果你的Bean确实需要保存状态有几种常见办法办法一用ThreadLocal每个线程一份独立副本天然隔离。常见场景是保存用户请求上下文比如登录用户信息。Component public class UserContextHolder { private static final ThreadLocalUser USER_HOLDER new ThreadLocal(); public static void set(User user) { USER_HOLDER.set(user); } public static User get() { return USER_HOLDER.get(); } public static void clear() { USER_HOLDER.remove(); } }但注意ThreadLocal使用完一定要remove()否则在线程池场景下会内存泄漏因为线程复用时旧数据还在。办法二用并发安全的数据结构比如ConcurrentHashMap、AtomicInteger。Component public class CounterService { private AtomicInteger count new AtomicInteger(0); public void increment() { count.incrementAndGet(); } public int getCount() { return count.get(); } }AtomicInteger就是热搜词里那个“atomicinteger线程安全吗”的经典答案——它基于CAS实现线程安全适合高并发计数器。办法三加锁。用synchronized或ReentrantLock保护临界区。但加了锁会降低并发性能尽量别用在热点路径上。办法四把状态外置。比如放到数据库、Redis里。这类系统天然支持并发控制单体Bean内部就不用操心状态了。我个人的原则是优先写无状态Bean。如果必须有状态先把状态能不能放外部缓存或数据库里不行再用ThreadLocal再不行才考虑加锁。这个顺序能省掉很多线上事故。3. Spring singleton与单例模式的详细对比3.1 维度对比看这张表就够对比维度Spring SingletonGoF单例模式控制方Spring容器类自身构造器通常是publicSpring负责调用private外部不能new实例获取Autowired或getBean静态方法如getInstance()唯一性范围每个Spring容器内唯一整个JVMClassLoader内唯一生命周期容器管理容器关闭时销毁类加载时初始化或懒加载全程存活可测试性好可以替换Mock差静态方法难Mock默认应用场景Spring管理的一切Bean连接池、配置类等需要全局唯一的工具类线程安全不保证看Bean有没有状态同样不保证需要自己写同步看到没两者在很多层面是截然相反的。GoF单例强调“如何保证唯一”而Spring singleton强调“容器统一管理”。Spring完全可以做到容器内唯一但你不需要在类上写任何单例逻辑。3.2 两者共存会怎样——一个类既是Spring Bean又是GoF单例有人会问我把一个GoF单例类注册成Spring Bean会有问题吗技术上没问题但不推荐。比如public class ConfigManager { private static final ConfigManager INSTANCE new ConfigManager(); private ConfigManager() {} public static ConfigManager getInstance() { return INSTANCE; } }如果我把它加上ComponentSpring容器会通过反射调用私有构造器吗实际上Spring对私有构造函数处理比较麻烦默认情况下它会用无参构造器反射创建但私有构造器会报错。除非在Component类上手动指定Autowired或者使用工厂方法否则很难注册成功。即便你通过静态工厂注入成功也违背了Spring的初衷。因为GoF单例的实例是没办法被Mock替换的Spring单元的MockBean那一套在它面前失效。而且它自己控制了生命周期Spring容器关闭时也管不了它容易造成资源泄漏。我见过一些老项目把数据库连接池直接写成GoF单例然后又用Spring管理一遍结果出现了两个连接池实例一个在Spring容器里一个在静态变量里后来排查了半宿。现在基本都是用Spring的Configuration加Bean来管理第三方资源让容器接管生命周期。3.3 常见的误区static变量、prototype作用域一锅乱炖误区一static变量就是单例有同学以为给类的字段加个static就成了单例。准确说static变量是“类级别共享”在同一个ClassLoader下只有一个副本但它跟Spring单例没有任何关系。而且static变量常常比Bean实例更难管理因为Spring的Bean还受容器生命周期影响static变量是全局的谁都能改更容易出现线程安全问题和内存泄漏。误区二Spring只有singleton这一种作用域远不止。Spring Bean支持prototype、request、session、application等作用域。prototype每次获取Bean都是新实例。适合有独享状态的组件但不被Spring管理完整生命周期。request每个HTTP请求一个实例只在Web应用里有效。session每个用户会话一个实例。application整个ServletContext一个实例相当于全局单例。在Spring Boot中可以通过Scope(prototype)指定Component Scope(prototype) public class TaskContext { private ListString steps new ArrayList(); }但注意prototype Bean如果被一个singleton Bean依赖注入那这个prototype Bean实际上只会被注入一次后面一直用的是同一个实例完全起不到“每次新创建”的效果。这个问题我们放到第5节讲。4. 线程安全实战怎么判断你的Bean是否有状态4.1 快速判断方法盯住Bean的字段在代码评审时我有个习惯拿到一个类先看非静态字段。没有非静态字段只依赖注入其他Bean且那些Bean也无状态基本线程安全。有非静态字段但是final且不可变比如String、基本类型包装类或者用Collections.unmodifiableList包裹的集合只要引用不变也是安全的。有非静态可变字段比如HashMap、ArrayList、自定义对象危险需要看这些字段会不会被多线程写入。有ThreadLocal字段安全但要注意清理。还有一种隐蔽情况虽然你的类没有字段但注入进来的某个Bean内部有状态。比如你注入了一个第三方库提供的SimpleDateFormat它是线程不安全的。这种问题很容易漏因为源码不在你手里。遇到这种情况要么不用它要么用ThreadLocalSimpleDateFormat包一层要么直接改用Java 8以后的时间API。4.2 案例演示一个带缓存的Service并发下怎么翻车的假设我们写一个用户查询服务为了性能加了个本地缓存Component public class UserService { private final MapString, User cache new HashMap(); public User getUser(String id) { if (cache.containsKey(id)) { return cache.get(id); } User user queryFromDb(id); cache.put(id, user); return user; } private User queryFromDb(String id) { // 模拟慢查询 try { Thread.sleep(50); } catch (InterruptedException e) { } return new User(id, name- id); } }看起来没毛病实际上并发时会有两个问题HashMap本身线程不安全。多个线程同时put时有可能导致链表成环CPU直接飚到100%。这是个经典问题了JDK 7时期特别容易触发。缓存穿透。两个线程同时发现缓存没有同时去查库同时写缓存数据库中压力翻倍。修复方案之一是改成ConcurrentHashMapprivate final MapString, User cache new ConcurrentHashMap();但ConcurrentHashMap也不能解决“缓存穿透”中的重复查询问题因为判断containsKey和put之间还是存在时间差。更稳妥的做法是用computeIfAbsent它会保证同一个key只有一个线程执行查询逻辑public User getUser(String id) { return cache.computeIfAbsent(id, this::queryFromDb); }这个方法是原子的允许同时传同一个key但只有一个线程执行计算其他线程等待结果。实际用下来很顺。需要注意的是computeIfAbsent如果value的创建过程里有递归调用同一个Map会出问题死循环或报错需要格外小心。我遇到过一例不过具体情况不常见写代码时留意就行。4.3 在Spring Boot中真正需要非单例的场景假设你要在Controller里保存一个“仅供本次请求使用”的上下文对象比如记录请求开始时间、操作日志、临时变量。如果用singleton Bean得用ThreadLocal但去清理比较麻烦更优雅的是用request作用域Component Scope(value request, proxyMode ScopedProxyMode.TARGET_CLASS) public class RequestContext { private long startTime System.currentTimeMillis(); private String requestId; // getter/setter }proxyMode这一行很关键。因为Controller本身是singleton它依赖注入RequestContext时Spring会注入一个代理对象。每次调用代理时代理会找到当前HTTP请求对应的真实实例从而保证同一个请求内拿到的是同一个对象不同请求之间互不干扰。如果不加proxyMode直接注入request作用域的Bean到singleton Controller里启动时可能直接报错或者永远拿到同一个固定实例反而不安全。session作用域同理适合放购物车数据。但这两种作用域只适用Web环境如果在非Web环境下启动会有问题。5. 常见问题与排查技巧5.1 面试高频追问singleton Bean注入prototype Bean的坑项目里有个常见需求一个单例的Service希望每次调用时都用一个全新的PrototypeWorker状态。很多人写成这样Service public class ParentService { Autowired private Worker worker; // Worker是Scope(prototype) public void doWork() { worker.execute(); } }结果发现虽然Worker标记了prototype但ParentService里的worker对象始终是同一个。原因是Spring在创建ParentService这个singleton时就已经把当时创建好的Worker注入进去了。之后你每次调用ParentService用的都是那个“当初的”Worker跟prototype的名头完全矛盾。解决办法有三种用ObjectProviderWorker。Autowired private ObjectProviderWorker workerProvider; public void doWork() { Worker w workerProvider.getObject(); w.execute(); }每次调用getObject()Spring都会从容器重新拿一个prototype实例保证每次都是全新的。用Lookup方法。Service public abstract class ParentService { Lookup protected abstract Worker getWorker(); public void doWork() { getWorker().execute(); } }Spring通过CGLIB生成子类来重写这个方法每次调用都会返回新Bean。这个写法省了ObjectProvider但类不能是finalSpring Boot里用起来能接受。注入ApplicationContext手动getBean(Worker.class)。最灵活但依赖容器一般最后选。这三种方法都能解决个人最推荐ObjectProvider代码直白且不易出错也是Spring官方推荐的思路。5.2 状态污染排查技巧怎么定位是不是单例Bean的问题假如你线上出了诡异Bug用户A下单结果用户B收到了A的购物车。第一反应就是怀疑有状态Bean共享。排查顺序我建议这样走看报错堆栈找到涉及业务逻辑的类。如果多个线程同时操作同一个类重点关注这个类的非静态字段。在字段写入处打断点或打日志记录当前线程ID和值。如果多个线程都进入同一个方法修改同一个字段实锤了。检查这个Bean的scope。默认都是singleton不要在没验证的情况下假设它是prototype。检查ThreadLocal的清理时机。很多状态污染其实是ThreadLocal没清理导致线程池复用时旧数据被下一个请求读到了。我之前帮同事查过一个问题一个支付回调服务把订单号存在ThreadLocal里但忘记在finally里调用remove()。结果Tomcat线程池复用线程被另一个请求拿到时读取的还是上一个请求的订单号导致对账全错。这种问题光看代码逻辑看不出毛病得看线程调度才知道。所以凡是用ThreadLocal的地方建议封装成一个清理工具类在请求结束时统一清理。Spring的HandlerInterceptor里的afterCompletion方法就是干这个的别嫌麻烦。5.3 分布式环境下的扩展本地锁没用怎么办如果服务只部署了一个实例用synchronized或者ReentrantLock保护本地状态没问题。但一旦上了多实例你加的锁只对当前JVM有效另一个实例完全不知道锁的存在。这时候就得用分布式锁比如基于Redis的SETNX或者用数据库乐观锁。举个场景一个发号器Bean本地维护一个序号单机没问题。部署两台后两台都从0开始发就重复了。解决办法是把号段分配给每个实例比如A用1-1000B用1001-2000或者直接用Redis原子自增。这些都是“Spring singleton线程安全”在单机之外的延伸本质都是要保证共享资源的并发一致性。不过这个是另一个大话题了做微服务的时候会经常碰到。现阶段先记住一条单例Bean的状态管理作用域不止一个JVM系统边界一变原来的“安全”就更脆弱了。最后说点实在的我在实际项目里踩过几次坑之后总结了一句口头禅“真正让Spring单例安全的不是Spring是你不给它写状态的机会。”大部分业务Service都应该长成“无状态计算器”的样子输入参数调用其他服务返回结果。所依赖的东西靠注入注入进来的Bean又都是无状态的整个调用链就是干净的。如果你非要写有状态的Bean那就在设计阶段明确三点这个状态属于谁请求会话线程谁来维护生命周期并发访问时允许多少线程同时写。想清了这三点再去选ThreadLocal、原子类还是分布式锁基本不会错。这篇聊得已经很细了从源码到实战再到面试题里的坑。以后再有人问你“Spring singleton线程安全吗”你就反问他你先告诉我这个Bean里有没有状态字段。你俩的眼神大概都会变得有点东西。

关于恒美微站

恒美微站专注于为个体商户、工作室提供极简自助建站服务,让每个人都能轻松拥有专业网站。

快速链接

  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心

服务项目

  • 可视化建站
  • 拖拽编辑
  • 主题定制
  • SEO 优化
  • 网站托管

联系方式

  • 📍 地址:北京市朝阳区建国路 88 号
  • 📞 电话:400-888-8888
  • ✉️ 邮箱:info@hmyw.cn
  • 🕐 时间:周一至周日 9:00-18:00

© 2024 恒美微站 hmyw.cn 版权所有 | 京 ICP 备 12345678 号