Simple Java name based locks?
Asked Answered
N

23

68

MySQL has a handy function:

SELECT GET_LOCK("SomeName")

This can be used to create simple, but very specific, name-based locks for an application. However, it requires a database connection.

I have many situations like:

someMethod() {
    // do stuff to user A for their data for feature X
}

It doesn't make sense to simply synchronize this method, because, for example, if this method is called for user B in the meantime, user B does not need to wait for user A to finish before it starts, only operations for the user A and feature X combination need to wait.

With the MySql lock I could do something like:

someMethod() {
    executeQuery("SELECT GET_LOCK('userA-featureX')")
    // only locked for user A for their data for feature X
    executeQuery("SELECT RELEASE_LOCK('userA-featureX')")
}

Since Java locking is based on objects, it seems like I would need to create a new object to represent the situation for this lock and then put it in a static cache somewhere so all the threads can see it. Subsequent requests to lock for that situation would then locate the lock object in the cache and acquire its lock. I tried to create something like this, but then the lock cache itself needs synchronization. Also, it is difficult to detect when a lock object is no longer being used so that it can be removed from the cache.

I have looked at the Java concurrent packages, but nothing stands out as being able to handle something like this. Is there an easy way to implement this, or am I looking at this from the wrong perspective?

Edit:

To clarify, I am not looking to create a predefined pool of locks ahead of time, I would like to create them on demand. Some pseudo-code for what I am thinking of is:

LockManager.acquireLock(String name) {
    Lock lock;  

    synchronized (map) {
        lock = map.get(name);

        // doesn't exist yet - create and store
        if(lock == null) {
            lock = new Lock();
            map.put(name, lock);
        }
    }

    lock.lock();
}

LockManager.releaseLock(String name) {
    // unlock
    // if this was the last hold on the lock, remove it from the cache
}
Narra answered 12/4, 2011 at 18:26 Comment(2)
I recommend this answer, usng Guava's Striped<Lock> to avoid excessive memory usage: https://mcmap.net/q/282154/-how-to-acquire-a-lock-by-a-keyEduce
Possible duplicate of Synchronizing on String objects in JavaBonnibelle
S
18

maybe this is useful for you: jkeylockmanager

Edit:

My initial response was probably a bit short. I am the author and was faced with this problem several times and could not find an existing solution. That's why I made this small library on Google Code.

Saunder answered 21/10, 2012 at 15:5 Comment(2)
@david.perez simple github search led me to github.com/mojgh/JKeyLockManager . Judging by the username, the author is same here too.Paring
@Saunder I would be interested to know if you are holding strong references to the keys - or are they released for garbage collection when no longer needed ?Uninstructed
P
45

All those answers I see are way too complicated. Why not simply use:

public void executeInNamedLock(String lockName, Runnable runnable) {
  synchronized(lockName.intern()) {
    runnable.run();
  }
}

The key point is the method intern: it ensures that the String returned is a global unique object, and so it can be used as a vm-instance-wide mutex. All interned Strings are held in a global pool, so that's your static cache you were talking about in your original question. Don't worry about memleaks; those strings will be gc'ed if no other thread references it. Note however, that up to and including Java6 this pool is kept in PermGen space instead of the heap, so you might have to increase it.

There's a problem though if some other code in your vm locks on the same string for completely different reasons, but a) this is very unlikely, and b) you can get around it by introducing namespaces, e.g. executeInNamedLock(this.getClass().getName() + "_" + myLockName);

Pokey answered 19/12, 2012 at 16:40 Comment(6)
This looks like a brilliant solution - but how safe is this? is this working under an assumption of how the JVM currently works, or is it true by spec that intern()'ed values are "==" ?Dairying
Yes, this is true by spec. See JavaDoc for String.intern().Pokey
see this for why interning is a bad idea: #134488Birck
In that thread, all concerns are about introducing unseen dependencies by the global state that String.intern() creates, which I overcome with the "namespace" pattern in the last paragraph.Pokey
Guava Interner Provides equivalent behavior to String.intern() for other immutable typesBonnibelle
I recommend this answer, usng Guava's Striped<Lock> to avoid excessive memory usage: https://mcmap.net/q/282154/-how-to-acquire-a-lock-by-a-keyEduce
S
20

Can you have a Map<String, java.util.concurrent.Lock>? Each time you require a lock, you basically call map.get(lockName).lock().

Here's an example using Google Guava:

Map<String, Lock> lockMap = new MapMaker().makeComputingMap(new Function<String, Lock>() {
  @Override public Lock apply(String input) {
    return new ReentrantLock();
  }
});

Then lockMap.get("anyOldString") will cause a new lock to be created if required and returned to you. You can then call lock() on that lock. makeComputingMap returns a Map that is thread-safe, so you can just share that with all your threads.

