java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > Spring中bean线程安全讨论

对Spring中bean线程安全的讨论

作者:找不到、了

这篇文章主要介绍了对Spring中bean线程安全的讨论,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教

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() {
        return count;
    }
}

1.2、无状态对象

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

代码示例如下:

@Service
public class Calculator {
    // 无状态:不保存任何实例变量
    public int add(int a, int b) {
        return a + b;
    }
}

两者的区别和联系:

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(){
        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

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()方法,可以获取当前线程所关联的值。

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、使用场景

以下是一个简单的 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 中实现线程安全,尤其是涉及到多个线程共享状态时,常常需要:

通过以上最佳实践,可以有效地在 Spring 应用中实现线程安全,确保系统的稳定性和数据一致性。

总结

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

您可能感兴趣的文章:
阅读全文