Java Threading Essentials: Virtual vs. Platform Threads Explained

M K Pavan Kumar
Stackademic
Published in
7 min readJan 8, 2024

--

In this article we will learn some concepts about threads and more recent features called virtual threads. How this platform threads and virtual threads differ in nature and how they contribute to the performance improvements of the application.

created by M K Pavan Kumar

Classic Thread Background:

Let us take a scenario of calling external API or some Database interaction and see the thread life cycle of execution.

  1. Threads created and ready to serve in the memory.
  2. Once request approached it is mapped to one of the threads and then served by calling external API or some Database query execution.
  3. Thread waits until it gets the response from the service or database.
  4. once the response is received it execute the post response activity and returns back to the pool.
created by M K Pavan Kumar

Observe in the above lifecycle, the step 3 where the thread is just waiting and doing nothing. This is a major drawback and an underutilization of the system resources by just waiting, and most of the threads in their life cycle they just wait for the responses and do nothing.

before Java 19 or above the standard way of creating threads or the threads that exist are called the Native Threads or Platform Threads In this architecture style there is a 1-on-1 mapping between Platform Thread and OS Thread. This means theOperating System Thread is underutilized as it just waits for the activity to complete and do nothing, hence making them heavy and costly.

The virtual Threads:

Virtual threads in Java represent a significant evolution in the way Java handles concurrency and multithreading. Introduced as part of Project Loom, an initiative by Oracle to address the challenges of writing, maintaining, and observing high-throughput concurrent applications, virtual threads are designed to be lightweight and to make concurrency easier for developers.

Virtual threads are lightweight threads managed by the Java Virtual Machine (JVM) rather than the operating system. Unlike platform threads, virtual threads are cheap to create and destroy. They are mapped to a smaller number of platform threads, enabling Java applications to handle thousands or even millions of tasks concurrently with a much lower resource footprint.

Usefulness of Virtual Threads over Platform Threads

The advantages of virtual threads in Java are numerous. Firstly, they enable more efficient use of system resources. Because virtual threads are lightweight, they consume less memory and CPU resources compared to platform threads. This efficiency allows for a higher degree of concurrency, making it possible to run a large number of concurrent tasks on a single JVM.

Secondly, virtual threads simplify concurrent programming in Java. They allow developers to write code in a straightforward, imperative style, similar to how they would write synchronous code, rather than dealing with the complexities of asynchronous programming models. This simplicity reduces the likelihood of common concurrency-related bugs, such as deadlocks and race conditions.

Furthermore, virtual threads facilitate better CPU utilization. In traditional thread models, a lot of CPU time can be wasted in managing and context-switching between a large number of threads. Virtual threads reduce the overhead associated with context switching, allowing for more efficient execution of concurrent tasks.

The Practical:

If we need want to create classic platform threads to get the things done, then we can do something as below. create a file called PlatformThreadDemo.java and copy the content as below.

package org.vaslabs;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class PlatformThreadDemo {
private static final Logger logger = LoggerFactory.getLogger(PlatformThreadDemo.class);

public static void main(String[] args) {
attendMeeting().start();
completeLunch().start();
}

private static Thread attendMeeting(){
var message = "Platform Thread [Attend Meeting]";
return new Thread(() -> {
logger.info(STR."{} | \{message}", Thread.currentThread());
});
}

private static Thread completeLunch(){
var message = "Platform Thread [Complete Lunch]";
return new Thread(() -> {
logger.info(STR."{} | \{message}", Thread.currentThread());
});
}

// using builder pattern to create platform threads
private static void attendMeeting1(){
var message = "Platform Thread [Attend Meeting]";
Thread.ofPlatform().start(() -> {
logger.info(STR."{} | \{message}", Thread.currentThread());
});
}

private static void completeLunch1(){
var message = "Platform Thread [Complete Lunch]";
Thread.ofPlatform().start(() -> {
logger.info(STR."{} | \{message}", Thread.currentThread());
});
}
}

The above example shows 2 ways of creating platform threads,
1. by using the Thread constructor and passing a runnable lambda to it
2. by using the Thread’s builder method ofPlatform()

Let us see more complex coding by creating some concurrent virtual threads. create a file named DailyRoutineWorkflow.java and copy the below code into it.

package org.vaslabs;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.concurrent.TimeUnit;

