关于Spring的bean线程安全讨论

目录

1、状态介绍

1.1、有状态对象

1.2、无状态对象

2、作用域

3、线程安全:

3.1、bean的分类

3.2、bean的安全

1、@Controller介绍

2、prototype注解

3、应用

4、ThreadLocal

4.1、概念

4.2、优点

4.3、原理

4.3、使用场景

5、解决方案



前言

            Spring容器中的Bean是否线程安全,容器本身并没有提供Bean的线程安全策略,因此可以说Spring容器中的Bean本身不具备线程安全的特性,但是具体还是要结合具体scope的Bean去研究。   


1、状态介绍

1.1、有状态对象

         有实例变量的对象,即每个用户最初都会得到一个初始的bean,可以保存数据,是非线程安全的。

        每个用户有自己特有的一个实例,在用户的生存期内,bean保持了用户的信息,即“有状态”;一旦用户灭亡(调用结束或实例结束),bean的生命期也告结束。

1.2、无状态对象

        没有实例变量的对象,不能保存数据,是不变类,是线程安全的

        bean一旦实例化就被加进会话池中,各个用户都可以共用。即使用户已经消亡,bean 的生命期也不一定结束,它可能依然存在于会话池中,供其他用户调用。

        由于没有特定的用户,那么也就不能保持某一用户的状态,所以叫无状态bean。但无状态会话bean 并非没有状态,

        如果有自己的属性(变量),那么这些变量就会受到所有调用它的用户的影响。


2、作用域

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 的配置。

3、应用

示例:

config里面自己定义的Bean: User

@Configuration
public class MyConfig {
    @Bean
    public User user(){
        return new User();
    }
}
@RestController
@Scope(value = "singleton") // 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: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

总结:

        多实例模式下普通变量,取配置的变量还有 ThreadLocal 变量都是线程安全的,而静态变量和 user (看它的 hashCode 都是一样的)对象中的变量都是非线程安全的。


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 对象为键,存储线程私有的变量。
  • 数据隔离:线程通过自己的 ThreadLocalMap 访问变量,不同线程之间的数据互不影响。
  • 内存模型
Thread-1 → ThreadLocalMap → { ThreadLocalA → Value1, ThreadLocalB → Value2 }
Thread-2 → ThreadLocalMap → { ThreadLocalA → Value3, ThreadLocalB → Value4 }

4.3、使用场景

  • 用户会话信息: 在 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 应用中实现线程安全,确保系统的稳定性和数据一致性。


参考文章

1、Spring中单例Bean是线程安全的吗

2、Spring Bean的线程安全问题

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值