Cómo adquirir un candado con una llave

¿Cuál es la mejor manera de evitar la actualización simultánea de un registro en un conjunto clave-valor sin bloquear todo el conjunto? Semánticamente, estoy buscando algún tipo de locking por una clave (idealmente, la implementación de Java, pero no necesariamente):

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

Este locking está destinado a sincronizar un acceso a una tienda remota, por lo que algunas colecciones sincronizadas de Java no son una opción.

Guava tiene algo así como esto lanzado en 13.0; puedes sacarlo de HEAD si quieres.

Striped asigna más o menos un número específico de lockings, y luego asigna cadenas a lockings basados ​​en su código hash. La API se parece más o menos a

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

Más o menos, el número controlable de rayas le permite intercambiar concurrencia con el uso de la memoria, porque asignar un locking completo para cada clave de cadena puede ser costoso; esencialmente, solo obtienes contienda de locking cuando obtienes colisiones hash, que son (previsiblemente) raras.

(Divulgación: Contribuyo a la guayaba).

Así es como; lo hice. Y sí, estoy de acuerdo en que si dos cadenas diferentes comparten el mismo código hash terminarán adquiriendo el mismo locking.

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

Mantenga un mutex / lock por cubo. Esto asegurará que solo las colisiones esperen en ese mutex.

Si el “registro” que menciona es un objeto mutable y “actualización” significa que el estado interno del objeto se modifica sin alterar la estructura del contenedor, entonces usted puede lograr lo que desea simplemente bloqueando el objeto de registro.

Sin embargo, si “actualizar” significa eliminar el objeto de registro del contenedor y reemplazarlo, entonces debe bloquear todo el contenedor para evitar que otros subprocesos lo vean en un estado incoherente.

En cualquier caso, debería mirar las clases en el paquete java.util.concurrent .

He escrito una clase que puede bloquear cualquier tecla de forma dinámica. Utiliza un CuncurrentHashMap estático. Pero si no se usa ningún locking, el mapa está vacío. La syntax puede ser confusa como un nuevo objeto que nosotros creamos basado en la clave. Limpia la cerradura, si no se usa, al unlock . Existe una garantía de que dos DynamicKeyLock creados con base en dos claves equal / hascode serán bloqueados mutuamente.

Ver implementación para Java 8, Java 6 y una pequeña prueba.

Java 8:

 public class DynamicKeyLock implements Lock { private final static ConcurrentHashMap 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 implements Lock { private final static ConcurrentHashMap 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() { 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(); } } 

Prueba:

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