spring单例引起的线程安全问题

==> 学习汇总(持续更新)
==> 从零搭建后端基础设施系列(一)-- 背景介绍


一、spring单例与多例定义

单例:一个类只能产生一个对象(对应到spring中,注入的对象永远是同一个)
多例:一个类能产生多个对象(对应到spring中,注入的对象永远是新的)

@Scope("prototype")
@Scope("singleton")

可以使用@Scope注解,标记这个类是单例还是多例,默认是单例。

二、使用单例引起线程安全问题的例子

那究竟什么时候会用到呢?我相信大多数人写的代码都不会去考虑这个事情,用spring就认为只有单例,也只习惯用单例。但是有时候你想将代码写得更优雅一些的时候,你不得不去思考单例以外的使用场景。接下来,看一个我抽象出来的实际例子

1.没有使用多例的时候
A作为父类,将许多固定的业务逻辑封装在了test这个方法中,然后对子类暴露num()方法,子类只需要实现这个方法即可,不需要感知test()方法中复杂的逻辑。

@Component
public abstract class A {

    @SneakyThrows
    public void test(){
        Thread.sleep(3000);
        System.out.println(num());
    }

    protected abstract int num();
}

@Component
public class B extends A {
    private int n;
    public void test(int n){
        this.n = n;
        super.test();
    }

    @Override
    protected int num() {
        return this.n;
    }
}

@Component
public class Run implements CommandLineRunner {
    @Autowired
    private B b;
    
    @Override
    public void run(String... args) throws Exception {
        b.test(1);
        b.test(2);
        b.test(3);
        b.test(4);
        b.test(5);
    }
}

看完这段简单的代码,你觉得会打印出什么?




……
在这里插入图片描述
是的,这样和预想的是一样的。
那么再看看下面这种呢?

@Component
public class Run implements CommandLineRunner {
    @Autowired
    private B b;

    @Override
    public void run(String... args) throws Exception {
        Thread t1 = new Thread(() -> b.test(1));

        Thread t2 = new Thread(() -> b.test(2));

        Thread t3 = new Thread(() -> b.test(3));

        Thread t4 = new Thread(() -> b.test(4));

        Thread t5 = new Thread(() -> b.test(5));

        t1.start();
        t2.start();
        t3.start();
        t4.start();
        t5.start();
    }
} 

结果是……
在这里插入图片描述
这就是单例所产生的线程安全问题了。

其实很好分析,我们来看
在这里插入图片描述
看图中的第一点,这里n被赋值了,如果是串行执行的话,那么就会继续执行2。但是改成并行执行,那么假设test需要3s才能处理完,那么n此时已经变成5了,最后拿到的也就是5,所以才会全部都打印5。

简单点说就是,多个线程同时修改一个对象,导致结果预期不一致。

2.使用多例的时候
可以看到,B多了个@Scope("prototype")注解,表示它是一个多例。这里为什么要使用C获取B,而不是直接注入B呢?前面说过,多例,每一次注入都是新的对象,但是你想想,在Run类里面,B只会注入一次,然后你使用N次,还是同一个对象啊。所以需要C,每次都拿一个新的B。有的人就会说,那为什么不直接new呢?那不是为了能让spring帮我们管理吗?所以就得遵守他们的规则。

@Component
public abstract class A {

    @SneakyThrows
    public void test(){
        Thread.sleep(3000);
        System.out.println(num());
    }

    protected abstract int num();
}

@Scope("prototype")
@Component
public class B extends A {
    private int n;
    public void test(int n){
        this.n = n;
        super.test();
    }

    @Override
    protected int num() {
        return this.n;
    }
}

@Component
public class C implements ApplicationContextAware {
    private ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    public B getB(){
        return (B)this.applicationContext.getBean("b");
    }
}

@Component
public class Run implements CommandLineRunner {
    @Autowired
    private C c;

    @Override
    public void run(String... args) throws Exception {
        Thread t1 = new Thread(() -> c.getB().test(1));

        Thread t2 = new Thread(() -> c.getB().test(2));

        Thread t3 = new Thread(() -> c.getB().test(3));

        Thread t4 = new Thread(() -> c.getB().test(4));

        Thread t5 = new Thread(() -> c.getB().test(5));

        t1.start();
        t2.start();
        t3.start();
        t4.start();
        t5.start();
    }
}

输出结果为:
在这里插入图片描述
符合预期,解决了单例中线程安全的问题。

扫描二维码关注公众号,回复: 11130755 查看本文章

三、总结

  • @Scope("prototype")可以标记该类为多例
  • 单例每次注入都是同一个对象,多例每次注入都是不同对象
  • 单例在并发的情况下,如果存在非单例成员变量,可能导致线程安全问题
  • 单多例混合使用时,需要注意是否生效,像上述例子,如果不是多次获取或注入,是不会生效的。
原创文章 257 获赞 277 访问量 69万+

猜你喜欢

转载自blog.csdn.net/qq_18297675/article/details/105738223