The Evolution of Java MultiThreading: From Threads to Virtual Threads

Multi-threading in Java has been a cornerstone for developing high - performance and responsive applications. It allows programs to execute multiple tasks concurrently, making the most of multi - core processors. Over the years, Java’s multi - threading capabilities have evolved significantly. Starting from the basic Thread class, which was the traditional way to handle concurrent execution, Java has now introduced virtual threads in Java 19 (incubating) and made them standard in Java 21. This blog will explore the journey of Java multi - threading from traditional threads to the new and exciting virtual threads.

Table of Contents

  1. Traditional Threads in Java
    • Fundamental Concepts
    • Usage Methods
    • Common Practices and Best Practices
  2. Introduction to Virtual Threads
    • Fundamental Concepts
    • Usage Methods
    • Common Practices and Best Practices
  3. Comparison between Traditional Threads and Virtual Threads
  4. Conclusion
  5. References

1. Traditional Threads in Java

Fundamental Concepts

In Java, a thread is an independent path of execution within a program. Each thread has its own call stack, program counter, and local variables. The Thread class in Java represents a thread of execution. When you create an instance of the Thread class and call its start() method, a new thread of execution is spawned.

Usage Methods

There are two common ways to create and start a thread in Java:

Extending the Thread class

class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("Running thread: " + Thread.currentThread().getName());
    }
}

public class TraditionalThreadExample1 {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.start();
    }
}

Implementing the Runnable interface

class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("Running thread: " + Thread.currentThread().getName());
    }
}

public class TraditionalThreadExample2 {
    public static void main(String[] args) {
        MyRunnable myRunnable = new MyRunnable();
        Thread thread = new Thread(myRunnable);
        thread.start();
    }
}

Common Practices and Best Practices

  • Resource Management: Threads are expensive in terms of system resources. Each thread requires its own stack space, and creating too many threads can lead to OutOfMemoryError. So, it’s important to limit the number of threads.
  • Synchronization: When multiple threads access shared resources, synchronization is required to avoid race conditions. You can use synchronized blocks or methods, or more advanced concurrency utilities like ReentrantLock.
class Counter {
    private int count = 0;

    public synchronized void increment() {
        count++;
    }

    public int getCount() {
        return count;
    }
}

public class SynchronizationExample {
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        });

        t1.start();
        t2.start();

        t1.join();
        t2.join();

        System.out.println("Final count: " + counter.getCount());
    }
}

2. Introduction to Virtual Threads

Fundamental Concepts

Virtual threads are lightweight threads managed by the Java Virtual Machine (JVM). Unlike traditional threads, which are mapped to operating - system threads, virtual threads are multiplexed over a small number of operating - system threads. This allows the JVM to run a large number of virtual threads efficiently, as the overhead of creating and managing virtual threads is much lower.

Usage Methods

To create and start a virtual thread, you can use the Thread.startVirtualThread() method or the Executors.newVirtualThreadPerTaskExecutor() method.

Using Thread.startVirtualThread()

public class VirtualThreadExample1 {
    public static void main(String[] args) {
        Thread.startVirtualThread(() -> {
            System.out.println("Running virtual thread: " + Thread.currentThread().getName());
        });
    }
}

Using Executors.newVirtualThreadPerTaskExecutor()

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class VirtualThreadExample2 {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
        executor.submit(() -> {
            System.out.println("Running virtual thread: " + Thread.currentThread().getName());
        });
        executor.shutdown();
    }
}

Common Practices and Best Practices

  • I/O - Bound Tasks: Virtual threads are especially useful for I/O - bound tasks, such as network requests or file operations. Since virtual threads can be parked and unparked efficiently, they can handle a large number of concurrent I/O operations without consuming excessive resources.
  • Avoid Blocking: Although virtual threads are designed to handle blocking operations better than traditional threads, it’s still a good practice to avoid long - running blocking operations in virtual threads. If a virtual thread blocks for a long time, it can reduce the overall efficiency.

3. Comparison between Traditional Threads and Virtual Threads

FeatureTraditional ThreadsVirtual Threads
Resource OverheadHigh. Each thread has its own stack space and is mapped to an operating - system thread.Low. Managed by the JVM and multiplexed over a small number of operating - system threads.
ScalabilityLimited. Creating too many threads can lead to resource exhaustion.High. Can handle a large number of concurrent tasks efficiently.
Use CasesCPU - bound tasks where the thread is actively using the CPU for a long time.I/O - bound tasks where the thread spends most of its time waiting for I/O operations.

Conclusion

The evolution of Java multi - threading from traditional threads to virtual threads is a significant step forward. Traditional threads have served Java developers well for many years, but they have limitations in terms of resource consumption and scalability. Virtual threads, on the other hand, offer a more efficient way to handle concurrent tasks, especially I/O - bound ones. By understanding the concepts, usage methods, and best practices of both traditional and virtual threads, developers can make informed decisions on which approach to use in different scenarios.

References