Is reactive programming changing again? See how JDK21 virtual threads are subverted!

This article explains why reactive programming exists, why it is less popular among developers, and why it may eventually disappear with the introduction of Java virtual threads.

Imperative style programming has always been loved by developers. Structures such as if-then-else, while loops, functions, and code blocks make the code easy to understand, debug, and exceptions easy to track. However, like all good things, there usually are problems. This programming style causes threads to be blocked for much longer than necessary.

1 Synchronous blocking design

1.1 Thread graph of synchronous blocking design

To make it easier for you to understand, let’s look at a typical enterprise use case request:

  • Get data from DB
  • Get data from web service
  • Merge the results and send the final merged result back to the user

In an application server like Tomcat, a platform thread will be dedicated to user requests, and this thread will continue to call the code that gets the data from the database (callFetchDataFromDB ), then calls the code that gets the data from the Web service (calling FetchDataFromService), and then continues merging and sending the data to the user (calling SendDataToUser).

As shown in the figure below, the execution thread is represented as a vertical arrow from top to bottom:

  • Green is the CPU portion of execution
  • Red is the time the thread waits for data

Most enterprise applications are IO bound, so threads are actually wasting resources most of the time.

1.2 Assessment

In Java, platform threads are expensive resources because, by default, each platform thread consumes 1MB of stack memory. That is, there is an upper limit on the number of platform threads running in the JVM. Therefore, if a platform thread is dedicated to user requests, it will cause problems for applications with high concurrent users. The traditional solution is to create a thread pool with a maximum number of threads and scale the application horizontally or vertically as needed:

  • Vertical scaling means adding more resources to a container or VM
  • Horizontal scaling means adding more instances of the application

2 Asynchronous blocking design

2.1 Asynchronous blocking design thread diagram

In order to improve performance, an asynchronous model can be used to run some serial tasks in parallel. Assuming that the database and web service fetch tasks can run in parallel, they can be executed in their own platform threads.

image-20231207103631608

The user request thread starts two threads:

  • One to handle getting data from the database
  • Another one for getting data from web service
  • It will then block to get both results and then continue merging and sending the data to the user

In Java, this can be achieved by submitting a Callable or Runnable task to the Executor Service and using Java Futures.

2.2 Assessment

This will improve performance since the two data fetches are performed in parallel. However, even though there may be a performance gain most of the time, for a short period of time the number of platform threads now increases from 1 to 3. From a scalability perspective, things were even worse during that time.

3 Responsive style design

Responsive programming is designed to solve this problem.

3.1 Part of the responsive design thread diagram

is that expensive platform threads waste most of their time during blocking operations. Introduced with Servlet 3.0 and 3.1, the Servlet thread sends HTTP data back The user does not need to be kept active, which opens the door to more clever programming to solve thread blocking. Java 8 CompletableFuture class where reactive pipelines can be created. The idea behind this development style is to specify an execution pipeline for the use case, rather than executing the use case itself.

The user requesting thread simply specifies the use case's CompletableFuture pipe (or any other pipe) and releases it back to the thread pool in the shortest possible time (since it no longer needs to be kept alive to send data to the user).

At this point, the user request thread creates a pipeline that runs 3 activities:

  • First run FetchDataFromService and FetchDataFromDB in parallel
  • Run Send2User again

But after this pipe is created, the user requesting thread will simply be released back to the thread pool. Greatly relieves the JVM as it now has one less thread to deal with. Once the data extraction thread completes its execution, the data will be sent to the user.

Evaluate

But this is only a partial solution to the problem, because the actual activity of getting data from the web service and DB is still blocked in their respective platform threads. This creates a problem: the SE must ensure that the tasks it generates from the pipeline are not blocking. This is difficult to do because it's done manually, and is definitely wrong since it won't be marked as a warning or error at compile time or run time.

4 Fully responsive style design

