开发者

对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)时,结果不会互相影响。

                  两者的区别和联系:

                  对Spring中bean线程安全的讨论

                  2、Bean作用域

                  bean的生命周期如下所示:

                  实例化--->设置属性--->初始化--->销毁

                  对Spring中bean线程安全的讨论

                  Spring 的 bean 作用域(scope)类型:

                  1、singleton:单例,默认作用域。

                  • 优点: 节省内存,因为只存在一个实例。
                  • 缺点: 由于多个线程可能共享同一个实例,需要格外注意线程安全(非线程安全的状态字段可能导致问题)

                  2、prototype:原型,每次创建一个新对象。

                  3、request:请求,每次Http请求创建一个新对象,适用于WebApplicationContext环境下。

                  4、session:会话,同一个会话共享一个实例,不同会话使用不用的实例。

                  5、global-session:全局会话,所有会话共享一个实例。

                  3、线程安全:

                  从单例与原型Bean分别进行说明。

                  对Spring中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、原理

                  如下图所示:

                  对Spring中bean线程安全的讨论

                  调用 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类,如下图所示:

                  对Spring中bean线程安全的讨论

                  而TheadLocalMap是由Entry[]组成组成,Entry[]维护了多个entry。如下所示:

                  对Spring中bean线程安全的讨论

                  一个entry由key(threadlocal)和value,Entry继承了弱引用,关于弱引用可参考:对Java 资源管理和引用体系的介绍

                  如下图所示:entry

                  对Spring中bean线程安全的讨论

                  如果使用不当,会引发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)。

                  0

                  上一篇:

                  下一篇:

                  精彩评论

                  暂无评论...
                  验证码 换一张
                  取 消

                  最新开发

                  开发排行榜