Subplot answered 12/4, 2011 at 18:31 Comment(7)
This is close to what I am looking for, but I was hoping to create something with just the standard library and perhaps the apache commons library. This solves how to create the locks on demand, but I'm still not sure how to clean up old locks that aren't being used. There may be thousands of users, but only about a hundred at most would be active at one time.Narra
I would argue you should consider Google Guava part of your standard library. It is incredibly useful and much higher quality than the Apache commons library. How do you know when a lock will no longer be used or is not already locked? You can ask MapMaker to expire entries that were created some time ago, but this can cause violations to the mutual exclusion rule you're trying to create (e.g. if somebody holds the lock for more than the expiration time).Subplot
@srj: Wouldn't simply using weakValues() do? If a thread holding the lock terminates (with or without releasing it), then it will be GC'ed.Bondage
@Subplot will I end up with one object in the map for every different String that I call a lock in?Disillusionize
yes it will create a lock for each string key that you specify. As @Bondage suggested you can use weakValues to make the map not hold a strong reference to the locks, just make sure you hold a reference to the lock while you have it openSubplot
@Subplot I was surprised that a thread that had taken a lock doesn't by itself guarantee a strong reference to it!Wayward
MapMaker is scheduled for deprecation because of the way it writes keys on a java.util.Map#get call, which was deemed a bit too magical, and had unintended consequences. See github.com/google/guava/wiki/MapMakerMigrationEarreach
U
20
// pool of names that are being locked
HashSet<String> pool = new HashSet<String>(); 

lock(name)
    synchronized(pool)
        while(pool.contains(name)) // already being locked
            pool.wait();           // wait for release
        pool.add(name);            // I lock it

unlock(name)
    synchronized(pool)
        pool.remove(name);
        pool.notifyAll();
Ultramundane answered 12/4, 2011 at 19:43 Comment(5)
It's not a fault in the implementation, but I think that callers should note that this code doesn't allow reentrant synchronization. Misuse may result in deadlock.Norval
I think it'll cause a deadlock. Example, T1 locks on "a". T2 tries to lock on "a" too so it stops at pool.wait() and also hold synchronized(pool). then T1 call unlock but cannot enter in synchronized(pool) block so it's a deadlock.Barometer
@Barometer A call to pool.wait() makes the thread lose the lock on pool.Autogenous
@Norval That could be fixed by replacing Set<String> with Map<String, Thread> and updating the while condition.Autogenous
@BartvanHeukelom However, you have to be careful: should also keep a count of locks acquired or you get weird semantics where you can lock twice and unlock once.Thies
S
18

maybe this is useful for you: jkeylockmanager

Edit:

My initial response was probably a bit short. I am the author and was faced with this problem several times and could not find an existing solution. That's why I made this small library on Google Code.

Saunder answered 21/10, 2012 at 15:5 Comment(2)
@david.perez simple github search led me to github.com/mojgh/JKeyLockManager . Judging by the username, the author is same here too.Paring
@Saunder I would be interested to know if you are holding strong references to the keys - or are they released for garbage collection when no longer needed ?Uninstructed
H
12

Maybe a little later but you can use Google Guava Striped

Conceptually, lock striping is the technique of dividing a lock into many stripes, increasing the granularity of a single lock and allowing independent operations to lock different stripes and proceed concurrently, instead of creating contention for a single lock.

//init
stripes=Striped.lazyWeakLock(size);
//or
stripes=Striped.lock(size);
//...
Lock lock=stripes.get(object);
Hite answered 15/12, 2015 at 21:58 Comment(2)
what is the right 'size' to give? I am using 10 and not sure how it worksThreecornered
The size parameter determines how many individual locks are managed under the covers. Striped assigns locks to keys based on hash codes. You should set size to be proportional to the number of objects you're trying to protect with locking. For example, using size=1 to protect 100 objects is the same as exclusive locking; size=2 will use 2 locks for all 100 objects, so you only have a 50% chance of any concurrency at all; etc. It may also be worth considering how many threads are sharing access when computing size.Seriatim
N
8

For locking on something like a user name, in-memory Locks in a map might be a bit leaky. As an alternative, you could look at using WeakReferences with WeakHashMap to create mutex objects that can be garbage collected when nothing refers to them. This avoids you having to do any manual reference counting to free up memory.

You can find an implementation here. Note that if you're doing frequent lookups on the map you may run into contention issues acquiring the mutex.

Norval answered 12/4, 2011 at 18:57 Comment(2)
It is sufficient to prevent memory leaking in most cases if unlock code removes map entry and is enclosed in finally block. As maximum number of concurrently serviced users tend to be limited by number of active threads. See an example in my answer: https://mcmap.net/q/280666/-simple-java-name-based-locksBonnibelle
Weak references involve additional complexity due to link-death checks.Bonnibelle
U
5

A generic solution using java.util.concurrent

import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReentrantLock;

public class LockByName<L> {

    ConcurrentHashMap<String, L> mapStringLock;

    public LockByName(){
        mapStringLock = new ConcurrentHashMap<String, L>();
    }

    public LockByName(ConcurrentHashMap<String, L> mapStringLock){
        this.mapStringLock = mapStringLock;
    }

    @SuppressWarnings("unchecked")
    public L getLock(String key) {
        L initValue = (L) createIntanceLock();
        L lock = mapStringLock.putIfAbsent(key, initValue);
        if (lock == null) {
            lock = initValue;
        }
        return lock;
    }

    protected Object createIntanceLock() {
        return new ReentrantLock();
    }

