0

I implemented a simple locking solution that creates a lock for a value rather than object and want to know the experts' opinion for possible performance or security drawbacks. The idea is to use it for account balance update acquiring the lock for unique account number.

Here is an implementation:

import java.util.*;

public class Mutex<T> {

    private final Set<T> set = new HashSet();

    public synchronized Lock acquireLock(
            T value
            ) throws InterruptedException {
        while(!set.add(value)) {
            this.wait();
        }
        return new Lock(value);
    }

    public class Lock {

        private final T value;

        public Lock(T value) {
            this.value = value;
        }

        public T getValue() {
            return value;
        }

        public void release() {
            synchronized(Mutex.this) {
                set.remove(value);
                Mutex.this.notifyAll();
            }
        }
    }
}

And here is a sample usage to check the operability:

public class Test {

    private Mutex mutex = new Mutex();

    public static void main(String[] args) {
        Test test = new Test();
        Thread t1 = new Thread(() -> {
            try {
                test.test("SameValue");
            } catch (InterruptedException ex) {
                ex.printStackTrace();
            }
        });
        t1.setName("Thread 1");
        Thread t2 = new Thread(() -> {
            try {
                test.test("SameValue");
            } catch (InterruptedException ex) {
                ex.printStackTrace();
            }
        });
        t2.setName("Thread 2");

        t1.start();
        t2.start();
    }

    public void test(String value)
            throws
            InterruptedException {
        Lock lock = mutex.acquireLock(value);
        try {
            Thread.sleep(5000);
            System.out.println(Thread.currentThread().getName());
        } finally {
            lock.release();
        }
    }
}
chaplean
  • 197
  • 1
  • 3
  • 10
  • 1
    Maybe I'm missing something, but I honestly don't see why this code is necessary. Wouldn't a simple `synchronized (value) { ... }` serve the exact same purpose, eliminating the need for your entire Mutex class? Sure, you could beef it up a little bit so that all equivalent strings refer to the same lock object, but I think you're reinventing the wheel with your Mutex class. – Charlie Armstrong Sep 26 '21 at 08:50
  • @CharlieArmstrong synchronized block uses object, and I have only Account IBAN value (String). I know about String.intern() method but it has problems with GC. Regarding "all equivalent strings refer to the same lock object" - we have millions of account records, so it will not be a good solution to store their IBANs in a HashMap – chaplean Sep 26 '21 at 09:06
  • Hmm ok I get what you're saying now. I don't know of a great solution off the top of my head. It's possible your solution is the simplest way to do it, just to me this feels like a situation where there's got to be a more built-in way to do this. I'd be surprised if none of the Java libraries had something for this. – Charlie Armstrong Sep 26 '21 at 09:16
  • @CharlieArmstrong agree, same with me, that is why my question is here :) Let's see – chaplean Sep 26 '21 at 10:03
  • 2
    There is no point in wrapping a `while(condition) …` loop in a `if(condition) …`, the loop does already have a pre-test. You’re just performing the same test twice when `true`. Besides that, now that you changed the `list` to a `Set`, you shouldn’t name the variable `list` either, further, the `add` contract is already sufficient for testing the presence. In other words, you can simply do `while(!set.add(value)) { this.wait(); } return new Lock(value);` and that’s it. – Holger Sep 27 '21 at 10:29
  • Thanks for the comment, good point! Optimizing the code above :) – chaplean Sep 27 '21 at 10:49

1 Answers1

2

Regarding your implementation,

I would have use a Set instead of a List to hold your values (I assume the values have proper equals/hashcode for this to make sense): the List#contains method is in O(n) which might be expensive if you have a lot of IBAN used at the same time.

Also, you should avoid using synchronize(this) (which is the same as the synchronized keyword on method).

To solve your problem, I use something like this:

import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class Locks<T> {

    private final Lock lock = new ReentrantLock();

    //a Bimap from guava might be better here if you have the dependency
    //in your project
    private final Map<Reference<?>, T> valuePerReference = new HashMap<>();
    private final Map<T, Reference<Lock>> locks = new HashMap<>();

    private final ReferenceQueue<Lock> lockReferenceQueue = new ReferenceQueue<>();

    public Locks() {
        final Thread cleanerThread = new Thread(new Cleaner());
        cleanerThread.setDaemon(true);
        cleanerThread.start();
    }

    /**
     * @param value the value the synchronization must be made on
     * @return a lock that can be used to synchronize block of code.
     */
    public Lock getLock(T value) {
        lock.lock();
        try {
            return getExistingLock(value).orElseGet(() -> createNewLock(value));
        } finally {
            lock.unlock();
        }
    }


    private Optional<Lock> getExistingLock(T value) {
        return Optional.ofNullable(locks.get(value)).map(Reference::get);
    }

    private Lock createNewLock(T value) {
        //I create ReentrantLock here but a Supplier<Lock> could be a parameter of this
        //class to make it more generic. Same remark for SoftReference below.
        final Lock lock = new ReentrantLock();
        final Reference<Lock> reference = new SoftReference<>(lock, lockReferenceQueue);
        this.locks.put(value,reference);
        this.valuePerReference.put(reference,value);
        return lock;
    }


    private void removeLock(Reference<?> reference) {
        lock.lock();
        try {
            final T value = valuePerReference.remove(reference);
            locks.remove(value);
        } finally {
            lock.unlock();
        }
    }


    private class Cleaner implements Runnable {
        @Override
        public void run() {
            while (!Thread.currentThread().isInterrupted()) {
                try {
                    final Reference<? extends Lock> garbaged = lockReferenceQueue.remove();
                    removeLock(garbaged);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
        }
    }

}

I then use this like this:

import java.util.concurrent.locks.Lock;

public class Usage {

    private final Locks<String> locks = new Locks<>();


    public void doSomethind(String iban) {
        final Lock lock = locks.getLock(iban);
        lock.lock();
        try {
            //.. do something with your iban
        } finally {
            lock.unlock();
        }
    }
}

Although it uses ReentrantLock, the code can be easily modified for ReadWriteLock for instance.

Bastien Aracil
  • 1,531
  • 11
  • 6
  • Thanks for the answer. However regarding synchronized(this) and Thread 3: when thread Thread 2 calls wait it releases the current object lock so Thread 3 should not be blocked even if Thread 2 is waiting – chaplean Sep 26 '21 at 12:01
  • 1
    You are right. After re-reading your code, it seems that there is a bug since the value is not re added after the while loop exits. – Bastien Aracil Sep 26 '21 at 13:06
  • Good find! Thank you :) – chaplean Sep 26 '21 at 21:56
  • Fixed the code in question. – chaplean Sep 26 '21 at 22:03
  • 2
    Why should the OP “avoid using synchronize(this)” but can’t just use a different, non-exposed object instead? Which actual advantage over the OP’s approach justifies this significantly more complicated code? – Holger Sep 27 '21 at 10:38
  • 1
    @Holger, it is preferable to use an internal object to synchronize on (you get a better explanation in the linked post). The OP implementation is fine. Mine is another one (not implemented because synchonize(this) should not be used). It is a bit more complex because it separates the memorization of the locks and the use of the locks. I also use the standard Java API for the lock and as such can use all their functionalities (like await, signalAll). It is also easily modifiable to use ReadWrtieLock. – Bastien Aracil Sep 27 '21 at 11:29