Locks via Spring AOP

Introduction

Software development is getting harder and harder, especially with higher emphasis on writing multithreading applications nowadays.

Writing multithreaded applications are easy. Writing good multithreaded applications are not.

This article, however, does not teach you how to write good multithreaded applications. This article will teach you how to use Spring's AOP feature to inject locks into your single threaded application.

Locking, What is it?

Many software modules are still written with the assumption that they are single threaded. Thus when the need to scale (via multithreading) comes along, they run into a lot of concurrency issues, with many read/write problems and data consistency.

Locking is an easy and quick way to allow your application to take the first step in playing nice in a multithreading environment. I must stress, however, that it is not the final step. But it is a good step.

One quick way would be to simply sprinkle your code with synchronized keyword on methods. This, however, restricts access to an object to only one user at a time. Even when two users are both invoking on immutable methods of an object (methods that do not modify object states), only one of them can access it at one time! This does not make sense!

What we need are actually read/write locks!

A Better Lock: Read/Write Lock

A read/write lock essentially gives you the power to perform multiple reads on a single object at one time. However, when you need to perform a write, it waits till all read locks are released, and then obtain a write lock. When a write lock is obtained, no read lock can happen, until the write lock is released. Locks can be nested, but bear in mind that at one time, only one lock is allowed a write lock! A thread can, however, hold multiple read locks, even when it is itself holding a write lock.

Below is a sample of a read/write lock in Java.

import java.util.concurrent.locks.ReentrantReadWriteLock;
...
final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
lock.readLock().lock();
try {
  ...  // do stuff
  lock.writeLock().lock();
  try {
    ...  // do stuff
  } finally 
    
  
  ...  // do stuff
} finally 
  

As you can see, it is really a good idea to wrap the code after acquiring a lock in a try/finally block, to ensure that you release the lock, no matter what happens. Each lock acquisition must be matched by a lock release.

Manual Locking, A Tedious Approach

So now, to play nice in a multithreading environment, your library methods must actually have tons of such wrappers. In fact, minimally, one for each method, be it for reading or writing!

That is a very tedious thing to do.

In fact, is it really necessary? Sure it is. But it sure does not help in readablity!

Aspect Oriented Programming: Shifting The Responsibility Out

We can actually take advantage of AOP(Aspect Oriented Programming), to shift the responsibility of locking out of the methods themselves. For one, this result in clearer code, which helps in readablity and maintainablity of code. Second, there is no need to pay the penalty of locking, if the library is used in a single-threaded environment. Only in a multithreaded environment, would such penalty be required to be paid.

To begin, let's define some annotations.

Marking Methods With Annotations

Annotations are introduced in Java 5, which allows us to mark methods, variables, classes, etc with metadata. The metadata can be embedded in the generated class files, and be available to the JVM when the application executes. At runtime, we can query the methods for the annotations, and thus provide a very easy way to know which methods are read methods (requiring a read lock), and which are write methods (requiring a write lock).

So, we need two annotations, one for reading and one for writing.

package com.technoriment.data;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.annotation.ElementType;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ReadOperation {
  // empty
}
package com.technoriment.data;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.annotation.ElementType;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface WriteOperation {
  // empty
}

Now that we have two such annotations, we can mark our methods with them.

@ReadOperation
public final void read() {
  // empty
}
@WriteOperation
public final void write() {
  // empty
}

Defining the Aspect

Our first task on hand now would be to define (or rather code) the aspect. As mentioned before, this article will be using Spring's AOP functionality.

public class ReadWriteSynchronizationAspect {
  public final void beginRead() { // lock for reading
  }
  public final void endRead() { // unlock for reading
  }
  public final void beginWrite() { // lock for writing
  }
  public final void endWrite() { // unlock for writing
  }
}

Who Owns The Lock?

A relatively simple and clear aspect. We did, however, hit an interesting point. Should the aspect provide the locking object? If it does, then an aspect instance would have to be created for each advised object (an advised object is the object where the aspect is applied on). Ideally, the advised object should provide the locks themselves. So we end up with an interface that supplies the lock/unlock methods.

public interface ReadWriteLockProvider {
  void acquireReadLock();
  void releaseReadLock() throws IllegalMonitorStateException;
  void acquireWriteLock();
  void releaseWriteLock() throws IllegalMonitorStateException;
}

And we update the aspect to take advantage of the provider.

public class ReadWriteSynchronizationAspect {
  public final void beginRead(final ReadWriteLockProvider provider) 
    
  
  public final void endRead(final ReadWriteLockProvider provider) 
    
  
  public final void beginWrite(final ReadWriteLockProvider provider) 
    
  
  public final void endWrite(final ReadWriteLockProvider provider) 
    
  
}

Implementing the provider

So now we have a lock provider interface. Does that mean that we have to have all our classes implement that? That is hardly non-intrusive and defeats the purpose!

Luckily, Spring AOP (and most other Aspect Oriented Implementations) support a feature known as introduction, which allows you to declare that an advised object implements a particular interface.

So what we can do now instead is to create an implementation of a standalone lock provider! We will be using ReentrantReadWriteLock again.

import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock.ReadLock;
import java.util.concurrent.locks.ReentrantReadWriteLock.WriteLock;