    public static void main(String[] args) {

        LockByName<ReentrantLock> reentrantLocker = new LockByName<ReentrantLock>();

        ReentrantLock reentrantLock1 = reentrantLocker.getLock("pepe");

        try {
            reentrantLock1.lock();
            //DO WORK

        }finally{
            reentrantLock1.unlock();

        }


    }

}
Underscore answered 30/11, 2013 at 22:34 Comment(1)
It appears the map can only grow in size. Also, why check if (lock == null), it has to at least return the one you passed in.Tumbler
S
4

Based on the answer of McDowell and his class IdMutexProvider, I have written the generic class LockMap that uses WeakHashMap to store lock objects. LockMap.get() can be used to retrieve a lock object for a key, which can then be used with the Java synchronized (...) statement to apply a lock. Unused lock objects are automatically freed during garbage collection.

import java.lang.ref.WeakReference;
import java.util.WeakHashMap;

// A map that creates and stores lock objects for arbitrary keys values.
// Lock objects which are no longer referenced are automatically released during garbage collection.
// Author: Christian d'Heureuse, www.source-code.biz
// Based on IdMutexProvider by McDowell, http://illegalargumentexception.blogspot.ch/2008/04/java-synchronizing-on-transient-id.html
// See also https://mcmap.net/q/280666/-simple-java-name-based-locks
public class LockMap<KEY> {

private WeakHashMap<KeyWrapper<KEY>,WeakReference<KeyWrapper<KEY>>> map;

public LockMap() {
   map = new WeakHashMap<KeyWrapper<KEY>,WeakReference<KeyWrapper<KEY>>>(); }

// Returns a lock object for the specified key.
public synchronized Object get (KEY key) {
   if (key == null) {
      throw new NullPointerException(); }
   KeyWrapper<KEY> newKeyWrapper = new KeyWrapper<KEY>(key);
   WeakReference<KeyWrapper<KEY>> ref = map.get(newKeyWrapper);
   KeyWrapper<KEY> oldKeyWrapper = (ref == null) ? null : ref.get();
   if (oldKeyWrapper != null) {
      return oldKeyWrapper; }
   map.put(newKeyWrapper, new WeakReference<KeyWrapper<KEY>>(newKeyWrapper));
   return newKeyWrapper; }

// Returns the number of used entries in the map.
public synchronized int size() {
   return map.size(); }

// KeyWrapper wraps a key value and is used in three ways:
// - as the key for the internal WeakHashMap
// - as the value for the internal WeakHashMap, additionally wrapped in a WeakReference
// - as the lock object associated to the key
private static class KeyWrapper<KEY> {
   private KEY key;
   private int hashCode;
   public KeyWrapper (KEY key) {
      this.key = key;
      hashCode = key.hashCode(); }
   public boolean equals (Object obj) {
      if (obj == this) {
         return true; }
      if (obj instanceof KeyWrapper) {
         return ((KeyWrapper)obj).key.equals(key); }
      return false; }
   public int hashCode() {
      return hashCode; }}

} // end class LockMap

Example of how to use the LockMap class:

private static LockMap<String> lockMap = new LockMap<String>();

synchronized (lockMap.get(name)) {
   ... 
}

A simple test program for the LockMap class:

public static Object lock1;
public static Object lock2;

public static void main (String[] args) throws Exception {
   System.out.println("TestLockMap Started");
   LockMap<Integer> map = new LockMap<Integer>();
   lock1 = map.get(1);
   lock2 = map.get(2);
   if (lock2 == lock1) {
      throw new Error(); }
   Object lock1b = map.get(1);
   if (lock1b != lock1) {
      throw new Error(); }
   if (map.size() != 2) {
      throw new Error(); }
   for (int i=0; i<10000000; i++) {
      map.get(i); }
   System.out.println("Size before gc: " + map.size());   // result varies, e.g. 4425760
   System.gc();
   Thread.sleep(1000);
   if (map.size() != 2) {
      System.out.println("Size after gc should be 2 but is " + map.size()); }
   System.out.println("TestLockMap completed"); }

If anyone knows a better way to automatically test the LockMap class, please write a comment.

Saintmihiel answered 16/10, 2012 at 0:42 Comment(3)
That's the right approach, but: (1) In your map, you don't need to store twice the key. You can map the key to a boolean and that's it. (2) Using the class with a "new" each time is annoying. I'd recommend giving up the generics, and either use a singleton, or a static map.Oleta
@NadavB The map key must be referenced by the value that is used for the lock to guarantee that the WeakHashMap entry is not removed by garbage collection.Saxon
@Christiand'Heureuse I guess the implementation would be simpler if Java would allow us to retrieve the key object instead of the value object right? That way the get() function could simply return the key object, and you wouldn't have to wrap the value with the WeakReference, etc.Oleta
B
4

I'd like to notice that ConcurrentHashMap has built-in locking facility that is enough for simple exclusive multithread lock. No additional Lock objects needed.

Here is an example of such lock map used to enforce at most one active jms processing for single client.

private static final ConcurrentMap<String, Object> lockMap = new ConcurrentHashMap<String, Object>();
private static final Object DUMMY = new Object();

private boolean tryLock(String key) {
    if (lockMap.putIfAbsent(key, DUMMY) != null) {
        return false;
    }
    try {
        if (/* attempt cluster-wide db lock via select for update nowait */) {
            return true;
        } else {
            unlock(key);
            log.debug("DB is already locked");
            return false;
        }
    } catch (Throwable e) {
        unlock(key);
        log.debug("DB lock failed", e);
        return false;
    }
}

