浅析Spring IOC bean为什么默认是单例

 更新时间:2023年12月14日 09:57:17   作者:Lzfnemo2009  
单例的意思就是说在 Spring IoC 容器中只会存在一个 bean 的实例,无论一次调用还是多次调用,始终指向的都是同一个 bean 对象,本文小编将和大家一起分析Spring IOC bean为什么默认是单例,需要的朋友可以参考下

首先解释一下什么是单例 bean?

单例的意思就是说在 Spring IoC 容器中只会存在一个 bean 的实例,无论一次调用还是多次调用,始终指向的都是同一个 bean 对象

用代码来解释单例 bean

public class UserService {
    public void sayHello() {
        System.out.println("hello");
    }
}
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
                           http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
 
		<!-- scope 属性就是用来设置 bean 的作用域的,不配置的话默认就是单例,这里显示配置了 singleton -->
    <bean id="userService" class="com.fyl.springboot.bean.singleton.UserService" scope="singleton"/>
 
</beans>
public class Demo {
 
    public static void main(String[] args) {
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("classpath:beans-singleton.xml");
 
        UserService service = context.getBean(UserService.class);
        UserService service1 = context.getBean(UserService.class);
        System.out.println(service == service1);
    }
}

运行 main 方法最后会输出:true,这就很明显的说明了无论多少次调用 getBean 方法,最终得到的都是同一个实例。

把上面 xml 文件的配置修改一下,修改为:

<!-- scope 的值改为了 prototype,表示每次请求都会创建一个新的 bean -->
<bean id="userService" class="com.fyl.springboot.bean.singleton.UserService" scope="prototype"/>

然后再次运行 main 方法,结果输出:false,说明两次调用 getBean 方法,得到的不是同一个实例。

了解了什么是单例 bean 之后,我们继续来说说单例 bean 的线程安全问题

为什么会存在线程安全问题呢?

因为对于单实例来说,所有线程都共享同一个 bean 实例,自然就会发生资源的争抢。

用代码来说明线程不安全的现象

public class ThreadUnSafe {
 
    public int i;
 
    public void add() {
        i++;
    }
 
    public void sub() {
        i--;
    }
 
    public int getValue() {
        return i;
    }
}
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
                           http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
 
    <bean id="threadUnSafe" class="com.fyl.springboot.bean.singleton.ThreadUnSafe" scope="singleton"/>
 
</beans>
public class Demo {
 
    public static void main(String[] args) {
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("classpath:beans-singleton.xml");
 
        for (int j = 0; j < 10; j++) {
            new Thread(() -> {
                ThreadUnSafe service = context.getBean(ThreadUnSafe.class);
                for (int i = 0; i < 1000; i++) {
                    service.add();
                }
                for (int i = 0; i < 1000; i++) {
                    service.sub();
                }
                System.out.println(service.getValue());
            }).start();
        }
    }
}

上面的代码中,创建了 10 个线程来获取 ThreadUnSafe 实例,并且循环 1000 次加法,循环 1000 次减法,并把最后的结果打印出来。理想的情况是每个线程打印出来的结果都是 0

先看一下运行结果:

2073
1736
1080
1060
221
49
50
-231
-231
-231

从结果可以看出,运行结果都不是 0,这明显的是线程不安全啊!

为什么会出现这种情况?

因为 10 个线程获取的 ThreadUnSafe 实例都是同一个,并且 10 个线程都对同一个资源 i 发生了争抢,所以才会导致线程安全问题的发生。

现在把 xml 文件中的配置做一下更改:scope 的值改为 prototype

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
                           http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
 
    <!-- scope 的值改为 prototype -->
    <bean id="threadUnSafe" class="com.fyl.springboot.bean.singleton.ThreadUnSafe" scope="prototype"/>
 
</beans>

然后再次运行 main 方法,发现无论运行多少次,最后的结果都是 0,是线程安全的!

因为 prototype 作用域下,每次获取的 ThreadUnSafe 实例都不是同一个,所以自然不会有线程安全的问题。

如果单例 bean 是一个无状态的 bean,还会有线程安全问题吗?

不会,无状态 bean 没有实例对象,不能保存数据,是不变类,是线程安全的。

public class ThreadSafe {
 
    public void getValue() {
        int val = 0;
        for (int i = 0; i < 1000; i++) {
            val++;
        }
        for (int i = 0; i < 1000; i++) {
            val--;
        }
        System.out.println(val);
    }
}
public class Demo {
 
    public static void main(String[] args) {
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("classpath:beans-singleton.xml");
 
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                ThreadSafe service = context.getBean(ThreadSafe.class);
                service.getValue();
            }).start();
        }
    }
}
 

运行结果为 0

事实证明,无状态的 bean 是线程安全的。(无状态 bean 应该是这个意思,如有不对的地方,还望指出)

那么针对单例 bean,而且是有状态的 bean,应该如何保证线程安全呢?

那有人肯定会说了:既然是线程安全问题,那就加锁呗!

毫无疑问加锁确实可以,但是加锁多多少少有点性能上的下降

加锁代码如下所示:

public class Demo {
 
