How to acquire a lock by a key

前端 未结 6 1346
一个人的身影
一个人的身影 2020-11-28 09:47

What is the best way to prevent concurrent update of one record in a key-value set without locking the entire set? Semantically, I\'m looking for some kind of locking by a k

相关标签:
6条回答
  • 2020-11-28 10:16

    I've written a class that can lock on any key dynamically. It uses a static CuncurrentHashMap. But if no lock is used, the map is empty. The syntax can be confusing as a new object us created based on the key. It cleans up the lock, if not used, on unlock. There's a guarantee that any two DynamicKeyLock that were created based on two equal/hascode keys, they'll be mutually locked.

    See implementation for Java 8, Java 6 and a small test.

    Java 8:

    public class DynamicKeyLock<T> implements Lock
    {
        private final static ConcurrentHashMap<Object, LockAndCounter> locksMap = new ConcurrentHashMap<>();
    
        private final T key;
    
        public DynamicKeyLock(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();
        }
    }
    

    Java 6:

    public class DynamicKeyLock<T> implements Lock
    {
        private final static ConcurrentHashMap<Object, LockAndCounter> locksMap = new ConcurrentHashMap<Object, LockAndCounter>();
        private final T key;
    
        public DynamicKeyLock(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()
        {
            while (true) // Try to init lock
            {
                LockAndCounter lockAndCounter = locksMap.get(key);
    
                if (lockAndCounter == null)
                {
                    LockAndCounter newLock = new LockAndCounter();
                    lockAndCounter = locksMap.putIfAbsent(key, newLock);
    
                    if (lockAndCounter == null)
                    {
                        lockAndCounter = newLock;
                    }
                }
    
                lockAndCounter.counter.incrementAndGet();
    
                synchronized (lockAndCounter)
                {
                    LockAndCounter lastLockAndCounter = locksMap.get(key);
                    if (lockAndCounter == lastLockAndCounter)
                    {
                        return lockAndCounter;
                    }
                    // else some other thread beat us to it, thus try again.
                }
            }
        }
    
        private void cleanupLock(LockAndCounter lockAndCounter)
        {
            if (lockAndCounter.counter.decrementAndGet() == 0)
            {
                synchronized (lockAndCounter)
                {
                    if (lockAndCounter.counter.get() == 0)
                    {
                        locksMap.remove(key);
                    }
                }
            }
        }
    
        @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();
        }
    }
    

    Test:

    public class DynamicKeyLockTest
    {
        @Test
        public void testDifferentKeysDontLock() throws InterruptedException
        {
            DynamicKeyLock<Object> lock = new DynamicKeyLock<>(new Object());
            lock.lock();
            AtomicBoolean anotherThreadWasExecuted = new AtomicBoolean(false);
            try
            {
                new Thread(() ->
                {
                    DynamicKeyLock<Object> anotherLock = new DynamicKeyLock<>(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();
            DynamicKeyLock<Object> lock = new DynamicKeyLock<>(key);
            lock.lock();
            AtomicBoolean anotherThreadWasExecuted = new AtomicBoolean(false);
            try
            {
                new Thread(() ->
                {
                    DynamicKeyLock<Object> anotherLock = new DynamicKeyLock<>(key);
                    anotherLock.lock();
                    try
                    {
                        anotherThreadWasExecuted.set(true);
                    }
                    finally
                    {
                        anotherLock.unlock();
                    }
                }).start();
                Thread.sleep(100);
            }
            finally
            {
                Assert.assertFalse(anotherThreadWasExecuted.get());
                lock.unlock();
            }
        }
    }
    
    0 讨论(0)
  • 2020-11-28 10:17

    Keep a mutex/lock per bucket. This will ensure that only collisions wait on that mutex.

    0 讨论(0)
  • 2020-11-28 10:25

    This is how; i did it. And yes I agree if two different strings shares the same hashcode will end up with acquiring the same lock.

    class LockByKey {
        ObjectForString objHolder = new ObjectForString(100);
        public void lockThenWorkForKey (String key) {
            synchronized(objHolder.valueOf(key)){
                //DoSomeWork
            }
        }
    }
    
    public final class ObjectForString {
    
        private final Object[] cache;
        private final int cacheSize;
        final int mask;
    
        public ObjectForString(int size) {
            // Find power-of-two sizes best matching arguments
            int ssize = 1;
            while (ssize < size) {
                ssize <<= 1;
            }
    
            mask = ssize - 1;
            cache = new Object[ssize];
            cacheSize = ssize;
            //build the Cache
            for (int i = 0; i < cacheSize; i++) {
                this.cache[i] = new Object();
            }
        }
    
        public Object valueOf(String key) {
            int index = key.hashCode();
            return cache[index & mask];
        }
    }
    
    0 讨论(0)
  • 2020-11-28 10:29
    private static final Set<String> lockedKeys = new HashSet<>();
    
    private void lock(String key) throws InterruptedException {
        synchronized (lockedKeys) {
            while (!lockedKeys.add(key)) {
                lockedKeys.wait();
            }
        }
    }
    
    private void unlock(String key) {
        synchronized (lockedKeys) {
            lockedKeys.remove(key);
            lockedKeys.notifyAll();
        }
    }
    
    public void doSynchronously(String key) throws InterruptedException {
        try {
            lock(key);
    
            //Do what you need with your key.
            //For different keys this part is executed in parallel.
            //For equal keys this part is executed synchronously.
    
        } finally {
            unlock(key);
        }
    }
    

    try-finally - is very important - you must guarantee to unlock waiting threads after your operation even if your operation threw exception.

    0 讨论(0)
  • 2020-11-28 10:34

    If the "record" you mention is a mutable object and "update" means that the object's internal state is modified without disturbing the structure of the container, then you can accomplish what you want just by locking the record object.

    If however "update" means removing the record object from the container and replacing it, then you must lock then entire container to prevent other threads from seeing it in an inconsistent state.

    In either case, you should be looking at the classes in the java.util.concurrent package.

    0 讨论(0)
  • 2020-11-28 10:35

    Guava has something like this being released in 13.0; you can get it out of HEAD if you like.

    Striped<Lock> more or less allocates a specific number of locks, and then assigns strings to locks based on their hash code. The API looks more or less like

    Striped<Lock> locks = Striped.lock(stripes);
    Lock l = locks.get(string);
    l.lock();
    try {
      // do stuff 
    } finally {
      l.unlock();
    }
    

    More or less, the controllable number of stripes lets you trade concurrency against memory usage, because allocating a full lock for each string key can get expensive; essentially, you only get lock contention when you get hash collisions, which are (predictably) rare.

    (Disclosure: I contribute to Guava.)

    0 讨论(0)
提交回复
热议问题