4

For a particular thread-safe data structure, I am needed to protect access to a central data structure (namely a byte array). I am choosing to use ReentrantLocks in this case for it's fairness policy as well as advanced capabilities with creating multiple conditions.

The conditions for concurrency are complex and listed below:

  1. The central byte array has to be protected exclusively (i.e. one thread at once).
  2. Two accessing methods (foo and bar) must be able to run concurrently (blocking internally should they try to access the central byte array).
  3. Calls to any method (foo and bar) need to be exclusive (i.e. multiple calls to foo from different threads will result in one thread blocking).

In my original implementation, I chose to implement two nested locks as below:

ReentrantLock lockFoo = new ReentrantLock(true);
ReentrantLock lockCentral = new ReentrantLock(true);

Condition centralCondition = lockCentral.newCondition();

public void foo(){
    // thread-safe processing code here

    lockFoo.lock();        
    lockCentral.lock();

    try{
        // accessing code here

        try{
            // waits upon some condition for access
            while(someCondition){
                centralCondition.await();
            }
        }catch(InterruptedException ex){
            // handling code here
        }

        // more processing
    }finally{
        lockCentral.unlock();
        lockFoo.unlock();
    }
}

the structure is equivalent in method bar, simply with another lock object lockBar. Additionally, the code has reduced my more complex multi-condition awaits and signals to a single condition for simplicity.

Using this, I can't help but feel the code seems unnecessarily complex and obscure in that not only that there are two locks nested, they share one try-finally, not to mention how lockCentral may be released and reacquired several times whilst lockFoo is held throughout.

Instead I tried to re-organize the outer lock (lockFoo and lockBar) as a condition of lockCentral instead, as below:

ReentrantLock lockCentral = new ReentrantLock(true);

Condition fooCondition = lockCentral.newCondition();
Condition centralCondition = lockCentral.newCondition();

boolean isInFoo = false;

public void foo(){
    // thread-safe processing code here

    lockCentral.lock();

    try{
        // implement method exclusiveness via fooCondition

        try{
            while(isInFoo){
                fooCondition.await();
            }

            isInFoo = true;
        }catch(InterruptedException ex){
            return;
        }

        // accessing code here

        try{
            // waits upon some condition for access
            while(someCondition){
                centralCondition.await();
            }
        }catch(InterruptedException ex){
            // handling code here
        }

        // more processing
    }finally{
        isInFoo = false;
        fooCondition.signal();

        lockCentral.unlock();
    }
}

after some inspection, I can't decide between whether the former is a better idea or the latter (especially with the inclusion of that random boolean). The idea of simplifying the code seems to have resulted in longer code, very counter-intuitive in this case.

Is there some convention or compelling reason to give argument to either:

  1. Using one lock per locking context (the former code, where different reasons for locking share different locks).

  2. Using one lock per locking resource (the latter code, where the central structure to be protected uses a single lock, everything else implemented as conditions to access said structure).

initramfs
  • 8,275
  • 2
  • 36
  • 58

2 Answers2

5

Latter code differs from the former one just by manual implementation of the lock lockFoo with fooCondition (the same is true for bar-related part).

Because such lock implementation takes into account, that foo critical section is almost same as central one, it is garantee to be faster in case when no contention on foo() (waiting on fooCondition is never performed in that case).

Aside from performance reasons, the former code is preferrable, as it is self-documented. Also, it is extendable to the case when data, protected by the lockFoo, are needed be accessed without lockCentral. In that case manual implementation of the lock loses its performance gain.

Tsyvarev
  • 60,011
  • 17
  • 110
  • 153
  • Hmm... I see your point. I didn't think about the performance gains from the latter, does attempting to lock on a ReentrantLock cause sizable performance degradation (under contention)? Especially with the overhead of accessing the global `lockCentral` (since the `fooCondition` must somehow recognize the central mutex). Benchmarks aren't giving me conclusive results on my computer and this does happen to be a very performance critical segment of code. – initramfs Apr 28 '15 at 13:57
0

