SpringMVC:如何保證 Controller 的併發安全

單例模式(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);
    }
}

在本地運行後:

從這個例子可以看出,所有的請求訪問同一個 Controller 實例,Controller 的私有成員變量就是線程共用的。某個請求對應的線程如果修改了這個變量,那麼在別的請求中也可以讀到這個變量修改後的的值。

Controller 併發安全的解決辦法

如果要保證 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 的話,容器在接受到該類型對象的請求時,每次都會重新生成一個新的對象給請求方。

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 中定義成員變量爲好。

程序員追風 專注於分享 Java 各類學習筆記、面試題以及 IT 類資訊。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/IIOTgayKDR3nW6YYhZt8qw