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