Concurrency Control for CDI

At the moment there is no standard concurrency control mechanism for CDI managed beans, and at the time of writing it has not been taken into consideration for CDI 2.0 specification either.

The lack of any container based concurrency control, can lead to issues when working with unprotected contexts such as Application- or Session-Scoped beans.

Unlike EJB’s, where the container handles concurrency control, CDI only defines concurrency control for the conversation context.

The container ensures that a long-running conversation may be associated with at most one request at a time, by blocking or rejecting concurrent requests. If the container rejects a request, it must associate the request with a new transient conversation and throw an exception of type javax.enterprise.context.BusyConversationException from the restore view phase of the JSF lifecycle. The application may handle this exception using the JSF ExceptionHandler.
Conversation context lifecycle in Java EE – CDI Spec

Aside from this concurrency is only mentioned in conjunction with CDI EJB interaction.

Whilst all other relevant normal scoped contexts (Application- and Session- Scoped) are completely unguarded, the specification only defines a non adaptable concurrency control for conversation scoped beans. In my previous post on sychronizing access to CDI Conversations, I described how some of these limitation can be overcome.

As mentioned, the CDI specification (see CDI Spec. 1.2 – 1.2.2 Relationship to EJB) states the possibility of employing EJB session beans for concurrency control.

@SessionScoped
@Stateful
public class MySingleton {   
   public void threadUnsafeOperation() {
      ...
   }

   public void threadSafeOperation {
      ...
   }
} 

CDI manages the context and lifecycle of the stateful session bean, utilizing the EJB containers standard concurrency mechanics. The main limitation of this approach is that, apart from EJB Singleton (see EJB 3.1-Spec, Chap. 4.8.5 Singleton Concurrency), EJB concurrency control defaults to serializing all incoming requests.

By default, clients are allowed to make concurrent calls to a stateful session object and the container is required to serialize such concurrent requests. Note that the container never permits multi-threaded
access to the actual stateful session bean instance. For this reason, Read/Write method locking metadata, as well as the bean-managed concurrency mode, are not applicable to stateful session beans and must not be used. See Section 4.8.5 for a description of how these mode/locking types apply to Singleton session beans.
EJB 3.1 Spec – 4.3.14 Serializing Session Bean Methods

In this post we will explore how CDI interceptors can be used to implement EJB @Singleton style locking for CDI beans.

CDI interceptors are based upon the Interceptors Specification which was part of JSR-318 Enterprise JavaBeans.

The lifecycle of an interceptor instance is the same as that of the target class instance with which it is associated.
2.2 Interceptor Life Cycle – JSR 318 Interceptors 1.2

A locking interceptor binding has to be defined, which will be applicable to both type and method.

@Inherited
@InterceptorBinding
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD,ElementType.TYPE})
public @interface Lock {
  LockType value() default LockType.WRITE;
}

Since the interceptors are directly bound to the bean and its lifecycle, a lock can be defined in the interceptor for the bean.

@Lock
@Interceptor
public class LockInterceptor {

  private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(true);

  @AroundInvoke
  public Object concurrencyControl(InvocationContext ctx) throws Exception {
    Lock lockAnnotation = ctx.getMethod().getAnnotation(Lock.class);
    
    if (lockAnnotation == null) {
      lockAnnotation = ctx.getTarget().getClass().getAnnotation(Lock.class);
    }

    Object returnValue = null;
    switch (lockAnnotation.value()) {
      case WRITE:
        ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();
        try {
          writeLock.lock();
          returnValue = ctx.proceed();
        } finally {
          writeLock.unlock();
        }
        break;
      case READ:
        ReentrantReadWriteLock.ReadLock readLock = lock.readLock();
        try {
          readLock.lock();
          returnValue = ctx.proceed();
        } finally {
          readLock.unlock();
        }
        break;
    }
    return returnValue;
  }
}

It is important to note that by default ReentrantReadWriteLock does not guarantee the order in which waiting threads are granted access and is prone to thread starvation. This can be overriden, at the cost of throughput, by providing the constructor with a “fairness” parameter.

Non-fair mode (default)
When constructed as non-fair (the default), the order of entry to the read and write lock is unspecified, subject to reentrancy constraints. A nonfair lock that is continuously contended may indefinitely postpone one or more reader or writer threads, but will normally have higher throughput than a fair lock.
Fair mode
When constructed as fair, threads contend for entry using an approximately arrival-order policy. When the currently held lock is released, either the longest-waiting single writer thread will be assigned the write lock, or if there is a group of reader threads waiting longer than all waiting writer threads, that group will be assigned the read lock.
ReentrantReadWriteLock – Java 8

The interceptor can be applied globally by declaring it on the bean:

@Lock
@ApplicationScoped
public class MyAppScopedBean {
    
  @Lock(LockType.READ)
  public int threadSafeOperation() {
    ...
  }

  public void threadUnsafeOperation() {
    ...
  }
}

In which case the lock type can be overridden by defining it on the method.

Or placing the interceptor only on methods that need concurrency control:

@ApplicationScoped
public class MyAppScopedBean {
    
  public int threadSafeOperation() {
    ...
  }
  
  @Lock
  public void threadUnsafeOperation() {
    ...
  }
}

In which case the interceptor does not have to be defined on thread-safe methods, since it will be invoked as per normal without any concurrency control from the container.

It is important to note that this will not work for conversation context. As described above, the specification mandates that the container handles concurrency control for long-running conversations. This therefore renders any attempt to control concurrent access via interceptors futile, since it is preempted by the containers.

Acknowledgements

I would like to thank Björn Sonntag and Robert Meyer for reviewing my posts.

7 thoughts on “Concurrency Control for CDI”

  1. Great post!

    If I understood correctly your example, 2 threads may invoke (concurrently) different methods of the same instance if those methods have different lock types, right?

    1. Thank you.

      Methods with LockType.READ maybe accessed concurrently by any number of threads, as long as no write lock is held.

      A method with LockType.WRITE will request a write lock, granting it exclusive access to the instance.

  2. 1. “The interceptors that you specify in the beans.xml file apply only to classes in the same archive. Use the @Priority annotation to specify interceptors globally for an application that consists of multiple modules e.g. @Lock @Interceptor @Priority(Interceptor.Priority.APPLICATION) public class LockInterceptor…”
    2. The LockType parameter inside @Lock definition should imo be annotated with @Nonbinding. Otherwise when parameter is used with annotation on the bean (e.g. @Lock(LockType.READ) ) the interceptor won’t be bound to methods annotated with @Lock with some other param e.g. @Lock(LockType.WRITE)

Leave a Reply

Your email address will not be published. Required fields are marked *