Spring Controller單例與線程安全那些事兒


目錄


單例(singleton)作用域

每個添加@RestController或@Controller的控制器,默認是單例(singleton),這也是Spring Bean的默認作用域。

下面代碼示例參考了Building a RESTful Web Service,該教程搭建基於Spring Boot的web項目,源代碼可參考spring-guides/gs-rest-service

GreetingController.java代碼如下:

package com.example.controller;

import java.util.concurrent.atomic.AtomicLong;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class GreetingController {

    private static final String template = "Hello, %s!";
    private final AtomicLong counter = new AtomicLong();

    @GetMapping("/greeting")
    public Greeting greeting(@RequestParam(value = "name", defaultValue = "World") String name) {
        Greeting greet =  new Greeting(counter.incrementAndGet(), String.format(template, name));
        System.out.println("id=" + greet.getId() + ", instance=" + this);
        return greet;
    }
}

我們使用HTTP基准工具wrk來生成大量HTTP請求。在終端輸入如下命令來測試:

wrk -t12 -c400 -d10s http://127.0.0.1:8080/greeting

在服務端的標准輸出中,可以看到類似日志。

id=162440, instance=com.example.controller.GreetingController@368b1b03
id=162439, instance=com.example.controller.GreetingController@368b1b03
id=162438, instance=com.example.controller.GreetingController@368b1b03
id=162441, instance=com.example.controller.GreetingController@368b1b03
id=162442, instance=com.example.controller.GreetingController@368b1b03
id=162443, instance=com.example.controller.GreetingController@368b1b03
id=162444, instance=com.example.controller.GreetingController@368b1b03
id=162445, instance=com.example.controller.GreetingController@368b1b03
id=162446, instance=com.example.controller.GreetingController@368b1b03

日志中所有GreetingController實例的地址都是一樣的,說明多個請求對同一個 GreetingController 實例進行處理,並且它的AtomicLong類型的counter字段正按預期在每次調用時遞增。


原型(Prototype)作用域

如果我們在@RestController注解上方增加@Scope("prototype")注解,使bean作用域變成原型作用域,其它內容保持不變。

...

@Scope("prototype")
@RestController
public class GreetingController {
    ...
}

服務端的標准輸出日志如下,說明改成原型作用域后,每次請求都會創建新的bean,所以返回的id始終是1,bean實例地址也不同。

id=1, instance=com.example.controller.GreetingController@2437b9b6
id=1, instance=com.example.controller.GreetingController@c35e3b8
id=1, instance=com.example.controller.GreetingController@6ea455db
id=1, instance=com.example.controller.GreetingController@3fa9d3a4
id=1, instance=com.example.controller.GreetingController@3cb58b3

多個HTTP請求在Spring控制器內部串行還是並行執行方法?

如果我們在greeting()方法中增加休眠時間,來看下每個http請求是否會串行調用控制器里面的方法。

@RestController
public class GreetingController {

    private static final String template = "Hello, %s!";
    private final AtomicLong counter = new AtomicLong();

    @GetMapping("/greeting")
    public Greeting greeting(@RequestParam(value = "name", defaultValue = "World") String name) throws InterruptedException {
        Thread.sleep(1000); // 休眠1s
        Greeting greet =  new Greeting(counter.incrementAndGet(), String.format(template, name));
        System.out.println("id=" + greet.getId() + ", instance=" + this);
        return greet;
    }
}

還是使用wrk來創建大量請求,可以看出即使服務端的方法休眠1秒,導致每個請求的平均延遲達到1.18s,但每秒能處理的請求仍達到166個,證明HTTP請求在Spring MVC內部是並發調用控制器的方法,而不是串行。

wrk -t12 -c400 -d10s http://127.0.0.1:8080/greeting

Running 10s test @ http://127.0.0.1:8080/greeting
  12 threads and 400 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency     1.18s   296.41ms   1.89s    85.22%
    Req/Sec    37.85     38.04   153.00     80.00%
  1664 requests in 10.02s, 262.17KB read
  Socket errors: connect 155, read 234, write 0, timeout 0
