Problem of repetition

Suppose a typical Spring Boot Web project is on the line, and the processing logic for a piece of business is as follows:

Take a name string argument, assign that value to an injected bean object, modify the bean object’s name property and return, using Thread.sleep(300) to simulate time-consuming business on the line

The code is as follows:

@RestController @RequestMapping("name") public class NameController { @Autowired private NameService nameService; @RequestMapping("") public String changeAndReadName (@RequestParam String name) throws InterruptedException { System.out.println("get new request: " + name); nameService.setName(name); Thread.sleep(300); return nameService.getName(); }}Copy the code

The nameService mentioned above is also very simple, a normal Spring Service object

The specific code is as follows:

@Service public class NameService { private String name; public NameService() { } public NameService(String name) { this.name = name; } public String getName() { return name; } public NameService setName(String name) { this.name = name; return this; }}Copy the code

I believe that Spring Boot partners will not have any questions about this code, the actual operation is no problem, the test can also run, but really online, there will be a thread safety problem

If you don’t believe me, let’s test the NameController with 200 threads from the thread pool

The test code is as follows

    @Test
    public void changeAndReadName() throws Exception {
        ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(200, 300 , 2000, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(200));
        for (int i = 0; i < 200; i++) {
            poolExecutor.execute(new Runnable() {
                @Override
                public void run() {
                    try {
                        System.out.println(Thread.currentThread().getName() + " begin");
                        Map<String, String> headers = new HashMap<String, String>();
                        Map<String, String> querys = new HashMap<String, String>();

                        querys.put("name", Thread.currentThread().getName());
                        headers.put("Content-Type", "text/plain;charset=UTF-8");
                        HttpResponse response = HttpTool.doGet("http://localhost:8080",
                                "/name",
                                "GET",
                                headers,
                                querys);
                        String res = EntityUtils.toString(response.getEntity());

                        if (!Thread.currentThread().getName().equals(res)) {
                            System.out.println("WE FIND BUG !!!");
                            Assert.assertEquals(true, false);
                        } else {
                            System.out.println(Thread.currentThread().getName() + " get received " + res);
                        }
                    }catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            });
        }
        while(true) {
            Thread.sleep(100);
        }
    }Copy the code

This test code starts 200 threads and tests the NameController. Each thread submits its own thread name as an argument and asserts the returned result. If the returned value does not match the submitted value, an AssertNotEquals exception is thrown

After actual testing, we found that nearly half of the 200 threads threw exceptions

Cause of the problem

First of all let us analysis the, when a thread, send a request to http://localhost:8080/name, the online Spring Boot services, through its built-in Tomcat 8.5 to receive the request

In Tomcat 8.5, NIO is implemented by default, with one server thread per request, which is then allocated to the corresponding servlet to handle the request

So we can assume that the 200 concurrent client requests coming into the NameController to execute the requests are also being processed by 200 different server threads

However, Spring does not provide thread-safe Bean objects by default. By default, our NameController and NameService are singleton objects

It is not possible for 200 threads to operate on two singletons (one NameController and one NameService) at the same time without using any locking mechanism (except for state independent operations) without creating thread safety issues.

Problem solving

According to the title, I offer three solutions here, respectively

  • Synchronized modification method
  • Synchronized code block
  • Change the scope of the bean object

Follow with a description of each solution, including its pros and cons

Synchronized modification method

Using synchronized to modify the way thread-safety issues can occur is probably the easiest and simplest solution, We simply add synchronized to public String changeAndReadName (@requestParam String Name)

Real test, that does solve the problem, but I wonder if you could think about one more question

When we run the test code again, we find that the program is much less efficient because each thread must wait for the previous thread to complete all the logic for the changeAndReadName() method. In this logic, Thread.sleep(300) is included, but has nothing to do with Thread safety

In this case, we can use the second method to solve the problem

Synchronized code block

In real online logic, it is often the case that the code that needs to be thread-safe is written in the same method as the code that needs to be time-consuming, such as calling third-party apis

In this case, it’s much more efficient to use synchronized blocks rather than direct modifications

The specific solution code is as follows:

    @RequestMapping("")
    public String changeAndReadName (@RequestParam String name) throws InterruptedException {
        System.out.println(Thread.currentThread().getName() + " get new request: " + name);
        String result = "";
        synchronized (this) {
            nameService.setName(name);
            result = nameService.getName();
        }
        Thread.sleep(300);
        return result;
    }Copy the code

Running the test code again, we can see that the efficiency problem is basically solved, but the disadvantage is that we need to figure out which parts of the code are likely to have thread-safety problems (and the actual on-line logic can be very complex and difficult to grasp).

Change the scope of the bean object

Now the unfortunate thing is that even the most time-consuming code is state dependent, and efficiency needs to be maintained, so the problem can only be solved by sacrificing a small amount of memory

The idea is to get around thread-safety issues by changing the scope of the bean object so that each server thread has a new bean object to handle the logic and is unrelated to each other

First we need to know what the scope of the bean object is, as shown in the table below

scope instructions
singleton The default scope, in which case the bean is defined as a singleton object with a life cycle consistent with the Spring IOC container (but created only when first used due to Spring lazy loading)
prototype A bean is defined to create a new object each time it is injected
request The bean is defined to create a singleton in each HTTP request, meaning that this singleton is reused in a single request
session A bean is defined to create a singleton object during the lifetime of a session
application The bean is defined to reuse a singleton object in the lifetime of the ServletContext
              websocket              

A bean is defined to reuse a singleton object over the life of a Websocket

Now that we know the scope of our bean objects, there is only one question to consider: which beans are scoped to change?

As I explained earlier, in this case, 200 server threads operate by default on two singleton bean objects, NameController and NameService (yes, Controller is also a singleton by default under Spring Boot).

Set NameController and NameServie to prototype.

This is fine if your project is using Struts2, but it can seriously impact performance under Spring MVC because Struts2 intercepts requests based on classes, whereas Spring MVC is based on methods

So we should fix this by setting NameController’s scope to Request and NameService to Prototype

The specific operation code is as follows

@RestController
@RequestMapping("name")
@Scope("request")
public class NameController {

}Copy the code

@Service
@Scope("prototype")
public class NameService {

}
Copy the code

reference

  • https://dzone.com/articles/understanding-spring-reactiveclient-to-server-comm
  • https://dzone.com/articles/understanding-spring-reactive-servlet-async
  • https://medium.com/sipios/how-to-make-parallel-calls-in-java-springboot-application-and-how-to-test-them-dcc27318a0cf

Original is not easy, reprint please state the source

Case project code: Github /liumapp/booklet