Core Java

Fine-Grained Concurrency with the Guava Striped Class

This post is going to cover how to use the Striped class from Guava to achieve finer-grained concurrency. The ConcurrentHashMap uses a striped locked approach to increase concurrency and the Striped class extends this principal by giving us the ability to have striped Locks, ReadWriteLocks and Semaphores. When accessing an object or data-structure such as an Array or HashMap typically we would synchronize on the entire object or data-structure, but is this always necessary? In several cases the answer is yes, but there may be times where we don’t that level of course-grained locking and can improve the performance of the application by using a finer-grained approach.

The Use Case for Striped Locking/Semaphores

It’s considered a best practice to always synchronize the smallest portion of code that is possible. In other words, we only want to synchronize around the parts of the code that are changing shared data. Striping takes this a step further by allowing us to reduce synchronization by distinct tasks. For example, let’s say we have an ArrayList of URL’s that represent remote resources and we want to restrict the total number of threads accessing these resources at any given time. Restricting access for N number of threads is a natural fit for a java.util.concurrent.Semaphore object. But the problem is restricting access to our ArrayList becomes a bottleneck. This is where the Striped class helps. What we really want to do is restrict access to the resources, and not the container of the URL’s. Assuming each one is distinct, we use a “striped” approach where access to each URL is limited to N threads at a time, increasing the throughput and responsiveness of the application.

Creating Striped Locks/Semaphores

To create a Striped instance we simply use one of the static factor methods available from the Striped class. For example, here’s how we would create an instance of a striped ReadWriteLock:

Striped<ReadWriteLock> rwLockStripes = Striped.readWriteLock(10);

In the above example we created a Striped instance where the individual “stripes” are eagerly created, strong references. The impact is the locks/semaphores won’t be garbage collected unless the Striped object itself is removed. However, the Striped class gives us another option when it comes to creating locks/semaphores:

int permits = 5;
int numberStripes = 10;
Striped<Semaphore> lazyWeakSemaphore = Striped.lazyWeakSemaphore(numberStripes,permits);

In this example the Semaphore instances will be lazily created and wrapped in WeakReferences thus available immediately for garbage collection unless another object is holding on to it.

Accessing/Using Striped Locks/Semaphores

To use the Striped ReadWriteLock created earlier, we do the following:

String key = "taskA";
ReadWriteLock rwLock = rwLockStripes.get(key);
try{
     rwLock.lock();
     .....
}finally{
     rwLock.unLock();
}

By calling the Striped.get method we are returned a ReadWriteLock instance that corresponds with the given object key. We will more extensive examples below using the Striped class.

Lock/Semaphore Retrieval Guarantees

When it comes to retrieving lock/semaphore instances the Striped class guarantees when objectA equals objectB (assuming properly implemented equals and hashcode methods) a call to the Striped.get method will return the same lock/semaphore instance. But the converse is not guaranteed, that is to say when objectA is not equal to ObjectB, we may still retrieve locks/semaphores that are the same reference. According to the Javadoc for the Striped class, specifying fewer stripes increased the probability of getting the same reference for two different keys.

An Example

Let’s take a look at a very simple example based on the scenario we laid out in the introduction. We’ve constructed a class ConcurrentWorker where we want to restrict concurrent access to some resource. But let’s make an assumption there are some number of distinct resources so ideally, we want to restrict access per resource and not just restrict the ConcurrentWorker.

public class ConcurrentWorker {

    private Striped<Semaphore> stripedSemaphores = Striped.semaphore(10,3);
    private Semaphore semaphore = new Semaphore(3);

    public void stripedConcurrentAccess(String url) throws Exception{
       Semaphore stripedSemaphore  = stripedSemaphores.get(url);
        stripedSemaphore.acquire();
       try{
           //Access restricted resource here
           Thread.sleep(25);
       }finally{
           stripedSemaphore.release();
       }
    }

    public void nonStripedConcurrentAccess(String url) throws Exception{
        semaphore.acquire();
        try{
           //Access restricted resource here
            Thread.sleep(25);
        }finally{
            semaphore.release();
        }
    }
}

In this example we have two methods ConcurrentWorker.stripedConcurrentAccess and ConcurrentWorker.nonStripedConcurrentAccess so we can make an easy comparison. In both cases we simulate work by calling Thread.sleep for 25 milliseconds. We’ve create a single Semaphore instance and a Striped instance that contains 10 eagerly created, strongly referenced Semaphore objects. In both cases we’ve specified 3 permits, so at any time 3 threads will have access to our “resource”. Here is a simple driver class used to measure the throughput between both approaches.

public class StripedExampleDriver {

    private ExecutorService executorService = Executors.newCachedThreadPool();
    private int numberThreads = 300;
    private CountDownLatch startSignal = new CountDownLatch(1);
    private CountDownLatch endSignal = new CountDownLatch(numberThreads);
    private Stopwatch stopwatch = Stopwatch.createUnstarted();
    private ConcurrentWorker worker = new ConcurrentWorker();
    private static final boolean USE_STRIPES = true;
    private static final boolean NO_STRIPES = false;
    private static final int POSSIBLE_TASKS_PER_THREAD = 10;
    private List<String> data = Lists.newArrayList();

    public static void main(String[] args) throws Exception {
        StripedExampleDriver driver = new StripedExampleDriver();
        driver.createData();
        driver.runStripedExample();
        driver.reset();
        driver.runNonStripedExample();
        driver.shutdown();
    }

    private void runStripedExample() throws InterruptedException {
        runExample(worker, USE_STRIPES, "Striped work");
    }

    private void runNonStripedExample() throws InterruptedException {
        runExample(worker, NO_STRIPES, "Non-Striped work");
    }

    private void runExample(final ConcurrentWorker worker, final boolean isStriped, String type) throws InterruptedException {
        for (int i = 0; i < numberThreads; i++) {
            final String value = getValue(i % POSSIBLE_TASKS_PER_THREAD);
            executorService.submit(new Callable<Void>() {
                @Override
                public Void call() throws Exception {
                    startSignal.await();
                    if (isStriped) {
                        worker.stripedConcurrentAccess(value);
                    } else {
                        worker.nonStripedConcurrentAccess(value);
                    }
                    endSignal.countDown();
                    return null;
                }
            });
        }
        stopwatch.start();
        startSignal.countDown();
        endSignal.await();
        stopwatch.stop();
        System.out.println("Time for" + type + " work [" + stopwatch.elapsed(TimeUnit.MILLISECONDS) + "] millis");
    }
  //details left out for clarity

We took 300 threads and called both methods in two separate passes and used the StopWach class to record the times.

Results

As you might expect the striped version faired much better. Here’s the console output from one of the test runs:

Time forStriped work work [261] millis
Time forNon-Striped work work [2596] millis

While the Striped class is not a fit for every situation, it really comes in handy when you need concurrency around distinct data. Thanks for your time.

Resources

 

Bill Bejeck

Husband, father of 3, passionate about software development.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Inline Feedbacks
View all comments
Back to top button