对Spring中bean线程安全的讨论
目录
- 1、Bean状态介绍
- 1.1、有状态对象
- 1.2、无状态对象
- 2、Bean作用域
- 3、线程安全:
- 3.1、bean的分类
- 3.2、bean的安全
- 1、@Controller相关
- 2、@prototype注解
- 3、静态变量
- 4、ThreadLocal
- 4.1、概念
- 4.2、优点
- 4.3、原理
- 4.4、注意
- 4.5、使用场景
- 5、解决方案
- 总结
Spring容器中的Bean是否线程安全,容器本身并没有提供Bean的线程安全策略,因此Spring容器中的Bean本身不具备线程安全的特性,但是具体要结合具体的scope、静态变量、常量、成员变量等多种属性去研究。
1、Bean状态介绍
1.1、有状态对象
有实例变量的对象,即每个用户最初都会得到一个初始的bean,可以保存数据,是非线程安全的。
每个用户有自己特有的一个实例,在用户的生存期内,bean保持了用户的信息,即“有状态”;一旦用户灭亡(调用结束或实例结束),bean的生命期也告结束。
代码示例:
@Service public class Counter { private int count = 0; // 有状态:保存实例变量 public void increment() { count++; // 非原子操作,线程不安全 } public int getCount() { retur编程n count; } }
- 多个线程调用 increment() 时,count++操作可能因指令重排或并发写入导致数据不一致。
1.2、无状态对象
没有实例变量的对象,不能保存数据,是不变类,是线程安全的。
- bean一旦实例化就被加进会话池中,各个用户都可以共用。即使用户已经消亡,bean 的生命期也不一定结束,它可能依然存在于会话池中,供其他用户调用。
- 由于没有特定的用户,那么也就不能保持某一用户的状态,所以叫无状态bean。但无状态会话bean 并非没有状态,
- 如果有自己的属性(变量),那么这些变量就会受到所有调用它的用户的影响。
代码示例如下:
@Service public class Calculator { // 无状态:不保存任何实例变量 public int add(int a, int b) { return a + b; } }
- 多个线程调用add(1,2)时,结果不会互相影响。
两者的区别和联系:
2、Bean作用域
bean的生命周期如下所示:
实例化--->设置属性--->初始化--->销毁
Spring 的 bean 作用域(scope)类型:
1、singleton:单例,默认作用域。
- 优点: 节省内存,因为只存在一个实例。
- 缺点: 由于多个线程可能共享同一个实例,需要格外注意线程安全(非线程安全的状态字段可能导致问题)
2、prototype:原型,每次创建一个新对象。
3、request:请求,每次Http请求创建一个新对象,适用于WebApplicationContext环境下。
4、session:会话,同一个会话共享一个实例,不同会话使用不用的实例。
5、global-session:全局会话,所有会话共享一个实例。
3、线程安全:
从单例与原型Bean分别进行说明。
3.1、bean的分类
1、原型Bean
对于原型Bean,每次创建一个新对象,也就是线程之间并不存在Bean共享,自然是不会有线程安全的问题。
2、单例Bean
对于单例Bean,所有线程都共享一个单例实例Bean,因此是存在资源的竞争。
3.2、bean的安全
1、@Controller相关
可以这样理解:
如果单例Bean,是一个无状态Bean,在线程中的操作不会对Bean的成员执行查询以外的操作,那么这个单例Bean是线程安全的。
比如Spring mvc 的 Controller、Service、Dao等,这些Bean大多是无状态的,默认情况下@Controller没有加上@Scope,默认Scope就是默认值singleton,单例的 ,系统只会初始化一次 Controller 容器,只关注于方法本身。
但是,如果每次请求的都是同一个 Controller 容器里面的非线程安全的字段,那么就不是线程安全的。
代码示例:
@RestController public class TestController { //非线程安全的字段 private int var = 0; @GetMapping(value = "/test_var") public String test() { System.out.println("普通变量var:" + (++var)); return "普通变量var:" + var ; } } 输出: 普通变量var:1 普通变量var:2 普通变量var:3
修改了作用于改为:prototype
每个请求都单独创建一个 Controller 容器,所以各个请求之间是线程安全的。
@RestController @Scope(value = "prototype") // 加上@Scope注解,有2个取值:单例-singleton 多实例-prototype public class TestController { private int var = 0; @GetMapping(value = "/test_var") public String test() { System.out.println("普通变量var:" + (++var)); return "普通变量var:" + var ; } } 输出: 普通变量var:1 普通变量var:1 普通变量var:1
总结
1、@Controller/@Service 等容器中,默认情况下,scope值是单例- singleton 的,是线程不安全的。
2、尽量不要在 @Controller/@Service 等容器中定义静态变量,不论是单例( singleton )还是多实例( prototype )都是线程不安全的。
3、默认注入的Bean对象,在不设置scope的时候也是线程不安全的。
4、一定要定义变量的话,用 ThreadLocal 来封装,这个是线程安全的。
2、@prototype注解
@Scope 注解的 prototype 实例一定就是线程安全的吗?
答案是否定的。上面已经解释过了,需要根据多方位去考量。
@RestController @Scope(value = "prototype") // 加上@Scope注解,有2个取值:单例-singleton 多实例-prototype public class TestController { private int var = 0; //只会初始化一次,因此也非线程安全的变量 private static int staticVar = 0; @GetMapping(value = "/test_var") public String test() { System.out.println("普通变量var:" + (++var)+ "---静态变量staticVar:" + (++staticVar)); return "普通变量var:" + var + "静态变量staticVar:" + staticVar; } } 输出: 普通变量var:1---静态变量staticVar:1 普通变量var:1---静态变量staticVar:2 普通变量var:1---静态变量staticVar:3
总结:线程安全在于怎样去定义变量以及 Controller 的配置。
示例:
config里面自己定义的Bean: User
@Configuration public class MyConfig { @Bean public User user(){ javascript return new User(); } }
@RestController @Scope(value = "singleton") // prototype singleton public class TestController { private int var = 0; // 定义一个普通变量 private static int staticVar = 0; // 定义一个静态变量 @Value("php${test-int}") private int testInt; // 从配置文件中读取变量 ThreadLocal<Integer> tl = new ThreadLocal<>(); // 用ThreadLocal来封装变量 @Autowired private User user; // 注入一个对象来封装变量 @GetMapping(value = "/jstest_var") public String test() { tl.set(1); System.out.println("先取一下user对象中的值:"+user.getAge()+"===再取一下hashCode:"+user.hashCode()); user.setAge(1); System.out.println("普通变量var:" + (++var) + "===静态变量staticVar:" + (++staticVar) + "===配置变量testInt:" + (++testInt) + "===ThreadLocal变量tl:" + tl.get()+"===注入变量user:" + user.getAge()); return "普通变量var:" + var + ",静态变量staticVar:" + staticVar + ",配置读取变量testInt:" + testInt + ",ThreadLocal变量tl:" + tl.get() + "注入变量user:" + user.getAge(); } }
输出:
先取一下user对象中的值:0===再取一下hashCode:241165852
普通变量var:1===静态变量staticVar:1===配置变量testInt:1===ThreadLocal变量tl:1===注入变量user:1
先取一下user对象中的值:1===再取一下hashCode:241165852
普通变量var:2===静态变量staticVar:2===配置变量testInt:2===ThreadLocal变量tl:1===注入变量user:1
先取一下user对象中的值:1===再取一下hashCode:241165852
普通变量var:3===静态变量staticVar:3===配置变量testInt:3===ThreadLocal变量tl:1===注入变量user:1
在单例模式下 Controller 中只有用 ThreadLocal 封装的变量是线程安全的。可以看到3次请求结果里面只有 ThreadLocal 变量值每次都是从 0+1=1 的,其他的几个都是累加的,而 user 对象呢,默认值是0,第二交取值的时候就已经是1了,关键它的 hashCode 是一样的,说明每次请求调用的都是同一个 user 对象。
TestController 上的 @Scope 注解的属性改一下改成多实例的: @Scope(value = "prototype") ,其他都不变,再次请求,结果如下:
public class MyConfig { @Bean @Scope(value = "prototype") public User user(){ return new User(); } }
@RestController @Scope(value = "prototype") // prototype singleton public class TestController { private int var = 0; // 定义一个普通变量 private static int staticVar = 0; // 定义一个静态变量 @Value("${test-int}") private int testInt; // 从配置文件中读取变量 ThreadLocal<Integer> tl = new ThreadLocal<>(); // 用ThreadLocal来封装变量 @Autowired private User user; // 注入一个对象来封装变量 @GetMapping(value = "/test_var") public String test() { tl.set(1); System.out.println("先取一下user对象中的值:"+user.getAge()+"===再取一下hashCode:"+user.hashCode()); user.setAge(1); System.out.println("普通变量var:" + (++var) + "===静态变量staticVar:" + (++staticVar) + "===配置变量testInt:" + (++testInt) + "===ThreadLocal变量tl:" + tl.get()+"===注入变量user:" + user.getAge()); return "普通变量var:" + var + ",静态变量staticVar:" + staticVar + ",配置读取变量testInt:" + testInt + ",ThreadLocal变量tl:" + tl.get() + "注入变量user:" + user.getAge(); } }
先取一下user对象中的值:0===再取一下hashCode:1612967699
普通变量var:1===静态变量staticVar:1===配置变量testInt:1===ThreadLocal变量tl:1===注入变量user:1先取一下user对象中的值:0===再取一下hashCode:985418837
普通变量var:1===静态变量staticVar:2===配置变量testInt:1===ThreadLocal变量tl:1===注入变量user:1先取一下user对象中的值:0===再取一下hashCode:1958952789
普通变量var:1===静态变量staticVar:3===配置变量testInt:1===ThreadLocal变量tl:1===注入变量user:1
3、静态变量
静态变量的生命周期由 JVM 管理,与 Spring 无关。所有实例(单例或原型)共享同一个静态变量。
@Component public class MyService { private static int count = 0; // 静态变量 public void increment() { count++; // 多线程环境下可能出问题 } }
在 Spring 中,无论是单例(Singleton)作用域还是原型(Prototype)作用域的 Bean,只要在类中定义了静态变量(static 变量),都可能存在线程安全问题。
总结:多实例模式下普通变量,取配置的变量还有 ThreadLocal 变量都是线程安全的,而静态变量和 user 对象中的变量都是非线程安全的。
4、ThreadLocal
4.1、概念
ThreadLocal 类提供了线程局部变量,每个线程可以将一个值存在 ThreadLocal 对象中,其他线程无法访问这些值。每个线程都有自己独立的变量副本。
ThreadLocal 的初始值可通过 withInitial() 方法设置:
private static final ThreadLocal<String> requestId = ThreadLocal.withInitial(() -> "default-id");
简单的内存模型:
+-----------------+ +------------------+ | Thread A | | Thread B | +-----------------+ +------------------+ | ThreadLocal | | ThreadLocal | | - value: 123 | | - value: 456 | +-----------------+ +------------------+ Thread A and Thread B can have different values in the same ThreadLocal.
不同线程直接保存了不同的值。
4.2、优点
若单例 Bean 需要保存线程私有的状态(如用户请求上下文),多线程场景下,多个线程对这个单例Bean的成员变量并不存在资源的竞争,因为ThreadLocal为每个线程保存线程私有的数据。这是一种以空间换时间的方式。
4.3、原理
如下图所示:
调用 ThreadLocal.set(value)方法时,它会将这个值与当前线程关联,而该值被存储在当前线程的一个内部数据结构中。通过 ThreadLocal.get()方法,可以获取当前线程所关联的值。
- 核心机制:每个线程内部维护一个 ThreadLocalMap(类似键值对存储,以 ThreadLocal&nb编程sp;对象为键,存储线程私有的变量。
- 数据隔离:线程通过自己的 ThreadLocalMap 访问变量,不同线程之间的数据互不影响。
- 内存模型:
Thread-1 → ThreadLocalMap → { ThreadLocalA → Value1, ThreadLocalB → Value2 } Thread-2 → ThreadLocalMap → { ThreadLocalA → Value3, ThreadLocalB → Value4 }
4.4、注意
由于ThreadLocal里面维护了ThreadLocalMap类,如下图所示:
而TheadLocalMap是由Entry[]组成组成,Entry[]维护了多个entry。如下所示:
一个entry由key(threadlocal)和value,Entry继承了弱引用,关于弱引用可参考:对Java 资源管理和引用体系的介绍
如下图所示:entry
如果使用不当,会引发oom问题,主要是由GC回收机制和内存结构两者引起。
可参考:就ThreadLocal使用时OOM的讨论
4.5、使用场景
- 用户会话信息: 在 web 应用中维护用户的会话信息,避免将状态信息写到全局上下文。
- 数据库连接: 在线程中维护数据源连接,避免不同线程之间共享资源引起的竞争。
- 事务管理(如 Spring 的 TransactionSynchronizationManager)。
以下是一个简单的 Spring Bean 示例,展示如何在 Spring 中使用 ThreadLocal 来存储用户会话信息。
1.定义一个 ThreadLocal Storage
import org.springframework.stereotype.Component; @Component public class UserContext { private static final ThreadLocal<String> userHolder = new ThreadLocal<>(); public void setCurrentUser(String username) { userHolder.set(username); } public String getCurrentUser() { return userHolder.get(); } //清理 ThreadLocal,防止内存泄漏 public void clear() { userHolder.remove(); // 清除当前线程中的值 } }
2.使用 UserContext
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class UserService { @Autowired private UserContext userContext; public void login(String username) { userContext.setCurrentUser(username); System.out.println("User logged in: " + userContext.getCurrentUser()); } public void logout() { System.out.println("User logged out: " + userContext.getCurrentUser()); userContext.clear(); } }
3 示例测试类
import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest public class UserServiceTest { @Autowired private UserService userService; @Test public void testThreadLocal() { userService.login("Alice"); userService.logout(); // Clear (will just have no output, but it demonstrates functionality) userService.login("Bob"); userService.logout(); } }
4. 图形展示
在多线程环境中的 ThreadLocal 可能如下图所示:
+-------------------+ | UserContext | |-------------------| | ThreadLocal | | - userHolder | +-------------------+ | | | | v v +------------+ +-------------+ | Thread A | | Thread B | |------------| |------------ | | - user: "Alice" | - user: "Bob" | +------------+ +--------------+
在每个线程中,UserContext 提供了对 ThreadLocal 变量独立的值,使得 Thread A 可以存储与 Thread B 不同的用户会话信息。
5、解决方案
根据以上介绍Spring Bean的线程安全问题,以下是各种常用的解决方案。
1、同步机制去处理
synchronized 关键字或者 ReentrantLock 可重入锁。
示例:
synchronized介绍:
import org.springframework.stereotype.Component; @Component public class OrderServiceBean { private int orderStatus; public synchronized void updateOrderStatus() { // 这里进行更新订单状态的具体业务逻辑,比如根据某些条件修改orderStatus的值 orderStatus++; } public int getOrderStatus() { return orderStatus; } }
ReentrantLock介绍:
import org.springframework.stereotype.Component; import java.util.concurrent.locks.ReentrantLock; @Component public class OrderServiceBean { private int orderStatus; private ReentrantLock lock = new ReentrantLock(); public void updateOrderStatus() { lock.lock(); try { // 这里进行更新订单状态的具体业务逻辑,比如根据某些条件修改orderStatus的值 orderStatus++; } finally { lock.unlock(); } } public int getOrderStatus() { return orderStatus; } }
2、Treadlocal对象(推荐)
3、采用不可变对象(Immutable Objects)
设置final对象或者成员变量属性。
4、使用原子类(Atomic Classes)
import org.springframework.stereotype.Component; import java.util.concurrent.atomic.AtomicInteger; @Component public class VisitCountBean { private AtomicInteger visitCount = new AtomicInteger(0); public void incrementVisitCount() { visitCount.incrementAndGet(); } public int getVisitCount() { return visitCount.get(); } }
在 Spring 中实现线程安全,尤其是涉及到多个线程共享状态时,常常需要:
- 选择适当的 Bean 作用域。
- 尽量减少或避免可变状态。
- 使用 ThreadLocal 来管理线程局部数据。
- 使用 AOP 及 Spring 事务来处理业务逻辑。
- 实现良好的设计模式以确保代码的可维护性。
通过以上最佳实践,可以有效地在 Spring 应用中实现线程安全,确保系统的稳定性和数据一致性。
总结
以上为个人经验,希望能给大家一个参考,也希望大家多多支持编程客栈(www.devze.com)。
精彩评论