一、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();
}
}
输出结果为:
符合预期,解决了单例中线程安全的问题。
三、总结
-
@Scope("prototype")
可以标记该类为多例 - 单例每次注入都是同一个对象,多例每次注入都是不同对象
- 单例在并发的情况下,如果存在非单例成员变量,可能导致线程安全问题
- 单多例混合使用时,需要注意是否生效,像上述例子,如果不是多次获取或注入,是不会生效的。