private void unlock(String key) {
    lockMap.remove(key);
}

@TransactionAttribute(TransactionAttributeType.REQUIRED)
public void onMessage(Message message) {
    String key = getClientKey(message);
    if (tryLock(key)) {
        try {
            // handle jms
        } finally {
            unlock(key);
        }
    } else {
        // key is locked, forcing redelivery
        messageDrivenContext.setRollbackOnly();
    }
}
Bonnibelle answered 20/6, 2014 at 14:22 Comment(1)
See also extended version of lock map with auto cleanup in my another answer: #134488Bonnibelle
E
3

2 years later but I was looking for a simple named locker solution and came across this, was usefull but I needed a simpler answer, so below what I came up with.

Simple lock under some name and release again under that same name.

private void doTask(){
  locker.acquireLock(name);
  try{
    //do stuff locked under the name
  }finally{
    locker.releaseLock(name);
  }
}

Here is the code:

public class NamedLocker {
    private ConcurrentMap<String, Semaphore> synchSemaphores = new ConcurrentHashMap<String, Semaphore>();
    private int permits = 1;

    public NamedLocker(){
        this(1);
    }

    public NamedLocker(int permits){
        this.permits = permits;
    }

    public void acquireLock(String... key){
        Semaphore tempS = new Semaphore(permits, true);
        Semaphore s = synchSemaphores.putIfAbsent(Arrays.toString(key), tempS);
        if(s == null){
            s = tempS;
        }
        s.acquireUninterruptibly();
    }

    public void releaseLock(String... key){
        Semaphore s = synchSemaphores.get(Arrays.toString(key));
        if(s != null){
            s.release();
        }
    }
}
Erikerika answered 14/10, 2013 at 11:11 Comment(2)
This implementation does not take care of disposing old (unused) synch semaphores...Gadmon
Yes that is true. Fortunately my scenario had limited names. For a more generic use you need to have some cleanup.Erikerika
A
2

Memory consideration

Often times, synchronization needed for a particular key is short-lived. Keeping around released keys can lead to excessive memory waste, making it non-viable.

Here's an implementation does not internally keep around released keys.

import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.CountDownLatch;

public class KeyedMutexes<K> {

    private final ConcurrentMap<K, CountDownLatch> key2Mutex = new ConcurrentHashMap<>();

    public void lock(K key) throws InterruptedException {
        final CountDownLatch ourLock = new CountDownLatch(1);
        for (;;) {
            CountDownLatch theirLock = key2Mutex.putIfAbsent(key, ourLock);
            if (theirLock == null) {
                return;
            }
            theirLock.await();
        }
    }

    public void unlock(K key) {
        key2Mutex.remove(key).countDown();
    }
}

Reentrancy, and other bells and whistles

If one wants re-entrant lock semantics, they can extend the above by wrapping the mutex object in a class that keeps track of the locking thread and locked count.

e.g.:

private static class Lock {
    final CountDownLatch mutex = new CountDownLatch(1);

    final long threadId = Thread.currentThread().getId();

    int lockedCount = 1;
}

If one wants lock() to return an object to make releases easier and safer, that's also a possibility.

Putting it all together, here's what the class could look like:

public class KeyedReentrantLocks<K> {

    private final ConcurrentMap<K, KeyedLock> key2Lock = new ConcurrentHashMap<>();

    public KeyedLock acquire(K key) throws InterruptedException {
        final KeyedLock ourLock = new KeyedLock() {
            @Override
            public void close() {
                if (Thread.currentThread().getId() != threadId) {
                    throw new IllegalStateException("wrong thread");
                }
                if (--lockedCount == 0) {
                    key2Lock.remove(key);
                    mutex.countDown();
                }
            }
        };
        for (;;) {
            KeyedLock theirLock = key2Lock.putIfAbsent(key, ourLock);
            if (theirLock == null) {
                return ourLock;
            }
            if (theirLock.threadId == Thread.currentThread().getId()) {
                theirLock.lockedCount++;
                return theirLock;
            }
            theirLock.mutex.await();
        }
    }

    public static abstract class KeyedLock implements AutoCloseable {
        protected final CountDownLatch mutex = new CountDownLatch(1);
        protected final long threadId = Thread.currentThread().getId();
        protected int lockedCount = 1;

        @Override
        public abstract void close();
    }
}

And here's how one might use it:

try (KeyedLock lock = locks.acquire("SomeName")) {

    // do something critical here
}
Audacity answered 13/4, 2016 at 7:48 Comment(1)
Your solution is both the most elegant and the most reliable. And it's sooo undervoted, especially compared to the String.intern() memory waster.Diphyllous
G
2

Many implementations but non similar to mine.

Called my Dynamic lock implementation as ProcessDynamicKeyLock because it's a single process lock, for any object as key (equals+hashcode for uniqueness).

TODO: Add a way to provide the actual lock, for example, ReentrantReadWriteLock instead of ReentrantLock.

Implementation:

public class ProcessDynamicKeyLock<T> implements Lock
{
    private final static ConcurrentHashMap<Object, LockAndCounter> locksMap = new ConcurrentHashMap<>();

    private final T key;

