Java Virtual Threads for high-performance Java Applications

Java virtual threads are a new type of thread that was introduced in Java 19 as a preview feature and is now available as a standard feature in Java 21. Virtual threads are managed by the JVM and do not require an equivalent number of platform threads or operating system threads. This means that virtual threads are much more lightweight than traditional threads and can be used to scale applications to thousands or even millions of concurrent tasks.

Virtual threads are especially well-suited for applications that perform a lot of I/O, such as web servers, database applications, and microservices. This is because virtual threads can be suspended while waiting for I/O to complete, without blocking the underlying platform thread or OS thread. This allows the JVM to continue running other virtual threads on the same platform thread, which can significantly improve performance

How to create virtual threads: ( All the samples are compiled & ran on Java 21)

        // various ways of creating virtual threads

        {
            // example 1
            var runningThreead = Thread.startVirtualThread(() -> {
                //Code to execute in virtual thread
                System.out.println(STR. "Is Virtual Thread: \{ Thread.currentThread().isVirtual() } ,Name:\{Thread.currentThread() }" );
            });
            runningThreead.join();
        }
        {
            // example 2
            Runnable runnable = () -> System.out.println(STR. "Is Virtual Thread: \{ Thread.currentThread().isVirtual() } ,Name:\{Thread.currentThread() }" );
            Thread virtualThread = Thread.ofVirtual().start(runnable);
            virtualThread.join();
        }
        {
             // example 3
            // Using Executors.newVirtualThreadPerTaskExecutor()
            try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
                    executor.submit(() -> {
                        System.out.println(STR. "Is Virtual Thread: \{ Thread.currentThread().isVirtual() } ,Name:\{Thread.currentThread()}" );
                        try {
                            Thread.sleep(Duration.ofSeconds(1));
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }
                    });

            }
        }

Following example demonstrate the use of Java Virtual Threads in simple TCPEcho server & Client implementation.

Echo server that uses that uses Executors.newVirtualThreadPerTaskExecutor() to handle large number of concurrent connections without creating large platform or OS threads

package org.example;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.logging.ConsoleHandler;
import java.util.logging.Logger;

public class EchoServer {
    private static Logger logger = Logger.getLogger("EchoServer");

    public static void main(String[] args) {
        CountDownLatch countDownLatch = new CountDownLatch(1);
        // Capture shutdown requests from the Virtual Machine.
        // This can occur when a user types Ctrl+C at the console
        Runtime.getRuntime().addShutdownHook(new Thread() {
            @Override
            public void run() {
                countDownLatch.countDown();
            }
        });


        try (var serverSocket = new ServerSocket(4444);
             var executors = Executors.newVirtualThreadPerTaskExecutor()) {
            logger.info("Accepting incoming connections on port " + serverSocket.getLocalPort());
            while (countDownLatch.getCount() != 0) {
                var clientSocket = serverSocket.accept();
                logger.info("Accepted connection from " + clientSocket.getRemoteSocketAddress());
                // submit Runnable task to  VirtualThread Executor
                executors.submit(new ClientHandler(clientSocket));
            }
            serverSocket.close();
            logger.info("Stopped accepting incoming connections.");
        } catch (IOException e) {
            throw new RuntimeException(e);
        }

    }

    /**
     * Handle the connected client on VirtualThread Executor
     */
    public static class ClientHandler implements Runnable {

        private final Socket clientSocket;

        public ClientHandler(Socket clientSocket) {
            this.clientSocket = clientSocket;
        }

        @Override
        public void run() {
            try (var out = new PrintWriter(clientSocket.getOutputStream(), true);
                 var in = new BufferedReader(
                         new InputStreamReader(clientSocket.getInputStream()));) {

                String inputLine;
                while ((inputLine = in.readLine()) != null) {
                    if (".".equals(inputLine)) {
                        out.println("bye");
                        break;
                    }
                    out.println(inputLine.toUpperCase());
                }
            } catch (IOException e) {
                EchoServer.logger.warning("Lost connection to " + this.clientSocket.getRemoteSocketAddress());
            } finally {
                // close the client socket using try-with resource
                try (clientSocket) {
                    EchoServer.logger.info("Closing  connection to " + this.clientSocket.getRemoteSocketAddress());
                } catch (IOException e) {
                    EchoServer.logger.severe("Exception while closing the connection  " + this.clientSocket.getRemoteSocketAddress());
                }
            }

        }
    }
}

Test application that creates large number of connection to EchoServer.

package org.example;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.time.Duration;
import java.time.Instant;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executors;
import java.util.logging.Logger;
import java.util.stream.IntStream;

public class TestApplication {
    private static Logger logger = Logger.getLogger("SimpleEchoClient");

    public static void main(String[] args) {
        int numberOfClients = 100;
        int numberOfMessagePerClient = 100;
        String ipAddresOrHostName = "localhost";
        int port = 4444;
        try (var executors = Executors.newVirtualThreadPerTaskExecutor()) {
            IntStream.range(0, numberOfClients).forEach(idx -> {
                executors.submit(new EchoClient(ipAddresOrHostName, port, numberOfMessagePerClient));
            });

        }
        logger.info("Done");

    }

    /**
     * Client Application that connects to EchoServer
     * and sends number of messages
     */
    public static class EchoClient implements Runnable {
        private final String ip;
        private final int port;
        private final int numberOfMessages;
        public EchoClient(String ip, int port, int numberOfMessages) {

            this.ip = ip;
            this.port = port;
            this.numberOfMessages = numberOfMessages;
        }
        @Override
        public void run() {
            try (var clientSocket = new Socket(ip, port);
                 var out = new PrintWriter(clientSocket.getOutputStream(), true);
                 var in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));) {
                for (int index = 0; index < numberOfMessages; index++) {

                    out.println("Random messsage " + Instant.now().toString());
                    String resp = in.readLine();
                    logger.info("[Msg from server] " + resp);
                    // introduce delay to simulate the IO
                    Thread.sleep(Duration.ofMillis(500));
                }
            } catch (IOException e) {
                logger.severe("Unable to connect to remote client or Unable to read/write to connected client " + e.getMessage());
            } catch (InterruptedException e) {
                logger.severe("InterruptedException" + e.getMessage());
            }
        }
    }
}

Conclusion:

Java virtual threads are a powerful new feature that can be used to write scalable and efficient concurrent applications. Virtual threads are easy to use and do not require any special programming techniques. If you are developing a Java application that needs to handle a large number of concurrent tasks, then you should consider using virtual threads.

Reference:

Java 21 new feature: Virtual Threads

Beyond Loom: Weaving new concurrency patterns

Java Virtual Threads

JEP 444: Virtual Threads