首先解释一下什么是单例 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 各有各的特点