    public ProcessDynamicKeyLock(T lockKey)
    {
        this.key = lockKey;
    }

    private static class LockAndCounter
    {
        private final Lock lock = new ReentrantLock();
        private final AtomicInteger counter = new AtomicInteger(0);
    }

    private LockAndCounter getLock()
    {
        return locksMap.compute(key, (key, lockAndCounterInner) ->
        {
            if (lockAndCounterInner == null) {
                lockAndCounterInner = new LockAndCounter();
            }
            lockAndCounterInner.counter.incrementAndGet();
            return lockAndCounterInner;
        });
    }

    private void cleanupLock(LockAndCounter lockAndCounterOuter)
    {
        if (lockAndCounterOuter.counter.decrementAndGet() == 0)
        {
            locksMap.compute(key, (key, lockAndCounterInner) ->
            {
                if (lockAndCounterInner == null || lockAndCounterInner.counter.get() == 0) {
                    return null;
                }
                return lockAndCounterInner;
            });
        }
    }

    @Override
    public void lock()
    {
        LockAndCounter lockAndCounter = getLock();

        lockAndCounter.lock.lock();
    }

    @Override
    public void unlock()
    {
        LockAndCounter lockAndCounter = locksMap.get(key);
        lockAndCounter.lock.unlock();

        cleanupLock(lockAndCounter);
    }


    @Override
    public void lockInterruptibly() throws InterruptedException
    {
        LockAndCounter lockAndCounter = getLock();

        try
        {
            lockAndCounter.lock.lockInterruptibly();
        }
        catch (InterruptedException e)
        {
            cleanupLock(lockAndCounter);
            throw e;
        }
    }

    @Override
    public boolean tryLock()
    {
        LockAndCounter lockAndCounter = getLock();

        boolean acquired = lockAndCounter.lock.tryLock();

        if (!acquired)
        {
            cleanupLock(lockAndCounter);
        }

        return acquired;
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException
    {
        LockAndCounter lockAndCounter = getLock();

        boolean acquired;
        try
        {
            acquired = lockAndCounter.lock.tryLock(time, unit);
        }
        catch (InterruptedException e)
        {
            cleanupLock(lockAndCounter);
            throw e;
        }

        if (!acquired)
        {
            cleanupLock(lockAndCounter);
        }

        return acquired;
    }

    @Override
    public Condition newCondition()
    {
        LockAndCounter lockAndCounter = locksMap.get(key);

        return lockAndCounter.lock.newCondition();
    }
}

Simple test:

public class ProcessDynamicKeyLockTest
{
    @Test
    public void testDifferentKeysDontLock() throws InterruptedException
    {
        ProcessDynamicKeyLock<Object> lock = new ProcessDynamicKeyLock<>(new Object());
        lock.lock();
        AtomicBoolean anotherThreadWasExecuted = new AtomicBoolean(false);
        try
        {
            new Thread(() ->
            {
                ProcessDynamicKeyLock<Object> anotherLock = new ProcessDynamicKeyLock<>(new Object());
                anotherLock.lock();
                try
                {
                    anotherThreadWasExecuted.set(true);
                }
                finally
                {
                    anotherLock.unlock();
                }
            }).start();
            Thread.sleep(100);
        }
        finally
        {
            Assert.assertTrue(anotherThreadWasExecuted.get());
            lock.unlock();
        }
    }

    @Test
    public void testSameKeysLock() throws InterruptedException
    {
        Object key = new Object();
        ProcessDynamicKeyLock<Object> lock = new ProcessDynamicKeyLock<>(key);
        lock.lock();
        AtomicBoolean anotherThreadWasExecuted = new AtomicBoolean(false);
        try
        {
            new Thread(() ->
            {
                ProcessDynamicKeyLock<Object> anotherLock = new ProcessDynamicKeyLock<>(key);
                anotherLock.lock();
                try
                {
                    anotherThreadWasExecuted.set(true);
                }
                finally
                {
                    anotherLock.unlock();
                }
            }).start();
            Thread.sleep(100);
        }
        finally
        {
            Assert.assertFalse(anotherThreadWasExecuted.get());
            lock.unlock();
        }
    }
}
Glaudia answered 22/5, 2018 at 15:25 Comment(0)
U
2

Another possible solution which I have implemented and tested when encountered the same requirements as the original poster.
In this solution:

  • No external libraries
  • Not leaving unused objects in memory
  • Minimal usage of synchronized and minimal "cross-names" locking
  • No downsides of using intern

Helper class code:

public class IdBasedLockHelper<T> {

    private final static AtomicIntegerWithEquals zero = new AtomicIntegerWithEquals(0);
    private final ConcurrentMap<T, AtomicIntegerWithEquals> identifierToLockCounter = new ConcurrentHashMap<>();
    

    public void executeLocked(T lockId, Runnable runnable) {
        AtomicIntegerWithEquals counterAndLock = identifierToLockCounter.compute(lockId, (key, existing) -> {
            if (existing == null) {
                return new AtomicIntegerWithEquals(1);
            }
            existing.atomicValue.incrementAndGet();
            return existing;
        });

        synchronized (counterAndLock) {
            try {
                runnable.run();
            } finally {
                counterAndLock.atomicValue.decrementAndGet();
                identifierToLockCounter.remove(lockId, zero);
            }
        }
    }

