SpringMVC中的Controller是單例還是多例?很多同學可能會想當然認為Controller是多例,其實不然。
對於一個瀏覽器請求,tomcat會指定一個處理線程,或是在線程池中選取空閑的,或者新建一個線程。在Tomcat容器中,每個servlet是單例的。
在SpringMVC中,Controller 默認也是單例。 采用單例模式的最大好處,就是可以在高並發場景下極大地節省內存資源,提高服務抗壓能力。
單例模式容易出現的問題是:在Controller中定義的實例變量,在多個請求並發時會出現競爭訪問,Controller中的實例變量不是線程安全的。
正因為Controller默認是單例,所以不是線程安全的。如果用SpringMVC 的 Controller時,盡量不在 Controller中使用實例變量,否則會出現線程不安全性的情況,導致數據邏輯混亂。
舉一個簡單的例子,在一個Controller中定義一個非靜態成員變量 num 。通過Controller成員方法來對 num 增加。
@Controller public class TestController { private int num = 0; @RequestMapping("/addNum") public void addNum() { System.out.println(++num); } }
在本地運行后:
- 首先訪問 http:// localhost:8080 / addNum,得到的答案是1;
- 再次訪問 http:// localhost:8080 / addNum,得到的答案是 2。
- 兩次訪問得到的結果不同,num已經被修改,並不是我們希望的結果,接口的冪等性被破壞。
從這個例子可以看出,所有的請求訪問同一個Controller實例,Controller的私有成員變量就是線程共用的。某個請求對應的線程如果修改了這個變量,那么在別的請求中也可以讀到這個變量修改后的的值。
如果要保證Controller的線程安全,有以下解決辦法:
- 盡量不要在 Controller 中定義成員變量 ;
- 如果必須要定義一個非靜態成員變量,那么可以通過注解 @Scope(“prototype”) ,將Controller設置為多例模式。
@Controller @Scope(value="prototype") public class TestController { private int num = 0; @RequestMapping("/addNum") public void addNum() { System.out.println(++num); } }
Scope屬性是用來聲明IOC容器中的對象(Bean )允許存在的限定場景,或者說是對象的存活空間。在對象進入相應的使用場景之前,IOC容器會生成並裝配這些對象;當該對象不再處於這些使用場景的限定時,容器通常會銷毀這些對象。
Controller也是一個Bean,默認的 Scope 屬性為Singleton ,也就是單例模式。如果Bean的 Scope 屬性設置為 prototype 的話,容器在接受到該類型對象的請求時,每次都會重新生成一個新的對象給請求方。
- Controller 中使用 ThreadLocal 變量。 每一個線程都有一個變量的副本。
public class TestController { private int num = 0; private final ThreadLocal <Integer> uniqueNum = new ThreadLocal <Integer> () { @Override protected Integer initialValue() { return num; } }; @RequestMapping("/addNum") public void addNum() { int unum = uniqueNum.get(); uniqueNum.set(++unum); System.out.println(uniqueNum.get()); } }
以上代碼運行以后,每次請求 http:// localhost:8080 / addNum , 得到的結果都是1。
更嚴格的做法是用AtomicInteger類型定義成員變量,對於成員變量的操作使用AtomicInteger的自增方法完成。
總的來說,還是盡量不要在 Controller 中定義成員變量為好。