Spring Controller singletons and thread-safe things

Keywords: Java Spring REST Session

Catalog


singleton scope

Each controller that adds @RestController or @Controller defaults to singleton, which is also the default scope for Spring Bean.

The following code example references Building a RESTful Web Service This tutorial builds a SpringBoot-based web project with source code for reference spring-guides/gs-rest-service

The GreetingController.java code is as follows:

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;
    }
}

We use the HTTP benchmark tool wrk To generate a large number of HTTP requests.Enter the following command at the terminal to test:

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

Similar logs can be seen in the standard output on the server side.

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

The address of all GreetingController instances in the log is the same, indicating that multiple requests process the same GreetingController instance and that its counter field of AtomicLong type is increasing as expected on each call.


Prototype Scope

If we add the @Scope("prototype") comment above the @RestController comment, the bean scope becomes the prototype scope, and the rest remains unchanged.

...

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

The standard output log on the server side is as follows, indicating that each request creates a new bean after changing to the prototype scope, so the returned id is always 1 and the bean instance address is different.

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

Does multiple HTTP requests execute serially or in parallel within the Spring Controller?

If we increase the hibernation time in the greeting() method, see if each http request serially calls the method in the controller.

@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); // Hibernate 1s
        Greeting greet =  new Greeting(counter.incrementAndGet(), String.format(template, name));
        System.out.println("id=" + greet.getId() + ", instance=" + this);
        return greet;
    }
}

Or using wrk to create a large number of requests, you can see that even though the server-side method hibernates for one second, resulting in an average delay of 1.18s per request, 166 requests can still be processed per second, proving that HTTP requests are a concurrent method of calling controllers instead of serial within 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

Implement singleton mode and simulate a large number of concurrent requests to verify thread security

Definition of a singleton class: 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();
    }

}

Test the case class and create a large number of requests with concurrent calls: SingletonTest.java

package com.demo.designpattern;

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

public class SingletonTest {

    public static void main(String[] args) {
        // Define a non-thread-safe Callback instance that returns the calculated results
        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());

        // Define a thread-safe Callback instance that returns calculated results (based on 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();
    }
}

Appendix: Spring Bean Scope

Range
describe
singleton (Default) Limits the scope of a single bean definition for each Spring IoC container to a single object instance.

In other words, when you define a bean and its domain is an instance, the Spring IoC container creates an instance of the object defined by the bean.The singleton is stored in the cache of the singleton beans and all subsequent requests and references to the named beans return the cached object.
prototype Limit the scope defined by a single bean to any number of object instances.

Each time a request is made for a particular bean, the bean prototype scope creates a new bean instance.That is, inject a Bean into another Bean, or you can call the getBean() method on the container to request it.Typically, prototype scopes should be used for all stateful beans and singleton scopes for stateless beans.
request Limits the scope defined by a single bean to the life cycle of a single HTTP request.That is, each HTTP request has a bean instance created after a single bean definition.Valid only in the Spring ApplicationContext context of web-aware.
session Limit the scope of a single bean definition to the life cycle of an HTTP Session.Valid only in the web-based Spring ApplicationContext context.
application Limit the scope of a single bean definition to the life cycle of the ServletContext.Valid only in the web-based Spring ApplicationContext context.
websocket Limit the scope defined by a single bean to the lifetime of a WebSocket.Valid only in the web-based Spring ApplicationContext context.

Posted by Joshua4550 on Fri, 27 Mar 2020 21:56:23 -0700