    // AtomicInteger does not implement equals() properly so there is a need for such wrapper
    private static class AtomicIntegerWithEquals {

        private final AtomicInteger atomicValue;

        AtomicIntegerWithEquals(int value) {
            this.atomicValue = new AtomicInteger(value);
        }

        // Used internally by remove()
        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (!(o instanceof IdBasedLockHelper.AtomicIntegerWithEquals)) return false;
            return atomicValue.get() == ((AtomicIntegerWithEquals) o).atomicValue.get();
        }

        // Not really used, but when implementing custom equals() it is a good practice to implement also hashCode()
        @Override
        public int hashCode() {
            return atomicValue.get();
        }
    }
}

Usage:

IdBasedLockHelper<String> idBasedLockHelper = new IdBasedLockHelper<>();

idBasedLockHelper.executeLocked("Some Name", () -> {
    // Your code to execute synchronized per name
});

ConcurrentHashMap is used to store synchronization object for each lock id.
ConcurrentHashMap already provides compute and remove (if value equals) as atomic operations. The AtomicInteger inside the stored value counts the number of holds of the synchronization object and this allows removing it from the map only if it is not in use (number of holds equals 0).

Utah answered 14/3, 2021 at 20:37 Comment(0)
W
1

Maybe something like that:

public class ReentrantNamedLock {

private class RefCounterLock {

    public int counter;
    public ReentrantLock sem;

    public RefCounterLock() {
        counter = 0;
        sem = new ReentrantLock();
    }
}
private final ReentrantLock _lock = new ReentrantLock();
private final HashMap<String, RefCounterLock> _cache = new HashMap<String, RefCounterLock>();

public void lock(String key) {
    _lock.lock();
    RefCounterLock cur = null;
    try {
        if (!_cache.containsKey(key)) {
            cur = new RefCounterLock();
            _cache.put(key, cur);
        } else {
            cur = _cache.get(key);
        }
        cur.counter++;
    } finally {
        _lock.unlock();
    }
    cur.sem.lock();
}

public void unlock(String key) {
    _lock.lock();
    try {
        if (_cache.containsKey(key)) {
            RefCounterLock cur = _cache.get(key);
            cur.counter--;
            cur.sem.unlock();
            if (cur.counter == 0) { //last reference
                _cache.remove(key);
            }
            cur = null;
        }
    } finally {
        _lock.unlock();
    }
}}

I didn't test it though.

Wassail answered 15/8, 2011 at 0:37 Comment(0)
D
1

After some disappointment that there is no language level support or simple Guava/Commons class for named locks,

This is what I settled down to:

ConcurrentMap<String, Object> locks = new ConcurrentHashMap<>();

Object getLock(String name) {
    Object lock = locks.get(name);
    if (lock == null) {
        Object newLock = new Object();
        lock = locks.putIfAbsent(name, newLock);
        if (lock == null) {
            lock = newLock;
        }
    }
    return lock;
}

void somethingThatNeedsNamedLocks(String name) {
    synchronized(getLock(name)) {
        // some operations mutually exclusive per each name
    }
}

Here I achieved: little boilerplate code with no library dependency, atomically acquiring the lock object, not polluting the global interned string objects, no low-level notify/wait chaos and no try-catch-finally mess.

Distinguished answered 6/12, 2013 at 7:6 Comment(4)
This is similar to [link]https://mcmap.net/q/280666/-simple-java-name-based-locks, but I wonder whether you need to do synchronize(getLock(name)). Seems like the concurrent hashmap will take care of that.Beeswing
@danb ConcurrentHashMap provides atomic(=lock-free) operations on its items but not mutexes out of the box. Since the author asked on-demand locks based on names, I have used a CHM to uniquely and atomically address the lock, and use a synchronized block which is cleaner than manually calling lock() and unlock().Distinguished
Right that makes sense here. The solution provided in the reference is more general in that you can return ReadWriteLocks. This is a good simple solution. Not sure why you did not get up votes. Very nice solution.Beeswing
This has the downside that it will only grow over time.Tumbler
M
1

Similar to the answer from Lyomi, but uses the more flexible ReentrantLock instead of a synchronized block.

public class NamedLock
{
    private static final ConcurrentMap<String, Lock> lockByName = new ConcurrentHashMap<String, Lock>();

    public static void lock(String key)
    {
        Lock lock = new ReentrantLock();
        Lock existingLock = lockByName.putIfAbsent(key, lock);

        if(existingLock != null)
        {
            lock = existingLock;
        }
        lock.lock();
    }

    public static void unlock(String key) 
    {
        Lock namedLock = lockByName.get(key);
        namedLock.unlock();
    }
}

Yes this will grow over time - but using the ReentrantLock opens up greater possibilities for removing the lock from the map. Although, removing items from the map doesn't seem all that useful considering removing values from the map will not shrink its size. Some manual map sizing logic would have to be implemented.

Misdoing answered 3/8, 2015 at 19:14 Comment(0)
G
0

In response to the suggestion of using new MapMaker().makeComputingMap()...

MapMaker().makeComputingMap() is deprecated for safety reasons. The successor is CacheBuilder. With weak keys/values applied to CacheBuilder, we're soooo close to a solution.

