Simple Java name based locks?

2019-01-02 20:40发布

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 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
}

标签: java locking
22条回答
几人难应
2楼-- · 2019-01-02 21:07

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.

查看更多
旧时光的记忆
3楼-- · 2019-01-02 21:10

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.

查看更多
何处买醉
4楼-- · 2019-01-02 21:11

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
}
查看更多
柔情千种
5楼-- · 2019-01-02 21:13

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.

查看更多
登录 后发表回答