spring单例bean线程安全问题讨论

不可变对象

final 和不可变对象的关系

final 修饰的基本类型 ,只能读不能写,天然类型安全

final 修饰的对象 ,只是对象引用不变,对象属性可以变,没有实现不可变对象

借助 final 真正实现不可变对象

  • 类使用 final 修饰(禁止子类继承,重写父类方法,添加逻辑,破坏不可变性)
  • 所有成员变量必须是 private final
  • 只在构造方法中赋值,不提供任何 setter
  • 引用类型成员,必须做防御性拷贝
java 复制代码
public User(List<String> tags) {
    // 构造时:拷贝一个新集合,不直接用外部引用
    this.tags = List.copyOf(tags);
}

返回也要返回不可变视图 ,由构造时通过 copyOf 保证不可变,或者返回时构造不可变视图(不推荐)

List.of() 和 unmodifiableList

在 Java 中,List.of() 和 Collections.unmodifiableList() 都会让列表拒绝写操作,但它们的语义完全不同。

List.of() 是 Java 9 引入的方式,用于从零构造一个结构不可变的列表 :该列表与任何外部集合无共享引用,严格拒绝 null ,并且拥有极简的内存布局和最佳性能。需要注意的是,它只保证列表结构不可变(不能增、删、替换元素引用),并不对元素本身做深拷贝:

java 复制代码
StringBuilder sb = new StringBuilder("hello");
List<StringBuilder> list = List.of(sb);
sb.append(" world");
System.out.println(list.get(0)); // "hello world"  元素内部状态仍可被外部修改

相比之下,Collections.unmodifiableList() 创建的是对现有列表的只读视图 ,它仅拒绝通过视图的写操作,不做任何拷贝。若底层源列表仍然可变,视图中的内容会随之变化:

java 复制代码
List<String> source = new ArrayList<>(List.of("a", "b"));
List<String> view = Collections.unmodifiableList(source);
source.add("c");
System.out.println(view); // [a, b, c]  视图跟着变了

选用建议:若目标是构建长期不变的常量或不可变对象的内部字段,优先使用 List.of()(从零构建)或 List.copyOf()(从已有集合拷贝)。unmodifiableList() 更适合在对外暴露内部状态时包一层只读壳,但要清楚它不提供真正的隔离。

讨论 Spring Beans 线程安全问题

重点在于是否存在共享状态,而非 Spring 容器本身。

Spring 的单例 Bean 只创建一个实例,被所有请求共享。如果该实例包含可变状态(成员变量),多线程并发修改时就会产生竞争条件。

1. 线程安全的场景:无状态 Bean

如果 Bean 没有实例变量,或只有不可变(final)变量(final 引用 + 引用的对象本身也是无状态/线程安全的),就是线程安全的。

java 复制代码
@Service
public class UserService {
    // 无成员变量,或只有不可变变量
    private final UserRepository userRepository; // final,线程安全

    public User findUser(Long id) {
        return userRepository.findById(id); // 局部变量,线程安全
    }
}

2. 线程不安全的场景:有状态 Bean

如果 Bean 包含可变成员变量,多个线程同时修改就会出问题。

java 复制代码
@Service
public class UnsafeService {
    private int counter; // 可变状态,线程不安全!

    public void increment() {
        counter++; // 非原子操作,并发时结果不确定
    }
}

如何避免

1. 使用无状态 Bean

2. 使用线程安全类(AtomicInteger 等)

3. 使用 ThreadLocal(线程本地存储)

java 复制代码
@Service
public class RequestContextService {
    private final ThreadLocal<String> currentUser = new ThreadLocal<>();

    public void setUser(String user) {
        currentUser.set(user);
    }

    public String getUser() {
        return currentUser.get();
    }

    public void clear() {
        currentUser.remove(); // 请求结束后必须清理!
    }
}

4. 方法 synchronized 修饰(不推荐)

相关推荐
thefool1122666 小时前
TCP 1
java·1024程序员节
金銀銅鐵9 小时前
[Java] 用 GUI 展示 EnumSet 增减元素时的内部变化(下)
java·后端·ai编程
AI人工智能+电脑小能手9 小时前
一次请求打几个下游,这个Java并行任务工具类6个方法按序聚合
java·线程池·并发编程·1024程序员节·批量处理·并行任务
paopaokaka_luck11 小时前
心理咨询管理系统(WebSocket实时沟通、基于用户的协同过滤、 ECharts多维分析、 预约业务流程、预约校验与定时处理)
java·vue.js·spring boot·mysql·spring·echarts·1024程序员节
2301_322414280411 小时前
springboot农产品交易平台的设计与实现08055-计算机毕设 原创(免费领源码+部署教程+开发环境)
java·spring boot·redis·mysql·tomcat·maven·mybatis
考虑考虑12 小时前
JDK26的JEP 500属性
java·后端·java ee
LMT流氓兔12 小时前
第五单元——第十二课:把脚本整理成项目
java·数据库·python·langchain·1024程序员节
yuniko-n12 小时前
【JUC】四种内存屏障示例
java·1024程序员节
雷一言说12 小时前
Spring AI 2.0 Tool Calling 实战:用一个订单查询跑通整个调用链
spring
念何架构之路13 小时前
Go reflect 包:结构体反射 —Field、Tag 与 VisibleFields
java·数据库·golang