The problem is a note in CacheBuilder.weakKeys():

when this method is used, the resulting cache will use identity (==) comparison to determine equality of keys. 

This makes it impossible to select an existing lock by string value. Erg.

Greenhead answered 18/7, 2012 at 23:28 Comment(3)
Is it not sufficient to just use weakValues(), and not weakKeys()? When a lock stops being (strongly) reachable, the cache entry becomes a candidate for removal from the cache. weakValues() doesn't change the equality test for keys.Wayward
how is this an answer?Tumbler
Sorry, it wasn't a solution, but a response. If recall correctly, I didn't yet have the rights to post comments, just new answers. I guess maybe it was a bit inappropriate. Dumb system. Then again, maybe I'm recalling incorrectly.Greenhead
T
0

(4 years later...) My answer is similar to user2878608's but I think there are some missing edge cases in that logic. I also thought Semaphore was for locking multiple resources at once (though I suppose using it for counting lockers like that is fine too), so I used a generic POJO lock object instead. I ran one test on it which demonstrated each of the edge cases existed IMO and will be using it on my project at work. Hope it helps someone. :)

class Lock
{
    int c;  // count threads that require this lock so you don't release and acquire needlessly
}

ConcurrentHashMap<SomeKey, Lock> map = new ConcurrentHashMap<SomeKey, Lock>();

LockManager.acquireLock(String name) {
    Lock lock = new Lock();  // creating a new one pre-emptively or checking for null first depends on which scenario is more common in your use case
    lock.c = 0;

    while( true )
    {
        Lock prevLock = map.putIfAbsent(name, lock);
        if( prevLock != null )
            lock = prevLock;

        synchronized (lock)
        {
            Lock newLock = map.get(name);
            if( newLock == null )
                continue;  // handles the edge case where the lock got removed while someone was still waiting on it
            if( lock != newLock )
            {
                lock = newLock;  // re-use the latest lock
                continue;  // handles the edge case where a new lock was acquired and the critical section was entered immediately after releasing the lock but before the current locker entered the sync block
            }

            // if we already have a lock
            if( lock.c > 0 )
            {
                // increase the count of threads that need an offline director lock
                ++lock.c;
                return true;  // success
            }
            else
            {
                // safely acquire lock for user
                try
                {
                    perNameLockCollection.add(name);  // could be a ConcurrentHashMap or other synchronized set, or even an external global cluster lock
                    // success
                    lock.c = 1;
                    return true;
                }
                catch( Exception e )
                {
                    // failed to acquire
                    lock.c = 0;  // this must be set in case any concurrent threads are waiting
                    map.remove(name);  // NOTE: this must be the last critical thing that happens in the sync block!
                }
            }
        }
    }
}

LockManager.releaseLock(String name) {
    // unlock
    // if this was the last hold on the lock, remove it from the cache

    Lock lock = null;  // creating a new one pre-emptively or checking for null first depends on which scenario is more common in your use case

    while( true )
    {
        lock = map.get(name);
        if( lock == null )
        {
            // SHOULD never happen
            log.Error("found missing lock! perhaps a releaseLock call without corresponding acquireLock call?! name:"+name);
            lock = new Lock();
            lock.c = 1;
            Lock prevLock = map.putIfAbsent(name, lock);
            if( prevLock != null )
                lock = prevLock;
        }

        synchronized (lock)
        {
            Lock newLock = map.get(name);
            if( newLock == null )
                continue;  // handles the edge case where the lock got removed while someone was still waiting on it
            if( lock != newLock )
            {
                lock = newLock;  // re-use the latest lock
                continue;  // handles the edge case where a new lock was acquired and the critical section was entered immediately after releasing the lock but before the current locker entered the sync block
            }

            // if we are not the last locker
            if( lock.c > 1 )
            {
                // decrease the count of threads that need an offline director lock
                --lock.c;
                return true;  // success
            }
            else
            {
                // safely release lock for user
                try
                {
                    perNameLockCollection.remove(name);  // could be a ConcurrentHashMap or other synchronized set, or even an external global cluster lock
                    // success
                    lock.c = 0;  // this must be set in case any concurrent threads are waiting
                    map.remove(name);  // NOTE: this must be the last critical thing that happens in the sync block!
                    return true;
                }
                catch( Exception e )
                {
                    // failed to release
                    log.Error("unable to release lock! name:"+name);
                    lock.c = 1;
                    return false;
                }
            }
        }
    }

}
Tadeas answered 12/2, 2015 at 0:56 Comment(0)
G
0

I've created a tokenProvider based on the IdMutexProvider of McDowell. The manager uses a WeakHashMap which takes care of cleaning up unused locks.

TokenManager:

/**
 * Token provider used to get a {@link Mutex} object which is used to get exclusive access to a given TOKEN.
 * Because WeakHashMap is internally used, Mutex administration is automatically cleaned up when
 * the Mutex is no longer is use by any thread.
 *
 * <pre>
 * Usage:
 * private final TokenMutexProvider&lt;String&gt; myTokenProvider = new TokenMutexProvider&lt;String&gt;();
 *
 * Mutex mutex = myTokenProvider.getMutex("123456");
 * synchronized (mutex) {
 *  // your code here
 * }
 * </pre>
 *
 * Class inspired by McDowell.
 * url: http://illegalargumentexception.blogspot.nl/2008/04/java-synchronizing-on-transient-id.html
 *
 * @param <TOKEN> type of token. It is important that the equals method of that Object return true
 * for objects of different instances but with the same 'identity'. (see {@link WeakHashMap}).<br>
 * E.g.
 * <pre>
 *  String key1 = "1";
 *  String key1b = new String("1");
 *  key1.equals(key1b) == true;
 *
 *  or
 *  Integer key1 = 1;
 *  Integer key1b = new Integer(1);
 *  key1.equals(key1b) == true;
 * </pre>
 */
