Using ehcache locking for highly asynchronous multi threaded programming

Use case overview using Ehcache

I have found myself using ehcache a lot lately at work for an in memory Java cache that can be backed to disk if desired. One interesting use case has been using it to cache intermediary updates that build up a complete and final object. These intermediary updates are delivered via messaging (JMS) and each message contains an update for a piece of a single specific object. These updates are processed in a highly multi-threaded asynchronous fashion and must be applied in the order they are received. If two updates for the same object are received at approximately the same time I have to make sure that the first update does not overwrite what is done in the later update (this is possible if the later update is quicker to apply than the first). Using ehcache’s write lock mechanism is perfect for this situation… it give’s granularity at the key/object/row level which allows all other updates that are received for other keys/objects/rows to be applied asynchronously at the same time without blocking. If two threads ask for write locks on the same key then the first thread will get the lock and will process while the second thread has to wait for the write lock to be released by the first thread.

Ehcache javadocs:
http://ehcache.org/apidocs/
The method providing write locks that we will be using:
http://ehcache.org/apidocs/net/sf/ehcache/Cache.html#acquireWriteLockOnKey(java.lang.Object)

Below is a very basic Java class with a main method that demonstrates the above use case.

package com.knowcean.ehcache;

import net.sf.ehcache.Cache;
import net.sf.ehcache.CacheManager;
import net.sf.ehcache.Element;

public class DemoEhcacheLocks {
    private Cache cache;

    public DemoEhcacheLocks() {
        CacheManager cacheManager = CacheManager.create();
        cache = cacheManager.getCache("demoCache");  // get the cache defined in ehcache.xml
        System.out.println("cache names length: " + cacheManager.getCacheNames().length);
        if(cache == null) System.out.println("cache is NULL");
        
        // prepopulate the cache with some test data
        cache.put(new Element("key1", "value1"));
        cache.put(new Element("key2", "value2"));
        cache.put(new Element("key3", "value3"));
        
        // create 2 threads that will work on the same cache key and start at approximately
        // the same time but take different amounts of time to complete
        Thread t1 = new Thread(new WriteLockRunnable(cache, "key1", "newval1", 5000), "t1");
        Thread t2 = new Thread(new WriteLockRunnable(cache, "key1", "newerval1", 0), "t2");
        
        t1.start();
        t2.start();

        try {
            // wait for the threads to finish
            t1.join();
            t2.join();

            // print out the contents of the cache
            System.out.println("key1 value = " + cache.get("key1").getValue());
            System.out.println("key2 value = " + cache.get("key2").getValue());
            System.out.println("key3 value = " + cache.get("key3").getValue());
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        
        // now run 2 threads that will work on different cache keys and start at approximately
        // the same time but take different amounts of time to complete
        t1 = new Thread(new WriteLockRunnable(cache, "key1", "finalval1", 7000), "t1");
        t2 = new Thread(new WriteLockRunnable(cache, "key3", "newval3", 0), "t2");
        
        t1.start();
        t2.start();

        try {
            // wait for the threads to finish
            t1.join();
            t2.join();

            // print out the contents of the cache
            System.out.println("key1 value = " + cache.get("key1").getValue());
            System.out.println("key2 value = " + cache.get("key2").getValue());
            System.out.println("key3 value = " + cache.get("key3").getValue());
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
    
    public class WriteLockRunnable implements Runnable {
        private Cache cache;
        private String key;
        private String value;
        private long sleep = 0;
        
        public WriteLockRunnable(Cache cache, String key, String value, long sleep) {
            this.cache = cache;  // cache to use
            this.key = key;      // key this thread will operate on
            this.value = value;  // new value to set on supplied key
            this.sleep = sleep;  // time this thread will sleep in milliseconds before updating the key's value
        }
        
        public void run() {
            System.out.println(Thread.currentThread().getName() + " running");
            cache.acquireWriteLockOnKey(key);
            try {
                System.out.println(Thread.currentThread().getName() + " updating");
                Thread.sleep(sleep);
                cache.put(new Element(key, value));
                System.out.println(Thread.currentThread().getName() + " updated");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                cache.releaseWriteLockOnKey(key);
            }
        }
    }

    public static void main(String[] args) {
        new DemoEhcacheLocks();
    }
}

The output of the above program:

cache names length: 1
t1 running
t2 running
t1 updating
t1 updated
t2 updating
t2 updated
key1 value = newerval1
key2 value = value2
key3 value = value3
t1 running
t2 running
t1 updating
t2 updating
t2 updated
t1 updated
key1 value = finalval1
key2 value = value2
key3 value = newval3

The above output is more interesting to see as each line gets printed out as you will notice where certain threads block because they are waiting for a write lock to be released.

If you are interested in seeing what else ehcache can do or how it will react and handle your use cases maybe start by modifying the above basic example as a first step.

Here is a very basic configuration file for Ehcache. Put this on your classpath and Ehcache will find it. If you are using Maven, then put it in src/main/resources.

<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
         xsi:noNamespaceSchemaLocation="ehcache.xsd"
         updateCheck="true"
         monitoring="autodetect">

   <defaultCache maxElementsInMemory="10000"
                 eternal="false"
                 timeToIdleSeconds="86400"
                 timeToLiveSeconds="86400"
                 overflowToDisk="true"
                 diskSpoolBufferSizeMB="10"
                 maxElementsOnDisk="10000000"
                 diskPersistent="false"
                 diskExpiryThreadIntervalSeconds="120"
                 memoryStoreEvictionPolicy="LRU"/>

   <!--
   This cache name should match the name of the cache in your
   Java program where you do CacheManager.getCache(<cache_name>)
   -->
   <cache name="demoCache" maxBytesLocalHeap="10485760">
   </cache>
   
</ehcache>

If you are using Maven for your build and dependency management, then you can just place this Maven pom.xml file in your project or use the dependencies listed in it.

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>knowcean</groupId>
  <artifactId>knowcean</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <name>knowcean</name>
  <dependencies>
    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-log4j12</artifactId>
      <version>1.5.8</version>
    </dependency>
    <!-- Ehcache -->
    <dependency>
      <groupId>net.sf.ehcache</groupId>
      <artifactId>ehcache-core</artifactId>
      <version>2.5.2</version>
    </dependency>
  </dependencies>
</project>

One of the other many beautiful things about ehcache is, it is dead simple to distribute it among multiple JVMs for a shared cache by using Terracotta. Not only can Terracotta distribute ehcache among multiple JVMs, but those JVMs can be running on many remote machines or VMs.

I will cover distributing ehcache via Terracotta in a later entry.