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
andScopedValue
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
Feature | ThreadLocal | ScopedValue |
Storage | Per thread | Explicit scope |
Cleanup | Manual | Automatic |
Use case | Thread-wide state | Context 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.
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.