Spring单例bean线程不安全问题学习研究

你好! 欢迎批评指导!

问题引入

  1. 如果有多个请求发过来,多个线程在处理这些请求,这个service会实例化几个?
  2. 如果service里放了一个公有的类变量,处理逻辑修改这个变量,多线程的情况下,会出现什么问题?

问题的回答

  1. 由于Spring容器生成的Bean都是默认单例的,故service会实例化一个单例对象。
  2. 多线程环境修改类变量,由于类变量存储位置属于方法区,由多线程共享,各线程对该共享存储区域的操作可能相互影响,产生如线程A对类变量content做一系列操作,过程中间结果被线程B读取,从而导致线程B结果错误的现象。

如何解决?

背景知识

Spring 的 bean 作用域(scope)类型

1)singleton:单例,默认作用域。
2)prototype:原型,每次创建一个新对象。
3)request:请求,每次Http请求创建一个新对象,适用于WebApplicationContext环境下。
4)session:会话,同一个会话共享一个实例,不同会话使用不用的实例。
5)global-session:全局会话,所有会话共享一个实例。

原型Bean与单例Bean

1)原型Bean: 对于原型Bean,每次创建一个新对象,也就是线程之间并不存在Bean共享,自然是不会有线程安全的问题。
2)单例Bean:对于单例Bean,所有线程都共享一个单例实例Bean,因此是存在资源的竞争。

理论依据

2.1局部变量的固有属性之一就是封闭在执行线程中。 它们位于执行线程的栈中,其他线程无法访问这个栈。
2.2如果单例Bean,是一个无状态Bean,也就是线程中的操作不会对Bean的成员执行查询以外的操作,那么这个单例Bean是线程安全的。比如Spring mvc 的 Controller、Service、Dao等,这些Bean大多是无状态的,只关注于方法本身。
2.3对于有状态的bean,Spring官方提供的bean,一般提供了通过ThreadLocal去解决线程安全的方法,比如RequestContextHolder、TransactionSynchronizationManager、LocaleContextHolder等。
注: Spring容器本身并没有提供线程安全的策略,因此是否线程安全完全取决于Bean本身的特性。

实验过程

多线程模型

service类含静态变量,不作任何其他修饰、限制:

@Service
public class SthServiceImpl extends Thread implements SthService{
    private static String content=new String();
    @Override
    public void init() {
        content="0";
    }
    public void dosth(){
        init();
        for(int i=1;i<10;i++)
        {
            int now=Integer.parseInt(content)+1;
            String tmp=String.valueOf(now);
            content=tmp;
            System.out.printf("i=%d %s %s\n",i,content,Thread.currentThread().getName());
        }
        System.out.printf("%s finished with content=%s\n",Thread.currentThread().getName(),content);
    }
    @Override
    public void  run() {
        dosth();
    }
}
@Test
    void threadTest(){
        new SthServiceImpl().start();
        new SthServiceImpl().start();
        new SthServiceImpl().start();
    }

算法简单完成了对content的初始化(置“0”)和十次加一操作。
ApplicationTests中创建三个线程执行算法。

运行效果:
i=1 1 Thread-105
i=2 2 Thread-105
i=3 3 Thread-105
i=4 4 Thread-105
i=5 5 Thread-105
i=6 6 Thread-105
i=7 7 Thread-105
i=8 8 Thread-105
i=9 9 Thread-105
Thread-105 finished with content=9
i=1 1 Thread-107
i=2 2 Thread-107
i=3 2 Thread-107
i=4 3 Thread-107
i=5 4 Thread-107
i=6 5 Thread-107
i=7 6 Thread-107
i=8 7 Thread-107
i=9 8 Thread-107
Thread-107 finished with content=8
i=1 1 Thread-106
i=2 9 Thread-106
i=3 10 Thread-106
i=4 11 Thread-106
i=5 12 Thread-106
i=6 13 Thread-106
i=7 14 Thread-106
i=8 15 Thread-106
i=9 16 Thread-106
Thread-106 finished with content=16

结果说明:
Thread-105正常执行结束,Thread-107 在i=2之后,i=3之前,被Thread-106介入初始化了静态变量并增值,导致i=3这次循环读取到content=1,增值后得content=2。Thread-106 的i=2这次循环执行在Thread-107 完成任务后,读到content=8,增值得9。

ThreadLocal

使线程在自己的存储空间保存下content的备份,多线程之间不互相干扰
SthServiceImpl1.class:

@Service
//@Scope("prototype")
public class SthServiceImpl1 extends Thread implements SthService{
    private static ThreadLocal<String> content=new ThreadLocal<>();
    @Override
    public void init() {
        content.set("0");
    }
    public void dosth(){
        init();
        for(int i=1;i<10;i++)
        {
            int now=Integer.parseInt(content.get())+1;
            String tmp=String.valueOf(now);
            content.set(tmp);
            System.out.printf("i=%d %s %s\n",i,content.get(),Thread.currentThread().getName());
        }
        System.out.printf("%s finished with content=%s\n",Thread.currentThread().getName(),content.get());
    }
    @Override
    public void  run() {
        dosth();
    }
}
运行效果:
i=1 1 Thread-105
i=2 2 Thread-105
i=3 3 Thread-105
i=4 4 Thread-105
i=5 5 Thread-105
i=6 6 Thread-105
i=7 7 Thread-105
i=8 8 Thread-105
i=9 9 Thread-105
Thread-105 finished with content=9
i=1 1 Thread-107
i=2 2 Thread-107
i=3 3 Thread-107
i=4 4 Thread-107
i=5 5 Thread-107
i=6 6 Thread-107
i=7 7 Thread-107
i=8 8 Thread-107
i=9 9 Thread-107
Thread-107 finished with content=9
i=1 1 Thread-106
i=2 2 Thread-106
i=3 3 Thread-106
i=4 4 Thread-106
i=5 5 Thread-106
i=6 6 Thread-106
i=7 7 Thread-106
i=8 8 Thread-106
i=9 9 Thread-106
Thread-106 finished with content=9

