37

防止在不锁定整个集合的情况下同时更新键值集中的一条记录的最佳方法是什么?从语义上讲,我正在寻找某种按键锁定(理想情况下,Java 实现,但不一定):

interface LockByKey {
   void lock(String key); // acquire an exclusive lock for a key   
   void unlock(String key); // release lock for a key
}

此锁旨在同步对远程存储的访问,因此某些同步的 Java 集合不是一个选项。

4

6 回答 6

50

Guava 在 13.0 中发布了类似的东西;如果你愿意,你可以把它从 HEAD 中取出。

Striped<Lock>或多或少分配特定数量的锁,然后根据其哈希码将字符串分配给锁。API看起来或多或少像

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

或多或少,可控制的条带数量让您可以用内存使用来交换并发性,因为为每个字符串键分配一个完整的锁可能会很昂贵;本质上,只有在遇到哈希冲突时才会出现锁争用,而哈希冲突(可以预见)很少见。

(披露:我为 Guava 做出了贡献。)

于 2012-06-20T18:11:48.620 回答
5
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 - 非常重要 - 即使您的操作抛出异常,您也必须保证在操作后解锁等待线程。

于 2019-02-12T19:02:50.753 回答
4

我编写了一个可以动态锁定任何键的类。它使用静态CuncurrentHashMap. 但如果不使用锁,则地图为空。作为我们基于键创建的新对象,语法可能会令人困惑。如果不使用,它会清理上的锁unlock。保证DynamicKeyLock基于两个相等/hascode 键创建的任何两个,它们将被相互锁定。

请参阅 Java 8、Java 6 的实现和一个小测试。

爪哇 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();
    }
}

爪哇 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();
    }
}

测试:

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();
        }
    }
}
于 2018-05-27T07:30:29.147 回答
0

如果你提到的“记录”是一个可变对象,而“更新”是指在不干扰容器结构的情况下修改了对象的内部状态,那么你只需锁定记录对象即可完成你想要的。

但是,如果“更新”意味着从容器中删除记录对象并替换它,那么您必须锁定整个容器以防止其他线程看到它处于不一致状态。

无论哪种情况,您都应该查看java.util.concurrent包中的类。

于 2012-06-20T17:21:44.840 回答
-1

每个桶保持一个互斥锁/锁。这将确保只有冲突在该互斥体上等待。

于 2012-06-20T17:04:21.460 回答
-1

就是这样;我做到了。是的,我同意如果两个不同的字符串共享相同的哈希码最终会获得相同的锁。

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];
    }
}
于 2016-03-17T08:17:44.800 回答