public class TokenMutexProvider<TOKEN> {

    private final Map<Mutex, WeakReference<Mutex>> mutexMap = new WeakHashMap<Mutex, WeakReference<Mutex>>();

    /**
     * Get a {@link Mutex} for the given (non-null) token.
     */
    public Mutex getMutex(TOKEN token) {
        if (token==null) {
            throw new NullPointerException();
        }

        Mutex key = new MutexImpl(token);
        synchronized (mutexMap) {
            WeakReference<Mutex> ref = mutexMap.get(key);
            if (ref==null) {
                mutexMap.put(key, new WeakReference<Mutex>(key));
                return key;
            }
            Mutex mutex = ref.get();
            if (mutex==null) {
                mutexMap.put(key, new WeakReference<Mutex>(key));
                return key;
            }
            return mutex;
        }
    }

    public int size() {
        synchronized (mutexMap) {
            return mutexMap.size();
        }
    }

    /**
     * Mutex for acquiring exclusive access to a token.
     */
    public static interface Mutex {}

    private class MutexImpl implements Mutex {
        private final TOKEN token;

        protected MutexImpl(TOKEN token) {
            this.token = token;
        }

        @Override
        public boolean equals(Object other) {
            if (other==null) {
                return false;
            }
            if (getClass()==other.getClass()) {
                TOKEN otherToken = ((MutexImpl)other).token;
                return token.equals(otherToken);
            }
            return false;
        }

        @Override
        public int hashCode() {
            return token.hashCode();
        }
    }
}

Usage:

private final TokenMutexManager<String> myTokenManager = new TokenMutexManager<String>();

Mutex mutex = myTokenManager.getMutex("UUID_123456");
synchronized(mutex) {
    // your code here
}

or rather use Integers?

private final TokenMutexManager<Integer> myTokenManager = new TokenMutexManager<Integer>();

Mutex mutex = myTokenManager.getMutex(123456);
synchronized(mutex) {
    // your code here
}
Gadmon answered 25/2, 2015 at 15:54 Comment(0)
A
0

This thread is old, but a possible solution is the framework https://github.com/brandaof/named-lock.

NamedLockFactory lockFactory = new NamedLockFactory();

...

Lock lock = lockFactory.getLock("lock_name");
lock.lock();

try{
  //manipulate protected state
}
finally{
    lock.unlock();
}
Annabellannabella answered 17/2, 2018 at 21:59 Comment(0)
F
0

Here is a simple and optimized solution which addresses the removal of used locks also, but with an overhead of synchronization of the Map:

public class NamedLock {
private Map<String, ReentrantLock> lockMap;

public NamedLock() {
    lockMap = new HashMap<>();
}

public void lock(String... name) {
    ReentrantLock newLock = new ReentrantLock(true);
    ReentrantLock lock;
    synchronized (lockMap) {
        lock = Optional.ofNullable(lockMap.putIfAbsent(Arrays.toString(name), newLock)).orElse(newLock);
    }
    lock.lock();
}

public void unlock(String... name) {
    ReentrantLock lock = lockMap.get(Arrays.toString(name));
    synchronized (lockMap) {
        if (!lock.hasQueuedThreads()) {
            lockMap.remove(name);
        }
    }
    lock.unlock();
}    

}

Fauver answered 27/4, 2018 at 14:10 Comment(0)
G
-1

Your idea about a shared static repository of lock objects for each situation is correct.
You don't need the cache itself to be synchronized ... it can be as simple as a hash map.

Threads can simultaneously get a lock object from the map. The actual synchronization logic should be encapsulated within each such object separately (see the java.util.concurrent package for that - http://download.oracle.com/javase/6/docs/api/java/util/concurrent/locks/package-summary.html)

Grangerize answered 12/4, 2011 at 18:36 Comment(0)
D
-2

TreeMap because in HashMap size of inner array can only increase

public class Locker<T> {
    private final Object lock = new Object();
    private final Map<T, Value> map = new TreeMap<T, Value>();

    public Value<T> lock(T id) {
        Value r;
        synchronized (lock) {
            if (!map.containsKey(id)) {
                Value value = new Value();
                value.id = id;
                value.count = 0;
                value.lock = new ReentrantLock();
                map.put(id, value);
            }
            r = map.get(id);
            r.count++;
        }
        r.lock.lock();
        return r;
    }

    public void unlock(Value<T> r) {
        r.lock.unlock();
        synchronized (lock) {
            r.count--;
            if (r.count == 0)
                map.remove(r.id);
        }
    }

    public static class Value<T> {

        private Lock lock;
        private long count;
        private T id;
    }
}
Dielectric answered 21/11, 2012 at 19:45 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.