Virtual Thread Support in Jakarta EE 11¶
Java 21 introduces Virtual Threads, a lightweight threading solution that allows applications to create a large number of concurrent threads with minimal memory consumption. Unlike Platform Threads, which are resource-intensive and heavily dependent on system processor cores, Virtual Threads are designed to be efficient and scalable. This new feature significantly enhances the concurrency capabilities of Java applications, making it easier to handle high-throughput workloads with improved performance and reduced overhead.
Jakarta EE 11 sets Java 17 as the baseline but it also requires all implementations to support Java 21. To align with Java 21, Jakarta Concurrency 3.1 introduces support for virtual threads, enhancing the concurrency capabilities of Jakarta EE applications.
In Jakarta Concurrency 3.1, a new attribute virtual
is added to existing annotations such as @ManagedExecutorDefinition
, @ManagedScheduledExecutorDefinition
, and @ManagedThreadFactoryDefinition
. This attribute specifies whether to create the managed resources using virtual threads. By default, the virtual thread support is not enabled, the virtual
attribute is false
implicitly. To enable it, explicitly set the value of the virtual
attribute to true
.
[!NOTE] To leverage Virtual Threads in Jakarta EE, ensure your applications are running on a Jakarta EE 11 implementation with a Java 21+ runtime.
For example, declare virtual thread-based resources:
@ManagedExecutorDefinition(
name = "java:comp/vtExecutor",
maxAsync = 10,
context = "java:comp/vtContextService",
virtual = true
)
@ContextServiceDefinition(
name = "java:comp/vtContextService",
propagated = {SECURITY, APPLICATION}
)
@ManagedThreadFactoryDefinition(
name = "java:comp/vtThreadFactory",
context = "java:comp/vtContextService",
virtual = true
)
@ManagedScheduledExecutorDefinition(
name = "java:comp/vtScheduleExecutor",
context = "java:comp/vtContextService",
maxAsync = 10,
virtual = true
)
@ApplicationScoped
public class VirtualThreadAsyncConfig {
}
In this example, a dedicated ContextService
is defined specifically for the virtual thread-based resources. This ensures that all related executors and thread factories share a consistent context configuration optimized for virtual thread usage.
To utilize these resources within your CDI beans, simply inject them using the @Resource
annotation with their respective JNDI names:
@Resource(lookup = "java:comp/vtExecutor")
ManagedExecutorService executor;
@Resource(lookup = "java:comp/vtContextService")
ContextService contextService;
@Resource(lookup = "java:comp/vtThreadFactory")
ManagedThreadFactory threadFactory;
@Resource(lookup = "java:comp/vtScheduleExecutor")
ManagedScheduledExecutorService scheduledExecutor;
Alternatively, Jakarta Concurrency 3.1 allows injecting managed resources using CDI @Inject
. To distinguish virtual thread-aware resources, use custom CDI @Qualifier
annotations. By setting the qualifiers
attribute on resource annotations, these resources can be injected type-safely in CDI beans, similar to other CDI beans.
For example, create a custom @Qualifier
annotation:
@Qualifier
@Retention(RUNTIME)
@Target({METHOD, FIELD, PARAMETER, TYPE})
public @interface WithVirtualThread {
}
Next, add the qualifiers
attribute to the resource annotations and set its value to the created @Qualifier
class.
@ManagedExecutorDefinition(
//...
qualifiers = {WithVirtualThread.class}
)
@ContextServiceDefinition(
//...
qualifiers = {WithVirtualThread.class}
)
@ManagedThreadFactoryDefinition(
//...
qualifiers = {WithVirtualThread.class}
)
@ManagedScheduledExecutorDefinition(
//...
qualifiers = {WithVirtualThread.class}
)
@ApplicationScoped
public class VirtualThreadAsyncConfig {
}
Finally, inject these resources using the custom @Qualifier
annotation:
@Inject
@WithVirtualThread
ManagedExecutorService executor;
@Inject
@WithVirtualThread
ContextService contextService;
@Inject
@WithVirtualThread
ManagedThreadFactory threadFactory;
@Inject
@WithVirtualThread
ManagedScheduledExecutorService scheduledExecutor;
Here are some examples of using these resources to run asynchronous tasks in a Jakarta EE environment:
executor.submit(() -> {
// Task logic here
System.out.println("Task executed using ManagedExecutorService");
});
scheduledExecutor.schedule(() -> {
// Task logic here
System.out.println("Task scheduled using ManagedScheduledExecutorService");
}, 10, TimeUnit.SECONDS);
try (ForkJoinPool pool = new ForkJoinPool(
Runtime.getRuntime().availableProcessors(),
threadFactory,
(t, e) -> LOGGER.log(Level.INFO, "Thread: {0}, error: {1}", new Object[]{t.getName(), e.getMessage()}),
true
)) {
pool.submit/invokeAll...
}
try (var scope = new StructuredTaskScope("vt", threadFactory)) {
Future<String> task1 = scope.fork(() -> {... });
Future<Integer> task2 = scope.fork(() -> {... });
scope.join(); // Wait for all tasks to complete
scope.throwIfFailed(); // Propagate any task failure
// handle the results
}
While virtual threads are not a silver bullet for all performance issues, they can significantly enhance overall application performance by increasing throughput in some scenarios, such as database calls, handling HTTP interactions, etc.