How can I perform better? What about reaching a higher standard for OKRs? In order to make this design fully responsive, data from DB and Web services must be obtained in a non-blocking manner.

As part ofJDK 7, NIO (New IO) Open the door to non-blocking IO. All Java-based IO classes and methods now have non-blocking versions. Such as socket read/write, file read/write, lock API, etc. Non-blocking versions of these classes/methods or libraries that support NIO must be used to obtain data.

4.1 Fully responsive style design thread diagram

In each Fetch Data that obtains data, the thread that makes the request and the thread that obtains the data are different, such as:

  • HTTP GET requests to retrieve data from the web service will run on a thread
  • And the thread that ultimately processes the retrieved data will run on another thread

This is fully reactive, which solves the key problem: no blocking during IO operations. The only time platform threads are used here is during CPU operations, not during IO operations. There is no longer any red part visible in the execution part of the platform thread.

This development style enables applications to be highly scalable. However, the solution is overly complex. Creating reactive pipelines, debugging them, and imagining their execution is difficult. So it's normal that this development style hasn't caught on, and only top developers can't put it down. Spring Boot A complete development technology stack dedicated to responsive programming, namely Spring WebFlux, which uses the Project Reactor library to provide non-blocking behavior for DB, Web services, etc.

5 virtual threads

Are there any alternatives to responsive design? Of course! Now with the release ofJava 21, Oracle launches the highly anticipatedVirtual Threads Function.

The problem with platform threads is that during blocking operations, they become effectively useless. Platform threads are basically simple wrappers for os threads. After all, os threads are expensive.

The virtual thread is the implementation of the Thread class in the JVM, and it is lightweight. Ultimately it comes down to this - when using a virtual thread for code execution, it will use the platform thread (called the carrier thread) during CPU operations, and release the carrier thread when it encounters an IO operation.

How does the JVM know when it encounters an IO operation?

When running in a virtual thread, the JVM will automatically switch to using a non-blocking version of IO operations. This change has been painfully modified in most core Java class libraries for most IO operations. When the code encounters an IO operation, the carrier thread is released, and when data for that IO becomes available, the virtual thread is rescheduled to process the data on another carrier thread. That is, blocking in the virtual thread is not a problem because the underlying carrier thread is released.

SE now has the option to use virtual threads for user requests. That is, SE can continue to use the same imperative development style as before, while getting the scalability benefits (but without the complexity) that you get when using reactive pipelines.

Synchronous blocking thread graph with virtual threads

This is the case in a synchronous blocking design (note that blocking is not an issue).

The user request thread is a virtual thread (blue vertical arrow). Red on threads is no longer an issue because during blocking operations, the underlying carrier thread is released, achieving the same scalability benefits as using a reactive framework.

6 Virtual threads and asynchronous blocking design

6.1 Virtual threads in asynchronous blocking designs

blocking is no longer a problem here. As mentioned earlier it can be implemented using Java Futures, and we do have the option to do so. But Java 21 introduces StructuredTaskScope and Subtask to handle structured asynchronous behavior .

The combination of Virtual Threads and StructuredTaskScope will be very powerful. Virtual threads make blocking less of an issue, and StructuredTaskScope will give us higher-level classes to handle asynchronous programming in an intuitive way. It's hard to see why Completable Futures are still needed.

Virtual thread V.S responsive framework

  • You can continue to use the imperative style of development
  • No need to create complex reactive pipelines
  • No need to use non-blocking IO directly in your code
  • Easier to code, debug and understand

7 Summary

With the introduction ofvirtual threads in Java 21, Virtual threads are no longer a problem in a blocked state. Developer:

  • No need to create complex responsive style pipelines
  • And there is no need to use non-blocking IO directly in the code

to create highly scalable applications. The alternative is to use Virtual Threads introduced in Java 21 with Java Futures or a combination of Structured Concurrency (preview feature in Java 21) classes.

reference:

Guess you like

Origin blog.csdn.net/qq_33589510/article/details/134854816