    public static void main(String[] args) {
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("classpath:beans-singleton.xml");
 
        for (int j = 0; j < 10; j++) {
            new Thread(() -> {
                ThreadUnSafe service = context.getBean(ThreadUnSafe.class);
                synchronized (service) {
                    for (int i = 0; i < 1000; i++) {
                        service.add();
                    }
                    for (int i = 0; i < 1000; i++) {
                        service.sub();
                    }
                    System.out.println(service.getValue());
                }
            }).start();
        }
    }
}

还有一种方法是使用 ThreadLocal

ThreadLocal 简单的说就是在自己线程内创建一个变量的副本,那么线程操作的自然也就是自己线程内的资源了,也就规避了线程安全问题。但是却带来了空间上的开销。

使用方法如下:

public class ThreadUnSafe {
 
    ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
 
    public void add() {
        Integer i = threadLocal.get();
        if (i == null) {
            i = 0;
        }
        i++;
        threadLocal.set(i);
    }
 
    public void sub() {
        Integer i = threadLocal.get();
        i--;
        threadLocal.set(i);
    }
 
    public Integer getValue() {
        return threadLocal.get();
    }
}
public class Demo {
 
    public static void main(String[] args) {
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("classpath:beans-singleton.xml");
 
        for (int j = 0; j < 10; j++) {
            new Thread(() -> {
                ThreadUnSafe service = context.getBean(ThreadUnSafe.class);
                for (int i = 0; i < 1000; i++) {
                    service.add();
                }
                for (int i = 0; i < 1000; i++) {
                    service.sub();
                }
                System.out.println(service.getValue());
            }).start();
        }
    }
}

使用 ThreadLocal 即使不加锁也保证了输出的结果都是 0

加锁和使用 ThreadLocal 各有各的特点

  • 加锁是以时间换空间
  • ThreadLocal 是以空间换时间

以上就是浅析Spring IOC bean为什么默认是单例的详细内容,更多关于Spring IOC bean默认单例的资料请关注脚本之家其它相关文章!

相关文章

  • Maven在Windows中的配置以及IDE中的项目创建(图文教程)

    Maven在Windows中的配置以及IDE中的项目创建(图文教程)

    这篇文章主要介绍了Maven在Windows中的配置以及IDE中的项目创建(图文教程),具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2017-09-09
  • SpringBoot 拦截器妙用你真的了解吗

    SpringBoot 拦截器妙用你真的了解吗

    关于springboot拦截器很多朋友掌握的不是多好,今天抽空给大家普及下这方面的知识,SpringBoot 拦截器妙用,让你一个人开发整个系统的鉴权模块,快来跟小编一起学习下吧
    2021-07-07
  • 详解关于springboot-actuator监控的401无权限访问

    详解关于springboot-actuator监控的401无权限访问

    本篇文章主要介绍了详解关于springboot-actuator监控的401无权限访问,非常具有实用价值,有兴趣的可以了解一下
    2017-09-09
  • Java实战之用Spring开发条形码和验证码

    Java实战之用Spring开发条形码和验证码

    这篇文章主要介绍了Java实战之用Spring开发条形码和验证码,文中有非常详细的代码示例,对正在学习java的小伙伴们有非常好的帮助,需要的朋友可以参考下
    2021-04-04
  • mybatis日志打印的两款IDEA插件推荐

    mybatis日志打印的两款IDEA插件推荐

    这篇文章主要给大家推荐介绍了关于mybatis日志打印的两款IDEA插件,文中通过图文以及实例代码介绍的非常详细,对大家学习或者使用mybatis具有一定的参考学习价值,需要的朋友可以参考下
    2023-04-04
  • SpringBoot事件发布和监听详解

    SpringBoot事件发布和监听详解

    今天去官网查看spring boot资料时,在特性中看见了系统的事件及监听章节,所以下面这篇文章主要给大家介绍了关于SpringBoot事件发布和监听的相关资料,文中通过实例代码介绍的非常详细,需要的朋友可以参考下
    2021-11-11
  • java类的定义与使用举例详解

    java类的定义与使用举例详解

    这篇文章主要给大家介绍了关于java类的定义与使用的相关资料,类的方法是用来定义类的行为,在方法中通过操作类的成员变量、编写业务逻辑、返回 结果等实现类的业务行为,需要的朋友可以参考下
    2023-11-11
  • java判断用户输入的是否至少含有N位小数的实例

    java判断用户输入的是否至少含有N位小数的实例

    下面小编就为大家分享一篇java判断用户输入的是否至少含有N位小数的实例,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2017-12-12
  • 新版SpringSecurity安全配置说明

    新版SpringSecurity安全配置说明

    这篇文章主要介绍了新版SpringSecurity安全配置说明,在 Spring Security 5.7.0-M2 中,我们弃用了WebSecurityConfigurerAdapter,因为我们鼓励用户转向基于组件的安全配置,需要的朋友可以参考下
    2023-07-07
  • 使用IntelliJ IDEA调式Stream流的方法步骤

    使用IntelliJ IDEA调式Stream流的方法步骤

    本文主要介绍了使用IntelliJ IDEA调式Stream流的方法步骤,文中通过示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2022-05-05

最新评论