1. 程式人生 > 遊戲 >《春逝百年抄》追加客串演員公佈 5月13日正式發售

《春逝百年抄》追加客串演員公佈 5月13日正式發售

單例模式(Singleton)是程式設計中一種非常重要的設計模式,設計模式也是Java面試重點考察的一個方面。面試經常會問到的一個問題是:SpringMVC中的Controller是單例還是多例,很多同學可能會想當然認為Controller是多例,其實不然。

Tomcat官網截圖

根據Tomcat官網中的介紹,對於一個瀏覽器請求,tomcat會指定一個處理執行緒,或是線上程池中選取空閒的,或者新建一個執行緒。

Each incoming request requires a thread for the duration of that request. If more simultaneous requests are received than can be handled by the currently available request processing threads, additional threads will be created up to the configured maximum (the value of the maxThreads attribute). If still more simultaneous requests are received, they are stacked up inside the server socket created by the Connector, up to the configured maximum (the value of the acceptCountattribute). Any further simultaneous requests will receive "connection refused" errors, until resources are available to process them.

—— https://tomcat.apache.org/tomcat-7.0-doc/config/http.html

在Tomcat容器中,每個servlet是單例的。在SpringMVC中,Controller 預設也是單例。採用單例模式的最大好處,就是可以在高併發場景下極大地節省記憶體資源,提高服務抗壓能力。

單例模式容易出現的問題是:在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的執行緒安全,有以下解決辦法:

  • 儘量不要在 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。關於ThreadLocal的原理可以參考前文《Java面試常考問題:ThreadLocal的基本原理 》。

更嚴格的做法是用AtomicInteger型別定義成員變數,對於成員變數的操作使用AtomicInteger的自增方法完成。

總的來說,還是儘量不要在 Controller 中定義成員變數為好。