首先解釋一下什么是單例 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.xsdhttp://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.xsdhttp://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.xsdhttp://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 是以空間換時間