Platform Threads, Virtual Threads, and Structured Concurrency

Java has undergone significant improvements in multithreading and concurrency management. With the introduction of Virtual Threads and Structured Concurrency in Project Loom, developers can now handle concurrency more efficiently. This article explores:

  • Platform Threads vs. Virtual Threads

  • Structured Concurrency

  • Sharing Data Between Threads

  • Using ThreadLocal and ScopedValue

Platform Threads

Platform threads are the traditional Java threads managed by the operating system. They rely on kernel threads and are mapped one-to-one to OS threads.

public class PlatformThreadExample {
    public static void main(String[] args) {
        Runnable task = () -> {
            System.out.println("Running in a platform thread: " + Thread.currentThread());
        };

        Thread thread = new Thread(task);
        thread.start();
    }
}

Pros:

  • Well-established and widely used.

  • Leverages OS scheduling.

  • Suitable for CPU-bound tasks.

Cons:

  • Creating too many threads incurs overhead.

  • Expensive context switching.

Virtual Threads

Virtual Threads, introduced in Java 19 (preview) and finalized in Java 21, are lightweight threads managed by the JVM, not the OS.

public class VirtualThreadExample {
    public static void main(String[] args) {
        Runnable task = () -> {
            System.out.println("Running in a virtual thread: " + Thread.currentThread());
        };

        Thread.startVirtualThread(task);
    }
}

Pros:

  • Extremely lightweight.

  • Allows creating millions of concurrent tasks.

  • Reduces context-switching overhead.

Cons:

  • Not ideal for CPU-bound tasks.

  • Requires adapting existing thread-based applications.

Structured Concurrency

Structured Concurrency simplifies thread management by ensuring that threads are properly managed and terminated.

import java.util.concurrent.*;

public class StructuredConcurrencyExample {
    public static void main(String[] args) throws InterruptedException, ExecutionException {
        try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
            Future<String> future1 = executor.submit(() -> "Task 1 Result");
            Future<String> future2 = executor.submit(() -> "Task 2 Result");

            System.out.println(future1.get());
            System.out.println(future2.get());
        }
    }
}
  • Ensures child tasks complete before the parent exits.

  • Prevents resource leaks and orphaned threads.

  • Reduces complexity in managing concurrent operations.


Sharing Data Between Threads

Using Shared Variables (Synchronization Required):

class SharedData {
    private int counter = 0;

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

    public synchronized int getCounter() {
        return counter;
    }
}

public class SharedDataExample {
    public static void main(String[] args) throws InterruptedException {
        SharedData data = new SharedData();
        Thread t1 = new Thread(data::increment);
        Thread t2 = new Thread(data::increment);

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

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

        System.out.println("Final Counter: " + data.getCounter());
    }
}

Using Concurrent Collections:

import java.util.concurrent.ConcurrentHashMap;

public class ConcurrentCollectionExample {
    public static void main(String[] args) {
        ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
        map.put("Key1", 1);

        Runnable task = () -> map.compute("Key1", (k, v) -> v + 1);

        Thread t1 = new Thread(task);
        Thread t2 = new Thread(task);

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

        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("Final Value: " + map.get("Key1"));
    }
}

ThreadLocal and ScopedValue

Using ThreadLocal

ThreadLocal allows each thread to maintain its own independent variable copy.

public class ThreadLocalExample {
    private static final ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);

    public static void main(String[] args) {
        Runnable task = () -> {
            threadLocal.set(threadLocal.get() + 1);
            System.out.println("Thread " + Thread.currentThread().getId() + " Value: " + threadLocal.get());
        };

        new Thread(task).start();
        new Thread(task).start();
    }
}

Using ScopedValue

ScopedValue (introduced in Java 21) provides an alternative to ThreadLocal, reducing memory leaks.

import java.lang.ScopedValue;

public class ScopedValueExample {
    private static final ScopedValue<String> CONTEXT = ScopedValue.newInstance();

    public static void main(String[] args) {
        ScopedValue.where(CONTEXT, "Scoped Value Example").run(() -> {
            System.out.println("Value: " + CONTEXT.get());
        });
    }
}

ThreadLocal vs. ScopedValue

FeatureThreadLocalScopedValue
StoragePer threadExplicit scope
CleanupManualAutomatic
Use caseThread-wide stateContext passing

Conclusion

With Java's advancements in concurrency, developers now have better tools for managing threads efficiently. Virtual Threads offer lightweight execution, Structured Concurrency simplifies task management, and ScopedValue improves context propagation. Understanding these features can help build scalable, high-performance applications.

0
Subscribe to my newsletter

Read articles from Ali Rıza Şahin directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Ali Rıza Şahin
Ali Rıza Şahin

Product-oriented Software Engineer with a solid understanding of web programming fundamentals and software development methodologies such as agile and scrum.