public class ReadWriteLockProviderImpl implements ReadWriteLockProvider {
  private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
  private final ReentrantReadWriteLock getLock() { return lock; }
  private final ReadLock getReadLock() { return getLock().readLock(); }
  private final WriteLock getWriteLock() { return getLock().writeLock(); }
  
  public final void acquireReadLock() 
    
  
  public final void releaseReadLock() 
    
  
  public final void acquireWriteLock() 
    
  
  public final void releaseWriteLock() 
    
  
}

Weaving in the advice

We now have all the foundation work done! Time to weave the aspect in using Spring! Below is the configuration file used.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
           http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
           http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.5.xsd">

  <aop:config>
    <aop:aspect id="readWriteSynchronizationAspect" ref="readWriteSynchronization">

      <aop:declare-parents
        types-matching="com.technoriment.aspects.locks.tests.support.IReadWriteSynchronizationTarget"
        implement-interface="com.technoriment.aspects.locks.ReadWriteLockProvider"
        delegate-ref="readWriteProvider" />

      <aop:before pointcut="@annotation(com.technoriment.data.ReadOperation) and this(provider)" method="beginRead" />
      <aop:after pointcut="@annotation(com.technoriment.data.ReadOperation) and this(provider)" method="endRead" />
      
      <aop:before pointcut="@annotation(com.technoriment.data.WriteOperation) and this(provider)" method="beginWrite" />
      <aop:after pointcut="@annotation(com.technoriment.data.WriteOperation) and this(provider)" method="endWrite" />

    </aop:aspect>
  </aop:config>

  <bean id="readWriteSynchronization" class="com.technoriment.aspects.locks.ReadWriteSynchronizationAspect" scope="prototype">
  </bean>

  <bean id="readWriteProvider" class="com.technoriment.aspects.locks.ReadWriteLockProviderImpl" scope="prototype">
  </bean>

  <bean id="target"
    class="com.technoriment.aspects.locks.tests.support.ReadWriteSynchronizationTarget"
    autowire="byType"
    scope="singleton">

  </bean>

</beans>

Dissecting the Configuration File

I defined a total of three Spring objects in the example configuration. First is the aspect(readWriteSynchronization of type ReadWriteSynchronizationAspect, which could be a singleton actually, since they have no shared data), a lock provider(readWriteProvider of type ReadWriteLockProviderImpl, which we really want it to be a prototype, since we don't want different object instances to share the same lock), and finally, the advised object itself (target of type ReadWriteSynchronizationTarget).

We next define an aspect in the application.

<aop:config>
  <aop:aspect id="readWriteSynchronizationAspect" ref="readWriteSynchronization">
    ....
  </aop:aspect>
</aop:config>

Aspects are to be defined within a config section. Each aspect requires a unique id to be specified, and the ref fields indicate the Spring bean object to be used as the aspect object.

I then had the following section

<aop:declare-parents
  types-matching="com.technoriment.aspects.locks.tests.support.IReadWriteSynchronizationTarget"
  implement-interface="com.technoriment.aspects.locks.ReadWriteLockProvider"
  delegate-ref="readWriteProvider" />

What this does is to tell Spring that we would really like objects that implement the interface IReadWriteSynchronizationTarget (which incidentally, is what ReadWriteSynchronizationTarget implements) to be introduced to the interface ReadWriteLockProvider (that is, to implement that same interface as well). We also tell Spring to use the implementation of the Spring bean identified by readWriteProvider as the implementation to be introduced.

So now, within a Spring application, all instances of type IReadWriteSynchronizationTarget are seen to implement the ReadWriteLockProvider as well, based on the implementation in ReadWriteLockProviderImpl!

Now to actually advise the advised object!

<aop:before pointcut="@annotation(com.technoriment.data.ReadOperation) and this(provider)" method="beginRead" />
<aop:after pointcut="@annotation(com.technoriment.data.ReadOperation) and this(provider)" method="endRead" />
      
<aop:before pointcut="@annotation(com.technoriment.data.WriteOperation) and this(provider)" method="beginWrite" />
<aop:after pointcut="@annotation(com.technoriment.data.WriteOperation) and this(provider)" method="endWrite" />

These are the four advices we are applying on the advised object. A pointcut is an expression to be matched by Spring, that the advice will be applied on. We see two forms of advice types here: before and after. They are quite clear, in that a before advice would be applied before a method call, and an after advice would be applied after a method call (regardless of any exceptions). There are more advice types, but for the purpose of this article, these two will be sufficient. Once the advised method is triggered (the stage of trigger depends on the type, before or after), the specified method in the field "method" will be invoked.

The pointcut expression we used here are fairly simple. For example, the following

@annotation(com.technoriment.data.ReadOperation) and this(provider)

The first part indicates that all methods marked with an annotation of ReadOperation will be matched. The second part has two meanings. First it adds an additional matching expression that the object that will be advised must also implements an additional type. Notice the use of the word provider, which is actually the parameter name of the method to be invoked in the aspect. Spring will lookup the method for the type the advised object must implement. Next, it tells Spring to bind the advised object as a parameter to the aspect method.

And this is, in a nutshell, how to weave the advice in!

Conclusion

As we can see, using an AOP approach actually results in more 'dynamic configurations', reduce 'unnecessary' logic within the application itself, resulting in more easily maintainable code.

We could swap in different lock implementations easier and propagate the change easily to all classes, or we could stop using locking all together on applications we know for sure that is single threaded! Code change impact is kept to a bare miminal!