Concurrency in microservices

Concurrency refers to the ability to run multiple tasks at the same time, which can increase the efficiency of an application. Tasks can be submitted to run immediately, at a specified time, or in response to the completion of one or more actions upon which they depend.

For example, an application that computes the total cost of a merchandise order must run a series of tasks. First, it looks up the state and local sales tax rates in one task while concurrently calculating the shipping costs in another. Then, in a third task that runs upon completion of the first two, it totals the results.

Concurrency in Java SE and Jakarta EE

Java SE and Jakarta EE both support concurrency with a standardized API that enables the creation and scheduling of concurrent tasks. Jakarta Concurrency 3.0 enhances these capabilities by providing context awareness between concurrent tasks, which improves consistency and visibility across an application.

Java SE includes a Concurrency Utilities specification. This specification standardizes an API that supports the fundamental operations of concurrent programming, such as:

  • Creating threads

  • Submitting tasks to run in parallel

  • Scheduling tasks for execution at a future time

  • Awaiting completion of tasks and obtaining the result of their execution

In the Java SE environment, a single instance of an application services a single request and submits all of the associated tasks. However, in a Jakarta EE environment, multiple applications service multiple requests simultaneously. These applications perform tasks concurrently to improve throughput and decrease response time. They also share thread pools and task scheduling to maximize efficiency. Jakarta Concurrency extends the Java SE ExecutorService, ScheduledExecutorService, and ThreadFactory classes with the following subclasses that are implemented by the application server: ManagedExecutorService, ManagedScheduledExecutorService, and ManagedThreadFactory.

Concurrency in MicroProfile and Jakarta EE 10+

Microservices and other modern applications have many of the same needs as those in Java EE, but with a greater focus on reactive patterns. One form of reactive programming is the Java SE CompletionStage model, which creates pipelines of dependent actions that run upon completion of the states on which they depend. However, in Java SE, these pipelines run with inconsistent thread contexts. The Jakarta Concurrency 3.0 specification solves this problem by introducing a variety of methods to the ManagedExecutorService interface that create context-aware implementations of the CompletionStage object. The MicroProfile Context Propagation specification similarly adds these same methods to the ManagedExecutor interface, which is nearly identical to and fully compatible with ManagedExecutorService.

Sharing thread pools

By making executors and thread factories into managed resources, the Concurrency Utilities for Java EE specification enables the sharing of these resources across applications and application components. Furthermore, this specification introduces a standard set of default resources (for example, java:comp/DefaultManagedScheduledExecutorService) that are available to every application. Applications can then share a single threading and scheduling implementation that requires no configuration because it is managed by the application server.

With MicroProfile Context Propagation, all managed executors share a common thread pool with the core server implementation. This thread pool is automatically tuned throughout the lifetime of the server by measuring the impact of incremental changes to the number of threads in the pool.

Commonalities between Java SE, Java EE Concurrency Utilities, Jakarta Concurrency and MicroProfile

The MicroProfile Context Propagation specification closely matches the Concurrency specifications for Java SE, Java EE, and Jakarta EE. This commonality means you can write applications by using Java SE, Java EE, or Jakarta EE APIs and still take advantage of MicroProfile Context Propagation. You can even inject executor services and thread factories under the Java SE interface names (for example, java.util.concurrent.ExecutorService). With the MicroProfile Context Propagation feature enabled, you can also cast these executors to the org.eclipse.microprofile.context.ManagedExecutor interface, which enables context-aware completion stages. With the Jakarta Concurrency 3.0 feature enabled, these executors can already create context-aware completion stages. After you obtain the completion stage from the managed executor, you interact with it exactly as you would the Java SE API. The MicroProfile Context Propagation specification has the added benefit of having a consistent thread context in your completion stage actions.

Thread context awareness

In Jakarta Concurrency, Concurrency Utilities for Java EE, and MicroProfile Context Propagation, the executors are aware of thread context. The executors can propagate the context of the task submitter to the thread where the task or action runs. Tasks can perform lookups in the java:comp, java:module, or java:app namespace of the submitting thread. They can also run with the security context of the submitting thread, load classes from its thread context classloader, and so on. With Liberty, you can configure which types of thread context are propagated. The following example shows a managed executor that is configured to propagate the thread context type ContextServiceDefinition.APPLICATION (or ThreadContext.APPLICATION in MicroProfile). This configuration enables the second stage to perform the java:comp lookup against the namespace of the application that invoked thenApply.

CompletableFuture<Long> stage = managedExecutor
    .supplyAsync(supplier)
    .thenApply(value -> {
        // only possible with the application component's context available:
        DataSource ds = InitialContext.doLookup("java:module/env/jdbc/ds1");
        ...
    });

Thread factories are also context aware and can propagate the context of the requesting or injecting thread to the threads that they create. Therefore, thread pools that are created by these thread factories all run under the same thread context, which minimizes the costs of thread context switching.

For thread context propagation, managed executors and thread factories rely on the Context Service (javax.enterprise.concurrent.ContextService), which is provided by Jakarta Concurrency or Concurrency Utilities for Java EE. Context services are also available for direct usage, giving you more flexible and fine grained control of thread context propagation. With context services, you can construct proxies that save the thread context at creation time and apply it upon the invocation of interface methods. In MicroProfile Context Propagation, the ThreadContext API provides a similar function.

Triggers and notifications

Jakarta Concurrency and Concurrency Utilities for Java EE both provide a trigger API that you can use to customize scheduling for complex business logic. You can use a trigger to recompute the next execution time each time a task runs, so you are not limited to scheduling repeating tasks at fixed intervals. For example, you can schedule a task that runs only Monday through Friday, skipping weekend days. Or, you can use a trigger to make the frequency of your task depend on other external stimuli, such as the weather or the rate at which sales are being made. The specification also gives you the ability to register for notifications of task lifecycle, which provide visibility across concurrent tasks.

Try out the Liberty Concurrency and MicroProfile Context Propagation features

With the Concurrency feature, Liberty provides a robust, fully spec-compliant implementation of Jakarta Concurrency. MicroProfile Context Propagation is included with the MicroProfile Context Propagation feature. Both features can be enabled and used in tandem. Try it out today, and start enjoying the advantages of concurrency and task scheduling in a Java EE or Jakarta EE environment.