结果正确,各线程独立操作备份下来的变量值。

@Scope(“prototype”)

线程各实例化一个service对象

@Service
//@Scope("prototype")
@Scope(value=ConfigurableBeanFactory.SCOPE_PROTOTYPE, proxyMode = ScopedProxyMode.TARGET_CLASS)
public class SthServiceImpl2 extends Thread implements SthService{
    private static String content=new String();
    @Override
    public void init() {
        content="0";
    }
    public void dosth(){
        init();
        for(int i=1;i<10;i++)
        {
            int now=Integer.parseInt(content)+1;
            String tmp=String.valueOf(now);
            content=tmp;
            System.out.printf("i=%d %s %s\n",i,content,Thread.currentThread().getName());
        }
        System.out.printf("%s finished with content=%s\n",Thread.currentThread().getName(),content);
    }
    @Override
    public void  run() {
        dosth();
    }
}

运行效果与3.2类似。
注:存在@Scope(“prototype”)无法生效的情况,产生错误结果类似3.1。这是因为没设置多例的代理模式的问题,须改成如下配置:
@Scope(value=ConfigurableBeanFactory.SCOPE_PROTOTYPE, proxyMode = ScopedProxyMode.TARGET_CLASS)

@Scope(“request”)

前提:该程序是web应用,可以使用Spring Bean的作用域中的request,就是说在类前面加上@Scope(“request”),表明每次请求都会生成一个新的Bean对象。作用于@Scope(“prototype”)类似。

实验引申

上文对适用于多线程将静态变量保存至私有存储空间进行操作的场景做了实验分析。
实际还存在其他场景如:多线程共同完成对数据的修改工作,可使用常规同步手段。

场景模型

public void dosth(){
        for(int i=1;i<10;i++)
        {
            int now=Integer.parseInt(content)+1;
            String tmp=String.valueOf(now);
            content=tmp;
            System.out.printf("i=%d %s %s\n",i,content,Thread.currentThread().getName());
        }
        System.out.printf("%s finished with content=%s\n",Thread.currentThread().getName(),content);
    }
创建三个线程执行,执行结果:
i=1 1 Thread-107
i=2 2 Thread-107
i=3 3 Thread-107
i=4 4 Thread-107
i=5 5 Thread-107
i=6 6 Thread-107
i=7 7 Thread-107
i=8 8 Thread-107
i=9 9 Thread-107
Thread-107 finished with content=9
i=1 5 Thread-108
i=2 10 Thread-108
i=1 2 Thread-106
i=2 12 Thread-106
i=3 13 Thread-106
i=4 14 Thread-106
i=5 15 Thread-106
i=6 16 Thread-106
i=7 17 Thread-106
i=8 18 Thread-106
i=9 19 Thread-106
Thread-106 finished with content=19
i=3 11 Thread-108
i=4 20 Thread-108
i=5 21 Thread-108
i=6 22 Thread-108
i=7 23 Thread-108
i=8 24 Thread-108
i=9 25 Thread-108
Thread-108 finished with content=25

可以看出,Thread-106的i=2和Thread-108的i=5,这两次加法执行后又被其他线程的赋值结果覆盖,加一操作失去效用。导致最终结果为25=27-2。

synchronized

synchronized修饰dosth方法代码块,实现同步。

public void dosth(){
        synchronized (this){
            for(int i=1;i<10;i++)
            {
                int now=Integer.parseInt(content)+1;
                String tmp=String.valueOf(now);
                content=tmp;
                System.out.printf("i=%d %s %s\n",i,content,Thread.currentThread().getName());
            }
            System.out.printf("%s finished with content=%s\n",Thread.currentThread().getName(),content);
        }
    }
结果:
i=1 1 Thread-107
i=2 4 Thread-107
i=1 3 Thread-108
i=1 2 Thread-109
i=2 6 Thread-108
i=3 8 Thread-108
i=4 9 Thread-108
i=5 10 Thread-108
i=6 11 Thread-108
i=7 12 Thread-108
i=8 13 Thread-108
i=9 14 Thread-108
Thread-108 finished with content=14
i=3 5 Thread-107
i=4 15 Thread-107
i=5 16 Thread-107
i=6 17 Thread-107
i=7 18 Thread-107
i=8 19 Thread-107
i=9 20 Thread-107
Thread-107 finished with content=20
i=2 7 Thread-109
i=3 21 Thread-109
i=4 22 Thread-109
i=5 23 Thread-109
i=6 24 Thread-109
i=7 25 Thread-109
i=8 26 Thread-109
i=9 27 Thread-109
Thread-109 finished with content=27

能获得正确结果27。

总结

Spring容器默认将bean对象创建为单例,多线程环境下会产生错误结果。可以借助ThreadLocal、@Scope等手段解决。也可以从立场上,不对可能被多线程实例化的对象设置类变量。

参考

Spring解决单例bean线程不安全问题

  • 1
    点赞
  • 2
    评论
  • 5
    收藏
  • 打赏
    打赏
  • 扫一扫,分享海报

©️2022 CSDN 皮肤主题:游动-白 设计师:我叫白小胖 返回首页

打赏作者

高中时候的礼

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值