Spring中的Bean是线程安全的吗?

Spring中的Bean是否线程安全,其实跟Spring容器本身没有关系。Spring框架中没有提供线程安全的策略,因此,Spring容器中存在的Bean本身也不具备线程安全的特性。

1、Spring中Bean从哪里来的?

在Spring容器中,除了很多Spring内置的Bean以外,其它的Bean都是我们自己通过Spring配置来声明的,然后由Spring容器统一加载。我们在Spring声明配置中通常会配置以下内容,如:class(全类名)、id(Bean唯一标识)、scope(作用域)以及lazy-init(是否延时加载)等。Spring容器根据配置内容使用对应的策略来创建Bean的实例。Spring容器中的Bean其实都是根据我们自己写的类来创建实例,因此,Spring中的Bean是否线程安全,跟Spring容器本身无关,只是交给Spring容器托管。

 2、Spring的Bean作用域(scope)定义

 a,singleton:单例,默认作⽤域

用来定义一个Bean为单例,在Spring IOC容器中仅有唯一的一个实例对象,Spring中的Bean默认都是单例的。它的作用域范围是ApplicationContext容器。

b,prototype:原型(多例Bean),每次创建⼀个新对象

用来定义一个Bean为多例,再每次请求获取Bean的时候都会重新创建实例,因此每次获取到的实例对象都是不同的。它的作用域范围是调用getBean方法直至获取对象。

c,request:请求,每次Http请求创建⼀个新对象,适⽤于WebApplicationContext环境下。

用来定义一个作用范围仅在request中的Bean,在每次HTTP请求时会创建一个实例,该实例仅在当前Request中有效。它的作用域范围是每次发起HTTP请求直至拿到响应结果。 

d,session:会话,同⼀个会话共享⼀个实例,不同会话使⽤不⽤的实例

用来定义一个作用范围仅在session中的Bean,在每次HTTP请求时会创建一个实例,该实例仅在当前HTTP Session中有效。它的作用域范围是浏览器首次访问至浏览器关闭 

e,global-session:全局会话,所有会话共享⼀个实例

 用来定义一个作用范围仅在ServeletContext的生命周期的Bean,也就是说该实例仅存在WebApplicationContext环境中。它的作用范围是整个WebApplicationContext容器。同session作用域不同的是,所有的session共享一个Bean实例。

  线程安全这个问题,要从单例与原型(多例)Bean分别进⾏说明。对于原型Bean,每次创建⼀个新对象,也就是线程之间并不存在Bean共享,⾃然是不会有线程安全的问题。对于单例Bean,所有线程都共享⼀个单例实例Bean,因此是存在资源的竞争,因此就会存在线程安全问题。单例Bean又分为无状态Bean和有状态Bean。在多线程操作中智慧对Bean的成员变量执⾏查询操作,不会修改成员变量的值,这样的Bean称之为无状态Bean;无状态的单例Bean是不存在线程安全问题的。⽐如Spring mvc 的Controller、Service、Dao等,这些Bean⼤多是⽆状态的,只关注于⽅法本⾝。在多线程操作中如果需要对Bean中的成员变量进行数据更新操作,这样的Bean称之为有状态Bean,有状态的单例Bean就可能存在线程安全问题。

3、Bean 状态

有状态对象(Stateful Bean) :就是有实例变量的对象,可以保存数据,是非线程安全的。每个用户有自己特有的一个实例,在用户的生存期内,bean保持了用户的信息,即“有状态”;一旦用户灭亡(调用结束或实例结束),bean的生命期也告结束。即每个用户最初都会得到一个初始的bean。

无状态对象(Stateless Bean):就是没有实例变量的对象,不能保存数据,是不变类,是线程安全的。bean一旦实例化就被加进会话池中,各个用户都可以共用。即使用户已经消亡,bean 的生命期也不一定结束,它可能依然存在于会话池中,供其他用户调用。由于没有特定的用户,那么也就不能保持某一用户的状态,所以叫无状态bean。但无状态会话bean 并非没有状态,如果它有自己的属性(变量),那么这些变量就会受到所有调用它的用户的影响,这是在实际应用中必须注意的。

4、如何处理Spring中Bean线程安全问题?

1、将 Bean 的作用域由 “singleton” 单例改为 “prototype” 多例;

2、在 Bean 对象中避免定义可变的成员变量;

3、在类中定义 ThreadLocal 的成员变量,并将需要的可变成员变量保存在 ThreadLocal 中,ThreadLocal 本身就具备线程隔离的特性,这就相当于为每个线程提供了一个独立的变量副本,每个线程只需要操作自己的线程副本变量,从而解决线程安全问题。

5、示例代码

a,测试默认(单例)模式Bean存在线程安全问题

package com.icoolkj.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class TestController {

    private int num =0;
    private static int staticNum =0;
    ThreadLocal<Integer> threadLocalNum=new ThreadLocal<>();

    @GetMapping("/test")
    public  void  test(){
        threadLocalNum.set(1);
        System.out.println("普通变量:"+(++num) +"-----静态变量:"+(++staticNum)+"-----线程独享:"+threadLocalNum.get());
    }
}

结果:

b42251e2ac32f9f5c430ecb7b07104d3.png

b,测试多例模式Bean存在线程安全问题

package com.icoolkj.controller;

import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
@Scope("prototype")
public class TestController {

    private int num =0;
    private static int staticNum =0;
    ThreadLocal<Integer> threadLocalNum=new ThreadLocal<>();

    @GetMapping("/test")
    public  void  test(){
        threadLocalNum.set(1);
        System.out.println("普通变量:"+(++num) +"-----静态变量:"+(++staticNum)+"-----线程独享:"+threadLocalNum.get());
    }
}

结果:

 静态变量被所有实例对象共享,在内存中只有一个副本,当且仅当类初次加载时被初始化。

实例变量是实例对象所拥有的,在创建对象的时候被初始化,存在多个副本,各个实例对象拥有的副本互不影响。

虽然多例每次都是单独创建一个Controller,但是扛不住静态变量,故即便是加上@Scope注解也不一定能保证Controller 100%的线程安全。所以是否线程安全在于怎样去定义变量以及Controller的配置。下面总结一下:

1、在@Controller/@Service/@Repository等容器中,默认情况下,scope值是单例-singleton的,也是线程不安全的。

2、尽量不要在@Controller/@Service/@Repository等容器中定义静态变量,不论是单例(singleton)还是多实例(prototype),它都是线程不安全的。

3、一定要定义变量的话,用ThreadLocal来封装,这个是线程安全的。

4、对有状态的 bean 要使用 prototype。

5、对无状态的 bean 使用 singleton 作用域。


版权声明:本文为qq_29768197原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。