Sunday, December 8, 2024

Virtual Threads in Java 21: Simplified Concurrency for Modern Applications

 

With Java 21, Virtual Threads have redefined how we approach concurrency, offering a lightweight and efficient way to handle parallel and asynchronous tasks. Unlike CompletableFuture or ParallelStream, Virtual Threads allow developers to write simple, synchronous-looking code while achieving the scalability of asynchronous solutions.




Key Features of Virtual Threads:

  • Lightweight Concurrency: Virtual Threads are cheap to create and manage, enabling applications to handle thousands or even millions of concurrent tasks.
  • Simpler Programming Model: Code written with Virtual Threads resembles traditional synchronous code, making it easier to understand and maintain.
  • Automatic Thread Management: The JVM manages Virtual Threads' scheduling, eliminating the need to manually manage thread pools.
  • Great for IO-Bound Tasks: Virtual Threads excel in handling IO-bound workloads, such as making multiple HTTP calls concurrently.

Example: Virtual Threads with HTTP Requests

Here’s how you can use Virtual Threads to fetch posts concurrently:

Example: Fetch Posts Using Virtual Threads


import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.concurrent.*;

public class VirtualThreadExample {

    public static void main(String[] args) {
        try (ExecutorService myExecutor = Executors.newVirtualThreadPerTaskExecutor()) {
            // List of posts to process
            List<Integer> posts = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20);
            long start = System.nanoTime();

            // Submit a task for each post
            List<Future<Object>> futures = posts.stream()
                    .map(post -> myExecutor.submit(() -> {
                        getResponse("https://jsonplaceholder.typicode.com/posts/" + post);
                        return null; // Explicitly return null for Future<Void>
                    }))
                    .toList();

            // Wait for all tasks to complete
            for (Future<Object> future : futures) {
                future.get(); // Ensures task completion
            }

            long duration = (System.nanoTime() - start) / 1_000_000;
            System.out.printf("Processed %d posts in %d millis%n", posts.size(), duration);
            System.out.println("Program Completed !!");
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
    }

    public static void getResponse(String urlRest) {
        try {

            InputStream is = getInputStream(urlRest);

            BufferedReader rd = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8));
            StringBuilder response = new StringBuilder();
            String line;
            while ((line = rd.readLine()) != null) {
                response.append(line);
            }
            rd.close();
            System.out.println("Response: " + response.toString().trim());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private static InputStream getInputStream(String urlRest) throws IOException {
        URL url = new URL(urlRest);
        HttpURLConnection connection = (HttpURLConnection) url.openConnection();
        connection.setRequestMethod("GET");
        connection.setRequestProperty("Content-Type", "application/json");
        connection.setRequestProperty("Content-Language", "en-US");
        connection.setConnectTimeout(60000);
        connection.setUseCaches(false);
        connection.setDoInput(true);
        connection.setDoOutput(true);

        return connection.getResponseCode() >= 400
                ? connection.getErrorStream()
                : connection.getInputStream();
    }
}

Performance and Simplicity

Output:

Processed 20 posts in 971 millis
Program Completed !!


Example: Asynchronous Task with CompletableFuture.runAsync()

Virtual Threads also work seamlessly with CompletableFuture, enabling fire-and-forget asynchronous tasks. Here’s an example of saving user data asynchronously:



import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executors;
import java.util.concurrent.Executor;

public class AsyncExample {

    static class UserRepository {
        public void saveUser(String user) {
            System.out.println("Saving user: " + user);
            try {
                Thread.sleep(1000); // Simulate database latency
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                throw new RuntimeException("Thread interrupted while saving user", e);
            }
            System.out.println("User saved: " + user);
        }
    }

    public static void main(String[] args) {
        Executor virtualThreadExecutor = Executors.newVirtualThreadPerTaskExecutor();
        String user = "JohnDoe";

        // Run the task asynchronously
        CompletableFuture.runAsync(() -> {
            System.out.println("Starting async task for user: " + user);
            UserRepository repository = new UserRepository();
            repository.saveUser(user);
            System.out.println("Async task completed for user: " + user);
        }, virtualThreadExecutor)
        .exceptionally(ex -> {
            System.err.println("Failed to save user: " + user);
            ex.printStackTrace();
            return null;
        });

        System.out.println("Main thread continues executing...");
        try {
            Thread.sleep(2000); // Simulate main thread work
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Main thread finished.");
    }
}


Output:

Main thread continues executing...
Starting async task for user: JohnDoe
Saving user: JohnDoe
User saved: JohnDoe
Async task completed for user: JohnDoe
Main thread finished.

Scalability and Readability


Scalability: Virtual Threads scale seamlessly for handling a large number of requests.

Readability: The synchronous-looking code is easy to understand and maintain.


Automatic Resource Management

The ExecutorService created using Executors.newVirtualThreadPerTaskExecutor() in the try-with-resources block will automatically be shut down when the block is exited. This is because ExecutorService implements the AutoCloseable interface, and in a try-with-resources block, the close() method is automatically called.

If you're using the ExecutorService outside of a try-with-resources block, you must explicitly shut it down. This ensures that all threads are properly terminated and resources are released.

Why Virtual Threads?

  1. No Thread Pool Management: Virtual Threads handle concurrency without the need for explicit thread pools or tuning.
  2. Efficient Resource Usage: They use far less memory compared to platform threads.
  3. Modern and Forward-Looking: As of Java 21, Virtual Threads are the future of Java concurrency, making them the ideal choice for most applications.

If you’re still on Java 8 or 17, you might use CompletableFuture or ParallelStream. But with Java 21, Virtual Threads are the most straightforward and efficient way to handle concurrent tasks. Whether you’re making HTTP requests, processing data, or performing database operations, Virtual Threads simplify concurrency while delivering exceptional performance.








No comments:

Post a Comment

Integrating Ollama with DeepSeek-R1 in Spring Boot

Are you looking to leverage the power of Ollama and DeepSeek-R1 in your Spring Boot application? This post will walk you through the entire ...