public class DailyRoutineWorkflow {
static final Logger logger = LoggerFactory.getLogger(DailyRoutineWorkflow.class);

static void log(String message) {
logger.info(STR."{} | \{message}", Thread.currentThread());
}


private static void sleep(Long duration) throws InterruptedException {
TimeUnit.MILLISECONDS.sleep(duration);
}

private static Thread virtualThread(String name, Runnable runnable) {
return Thread.ofVirtual().name(name).start(runnable);
}

static Thread attendMorningStatusMeeting() {
return virtualThread(
"Morning Status Meeting",
() -> {
log("I'm going to attend morning status meeting");
try {
sleep(1000L);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
log("I'm done with morning status meeting");
});
}

static Thread workOnTasksAssigned() {
return virtualThread(
"Work on the actual Tasks",
() -> {
log("I'm starting my actual work on tasks");
try {
sleep(1000L);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
log("I'm done with actual work on tasks");
});
}

static Thread attendEveningStatusMeeting() {
return virtualThread(
"Evening Status Meeting",
() -> {
log("I'm going to attend evening status meeting");
try {
sleep(1000L);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
log("I'm done with evening status meeting");
});
}

static void concurrentRoutineExecutor() throws InterruptedException {
var morningMeeting = attendMorningStatusMeeting();
var actualWork = workOnTasksAssigned();
var eveningMeeting = attendEveningStatusMeeting();
morningMeeting.join();
actualWork.join();
eveningMeeting.join();
}
}

The above code shows creation of Virtual Threads using factory methods. apart from the factory methods, we can also implement virtual threads using the java.util.concurrent.ExecutorService tailored on virtual threads, called java.util.concurrent.ThreadPerTaskExecutor . You can get the exactly the same functionality as above using ExecutorService as shown below.

package org.vaslabs;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class DailyRoutineWorkflowUsingExecutors {
static final Logger logger = LoggerFactory.getLogger(DailyRoutineWorkflowUsingExecutors.class);

static void log(String message) {
logger.info(STR."{} | \{message}", Thread.currentThread());
}


private static void sleep(Long duration) throws InterruptedException {
TimeUnit.MILLISECONDS.sleep(duration);
}

public static void executeJobRoute() throws ExecutionException, InterruptedException {
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
var morningMeeting = executor.submit(() -> {
log("I'm going to attend morning status meeting");
try {
sleep(1000L);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
log("I'm done with morning status meeting");
});

var actualWork = executor.submit(() -> {
log("I'm starting my actual work on tasks");
try {
sleep(1000L);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
log("I'm done with actual work on tasks");
});

var eveningMeeting = executor.submit(() -> {
log("I'm going to attend evening status meeting");
try {
sleep(1000L);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
log("I'm done with evening status meeting");
});

morningMeeting.get();
actualWork.get();
eveningMeeting.get();
}
}
}

The Deep Dive on the output

If you run above code either using factory methods or ExecutorServices you the something similar as below.

Observe carefully Info log, you will see two sections either side of “|” (pipe symbol) the first section explains you about the virtual thread like VirtualThread[#26]/runnable@ForkJoinPool-1-worker-3 This tells VirtualThread[#26] maps to platform thread runnable@ForkJoinPool-1-worker-3and the other section is info part of the log.

ThreadLocals and Virtual Threads:

ThreadLocal in Java is a mechanism that allows variables to be stored on a per-thread basis. Each thread accessing a ThreadLocal variable gets its own, independently initialized copy of the variable, which can be accessed and modified without affecting the same variable in other threads. This is particularly useful in scenarios where you want to maintain thread-specific state, such as user sessions or database connections.

However, the behavior of ThreadLocal changes significantly when used with virtual threads, introduced as part of Project Loom. Virtual threads are lightweight threads managed by the Java Virtual Machine (JVM) and are designed to be scheduled in large numbers, unlike traditional platform threads which are tied to the operating system's thread management.

Since VirtualThreads can be created as millions creating and using of ThreadLocal can create memory leaks.

package org.vaslabs;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.concurrent.TimeUnit;

public class ThreadLocalDemo {
private static ThreadLocal<String> stringThreadLocal = new ThreadLocal<>();

static final Logger logger = LoggerFactory.getLogger(DailyRoutineWorkflow.class);

static void log(String message) {
logger.info(STR."{} | \{message}", Thread.currentThread());
}

private static void sleep(Long duration) throws InterruptedException {
TimeUnit.MILLISECONDS.sleep(duration);
}

public static void virtualThreadContext() throws InterruptedException {
var virtualThread1 = Thread.ofVirtual().name("thread-1").start(() -> {
stringThreadLocal.set("thread-1");
try {
sleep(1000L);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
log(STR."thread name is \{stringThreadLocal.get()}");
});
var virtualThread2 = Thread.ofVirtual().name("thread-2").start(() -> {
stringThreadLocal.set("thread-2");
try {
sleep(1000L);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
log(STR."thread name is \{stringThreadLocal.get()}");
});
virtualThread1.join();
virtualThread2.join();
}
}

Conclusion:

In conclusion, this article on Java’s threading models — Virtual Threads, Platform Threads, and the nuances of ThreadLocal — sheds light on the evolving landscape of concurrent programming in Java. Virtual Threads emerge as a game-changer, offering lightweight, efficient concurrency that starkly contrasts with the resource-intensive nature of Platform Threads. They revolutionize Java’s approach to handling multithreading by enabling a massive number of concurrent tasks with minimal resource overhead, thereby simplifying the programming model and enhancing application scalability.

However, the intricacies of ThreadLocal usage in this new context highlight the need for careful consideration. While ThreadLocal remains a powerful tool for maintaining thread-specific data in traditional threading, its application becomes more complex with virtual threads, necessitating alternative strategies for state and context management. Together, these concepts mark a significant shift in Java’s concurrency paradigm, opening new doors for developers to build more responsive, scalable, and efficient applications.

Stackademic

Thank you for reading until the end. Before you go:

  • Please consider clapping and following the writer! 👏
  • Follow us on Twitter(X), LinkedIn, and YouTube.
  • Visit Stackademic.com to find out more about how we are democratizing free programming education around the world.

--

--