In your original example, the lockFoo and lockBar locks are completely redundant since neither foo() nor bar() can do any work without locking the lockCentral lock. Unless you change the design of the program, lockCentral is the only lock you need.


You said you thought your first example was "too complicated", but your second example is way more complicated. It looks as if you are merely trying to replace lockFoo and lockBar with locking code of your own design. But what's the point of that? It won't do anything different from what your first example does.


What is the purpose of the locking anyway? You said "Calls to any method (foo and bar) need to be exclusive". That's starting off on the wrong foot: Don't use locks to protect methods; Use locks to protec data.

What is this "central byte array?" What do the threads do to it? Why does it need to be protected?

What are the data on which foo() operates? Why does it need to be protected? What are the data on which bar() operates? Why does that need to be protected?

Most important of all, do the foo() data and the bar() data both need to be protected at the same time as the central byte array?

In a well designed program, threads should do most of their work without holding any locks:

SomeObject someObject;
SomeOtherObject someOtherObject;
boolean success = false;
while (! success) {

    someLock.lock();
    try {
        someObject = getLocalCopyOfSomeData();
        someOtherObject = getLocalCopyOfSomeOtherData();
    } finally {
        someLock.unlock();
    }

    doTheRealWork(someObject, someOtherObject);

    someLock.lock();
    try {
        success = updateCentralCopyOf(someObject) || updateCentralCopyOf(someOtherObject);
    } finally {
        someLock.unlock();
    }
}
Solomon Slow
  • 25,130
  • 5
  • 37
  • 57
  • Firstly, I didn't say foo and bar need be exclusive, I said they needed to work **concurrently**. Perhaps I've oversimplified my situation too far. I can assure you that the code cannot survive on a single `lockCentral` as it would result in data corruption. Note how `lockCentral` has to be released and reacquired during the execution of the method when some condition calls `await()` (in my actual implementation, it has to do this several times, in stages). – initramfs Apr 28 '15 at 14:36
  • Take the following example: if thread A enters method foo, holds `lockCentral`, modifies some variables, then releases `lockCentral` as part of a condition. A opportunistic thread B could then hold `lockCentral`, entering method foo and modify variables in an invalid way. `lockFoo` is/was there to stop any other thread entering method foo until thread A can complete (i.e. being signaled by another thread from another method [e.g. bar]). – initramfs Apr 28 '15 at 14:36
  • @CPUTerminator My comments were based mostly on your first example. The code in your first example will not permit one thread to do _work_ in foo() while another thread works in bar() because neither of those methods does any work without holding the central lock. I admit that I did not earlier notice that it _is_ possible for one thread to `wait()` in foo() while another thread does work in bar(). But, I still question what's the point of allowing one thread into foo() and one into bar() if they can't both _work_ at the same time? – Solomon Slow Apr 28 '15 at 14:51
  • @CPUTerminator, I still think that you should shift your viewpoint: Stop talking about locking _methods_, and talk instead about locking _data_. Methods are just a means to operate on data. If you think more about the data---what states in can be in while thread A is working on it, what states you want thread B to be able to see---you will end up with a better design. – Solomon Slow Apr 28 '15 at 14:54
  • One method caters to data consumers whilst another caters to data producers. There are many such method pairs in this class, I simplified to one pair of foo and bar to avoid excessive unneeded detail. The central lock is held to protect the data the producers make for the consumers (after processing). This class can be thought to be very similar to a cyclic buffer (or a pair of PipedInputStream and PipedOutputStream in java's standard library) or perhaps the intermediate chain in a video decoder. – initramfs Apr 28 '15 at 14:58
  • I fully understand what locks are for. That's kind of why I decided to change my locking technique to a single ReentrantLock on multiple conditions (my latter code), though it did turn out to be more complex. The locks are there to protect data, **not** methods. Yet, most of the method needs to be enclosed in a lock to prevent interleaved access to the method by multiple threads. My phrasing of "central lock" may be inappropriate I guess. What I mean is, a primary large data structure with many supporting primitives (ints, longs, doubles) to maintain state. – initramfs Apr 28 '15 at 14:59