Requests/sec:    166.08
Transfer/sec:     26.17KB

實現單例模式並模擬大量並發請求,驗證線程安全

單例類的定義:Singleton.java

package com.demo.designpattern;

import java.util.concurrent.atomic.AtomicInteger;

public class Singleton {

    private volatile static Singleton singleton;
    private int counter = 0;
    private AtomicInteger atomicInteger = new AtomicInteger(0);

    private Singleton() {
    }

    public static Singleton getSingleton() {
        if (singleton == null) {
            synchronized (Singleton.class) {
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }

    public int getUnsafeNext() {
        return ++counter;
    }

    public int getUnsafeCounter() {
        return counter;
    }

    public int getSafeNext() {
        return atomicInteger.incrementAndGet();
    }

    public int getSafeCounter() {
        return atomicInteger.get();
    }

}

測試單例類並創建大量請求並發調用:SingletonTest.java

package com.demo.designpattern;

import java.util.*;
import java.util.concurrent.*;

public class SingletonTest {

    public static void main(String[] args) {
        // 定義可返回計算結果的非線程安全的Callback實例
        Callable<Integer> unsafeCallableTask = () -> Singleton.getSingleton().getUnsafeNext();
        runTask(unsafeCallableTask);
        // unsafe counter may less than 1000, i.e. 984
        System.out.println("current counter = " + Singleton.getSingleton().getUnsafeCounter());

        // 定義可返回計算結果的線程安全的Callback實例(基於AtomicInteger)
        Callable<Integer> safeCallableTask = () -> Singleton.getSingleton().getSafeNext();
        runTask(safeCallableTask);
        // safe counter should be 1000
        System.out.println("current counter = " + Singleton.getSingleton().getSafeCounter());

    }

    public static void runTask(Callable<Integer> callableTask) {
        int cores = Runtime.getRuntime().availableProcessors();
        ExecutorService threadPool = Executors.newFixedThreadPool(cores);
        List<Callable<Integer>> callableTasks = new ArrayList<>();
        for (int i = 0; i < 1000; i++) {
            callableTasks.add(callableTask);
        }
        Map<Integer, Integer> frequency = new HashMap<>();
        try {
            List<Future<Integer>> futures = threadPool.invokeAll(callableTasks);
            for (Future<Integer> future : futures) {
                frequency.put(future.get(), frequency.getOrDefault(future.get(), 0) + 1);
                //System.out.printf("counter=%s\n", future.get());
            }
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
        threadPool.shutdown();
    }
}

附錄:Spring Bean作用域

范圍
描述
singleton(單例) (默認值)將每個Spring IoC容器的單個bean定義范圍限定為單個對象實例。

換句話說,當您定義一個bean並且其作用域為單例時,Spring IoC容器將為該bean所定義的對象創建一個實例。該單例存儲在單例beans的高速緩存中,並且對該命名bean的所有后續請求和引用都返回該高速緩存的對象。
prototype(原型) 將單個bean定義的作用域限定為任意數量的對象實例。

每次對特定bean發出請求時,bean原型作用域都會創建一個新bean實例。也就是說,將Bean注入到另一個Bean中,或者您可以調用容器上的getBean()方法來請求它。通常,應將原型作用域用於所有有狀態Bean,將單例作用域用於無狀態Bean。
request 將單個bean定義的范圍限定為單個HTTP請求的生命周期。也就是說,每個HTTP請求都有一個在單個bean定義后創建的bean實例。僅在web-aware的Spring ApplicationContext上下文有效。
session 將單個bean定義的范圍限定為HTTP Session的生命周期。僅在基於web的Spring ApplicationContext上下文有效。
application 將單個bean定義的范圍限定為ServletContext的生命周期。僅在基於web的Spring ApplicationContext上下文有效。
websocket 將單個bean定義的作用域限定為WebSocket的生命周期。僅在基於web的Spring ApplicationContext上下文有效。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM