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. Java SE and EE both support concurrency with a standardized API that enables the creation and scheduling of concurrent tasks. MicroProfile Context Propagation enhances these capabilities by providing context awareness between concurrent tasks, which improves consistency and visibility across an application.

Concurrency in Java SE and Java EE

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 Java 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. Java EE 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

MicroProfile 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 MicroProfile Context Propagation specification solves this problem by introducing the ManagedExecutor subclass, which is nearly identical to and fully compatible with the ManagedExecutorService subclass in Java EE, except that it is enhanced with the ability to create consistent, context-aware implementations of the CompletionStage object.

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 (e.g. 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 and EE concurrency and MicroProfile

The MicroProfile Context Propagation specification closely matches the Concurrency Utilities specifications for Java SE and Java EE. This commonality means you can write applications by using Java SE or Java 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 (e.g. java.util.concurrent.ExecutorService) as well as the Java EE interface names (e.g. javax.enterprise.concurrent.ManagedExecutorService). With the mpContextPropagation feature enabled, you can also cast these executors to the org.eclipse.microprofile.context.ManagedExecutor interface, which enables context-aware completion stages. Once you obtain the completion stage from the managed executor, you interact with it exactly as you would the Java SE API, with the added benefit of a consistent thread context in your completion stage actions.

Thread context awareness

In both Concurrency Utilities for Java EE and MicroProfile Context Propagation, executors are aware of thread context and 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 ThreadContext.APPLICATION. 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. This enables thread pools that are created by these thread factories to all run under the same thread context, minimizing 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 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

Concurrency Utilities for Java EE provides a trigger API that allows you to customize scheduling for complex business logic. A trigger allows you to recompute the next execution time each time a task runs. This means that you are no longer limited to scheduling repeating tasks at fixed intervals. For example, you can schedule a task that only runs 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 concurrent feature, Liberty provides a robust, fully spec-compliant implementation of Concurrency Utilities for Java EE. MicroProfile Context Propagation is included with the mpContextPropagation 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 environment.