Простые блокировки на основе имени Java?
MySQL имеет удобную функцию:
SELECT GET_LOCK("SomeName")
Это можно использовать для создания простых, но очень специфичных блокировок на основе имен для приложения. Однако для этого требуется подключение к базе данных.
у меня есть много ситуаций, таких как:
someMethod() {
// do stuff to user A for their data for feature X
}
не имеет смысла просто синхронизировать этот метод, потому что, например, если этот метод вызывается для пользователя B в то же время, пользователю B не нужно ждать завершения пользователя A до его запуска, только операции для пользователю A и комбинации функций X нужно подождать.
С блокировкой MySql я мог бы сделать что-то вроде:
someMethod() {
executeQuery("SELECT GET_LOCK('userA-featureX')")
// only locked for user A for their data for feature X
executeQuery("SELECT RELEASE_LOCK('userA-featureX')")
}
поскольку блокировка Java основана на объектах, мне кажется, что мне нужно будет создать новый объект, чтобы представить ситуацию для этой блокировки, а затем поместить его в статический кэш где-нибудь, чтобы все потоки могли его видеть. Последующие запросы на блокировку для этой ситуации затем найдут объект блокировки в кэше и получат его блокировку. Я пытался создать что-то вроде этого, но тогда сам кэш блокировки нуждается в синхронизации. Кроме того, трудно определить, когда объект блокировки больше не используется, чтобы его можно было удалить из кэша.
Я посмотрел на параллельные пакеты Java, но ничто не выделяется как способное справиться с чем-то подобным. Существует ли простой способ реализовать это, или я смотрю на это с неправильной точки зрения?
Edit:
уточнить, я не ищу, чтобы создать предварительно определенный пул замков заранее, я хотел бы создать их по требованию. Некоторые псевдо-код для того, что я думаю:
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
}
22 ответа:
может быть, это полезно для вас: jkeylockmanager
Edit:
мой первоначальный ответ был, вероятно, немного короче. Я автор и несколько раз сталкивался с этой проблемой и не мог найти существующее решение. Вот почему я сделал эту небольшую библиотеку на GitHub.
все эти ответы, которые я вижу, слишком сложны. Почему бы просто не использовать:
public void executeInNamedLock(String lockName, Runnable runnable) { synchronized(lockName.intern()) { runnable.run(); } }
ключевым моментом является способ
intern
: это гарантирует, что возвращаемая строка является глобальным уникальным объектом, и поэтому ее можно использовать в качестве мьютекса на уровне экземпляра виртуальной машины. Все интернированные строки хранятся в глобальном пуле, так что это ваш статический кэш, о котором вы говорили в своем исходном вопросе. Не беспокойтесь о memleaks; эти строки будут gc'ed, если ни один другой поток не ссылается на него. Обратите внимание, однако, что до и в том числе Java6 этот пул хранится в пространстве PermGen вместо кучи, поэтому вам, возможно, придется его увеличить.есть проблема, хотя если какой-то другой код в вашей виртуальной машине блокируется на той же строке по совершенно другим причинам, но а) это очень маловероятно, и б) вы можете обойти его, введя пространства имен, например
executeInNamedLock(this.getClass().getName() + "_" + myLockName);
вы можете
Map<String, java.util.concurrent.Lock>
? Каждый раз, когда вам требуется блокировка, вы в основном звонитеmap.get(lockName).lock()
.вот пример использования Google Guava:
Map<String, Lock> lockMap = new MapMaker().makeComputingMap(new Function<String, Lock>() { @Override public Lock apply(String input) { return new ReentrantLock(); } });
затем
lockMap.get("anyOldString")
приведет к созданию новой блокировки при необходимости и вернулся к вам. Затем вы можете позвонитьlock()
на этот замок.makeComputingMap
возвращает потокобезопасную карту, поэтому вы можете просто поделиться ею со всеми своими потоками.
// 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();
для блокировки на что-то вроде имени пользователя, в памяти
Lock
s на карте может быть немного дырявым. В качестве альтернативы, вы можете посмотреть на использование WeakReferenceС WeakHashMap для создания объектов мьютекса, которые могут быть собраны в мусор, когда на них ничего не ссылается. Это позволяет избежать необходимости выполнять ручной подсчет ссылок для освобождения памяти.вы можете найти реализацию здесь. Обратите внимание, что если вы делаете частые поиски на карте вы можете столкнуться с проблемами конкуренции приобретает мьютекс.
может быть, немного позже, но вы можете использовать Google гуава полосатый
концептуально, чередование замков-это метод разделения замка на множество полос, повышающий детализацию одного замка и позволяющий независимым операциям блокировать разные полосы и действовать одновременно, вместо того, чтобы создавать конкуренцию для одного замка.
//init stripes=Striped.lazyWeakLock(size); //or stripes=Striped.lock(size); //... Lock lock=stripes.get(object);
универсальное решение с использованием java.утиль.одновременно
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(); } } }
на основе ответ Макдауэлл и IdMutexProvider, я написал общий класс
LockMap
использует WeakHashMap сохранить объект блокировки.LockMap.get()
может использоваться для получения объекта блокировки для ключа, который затем может использоваться с Javasynchronized (...)
заявление для применения блокировки. Неиспользуемые объекты блокировки автоматически освобождаются во время сборки мусора.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://stackoverflow.com/questions/5639870/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
пример использования карты блокировки класс:
private static LockMap<String> lockMap = new LockMap<String>(); synchronized (lockMap.get(name)) { ... }
простая тестовая программа для класса LockMap:
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"); }
если кто-нибудь знает лучший способ автоматического тестирования класса LockMap, пожалуйста, напишите комментарий.
Я хотел бы заметить, что
ConcurrentHashMap
имеет встроенное средство блокировки, которое достаточно для простой эксклюзивной многопоточной блокировки. Никаких дополнительныхLock
объекты, необходимые.вот пример такой карты блокировки, используемой для обеспечения не более одной активной обработки jms для одного клиента.
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(); } }
2 года спустя, но я искал простое решение с именем locker и наткнулся на это, было полезно, но мне нужен был более простой ответ, поэтому ниже того, что я придумал.
простая блокировка под некоторым именем и снова отпустите под тем же именем.
private void doTask(){ locker.acquireLock(name); try{ //do stuff locked under the name }finally{ locker.releaseLock(name); } }
вот код:
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(); } } }
может, что-то вроде этого:
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(); } }}
Я не проверял его, Хотя.
после некоторого разочарования, что нет поддержки уровня языка или простого класса Guava / Commons Для именованных блокировок,
вот к чему я пристроился:
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 } }
здесь я достиг: маленький шаблонный код без зависимости от библиотеки, атомарно приобретающий объект блокировки, не загрязняющий глобальные интернированные строковые объекты, без низкоуровневого хаоса уведомления/ожидания и без беспорядка try-catch-finally.
похоже на ответ от Lyomi, но использует более гибкий ReentrantLock вместо синхронизированного блока.
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(); } }
Да это будет расти с течением времени-но использование ReentrantLock открывает большие возможности для удаления блокировки с карты. Хотя удаление элементов с карты не кажется таким уж полезным, учитывая, что удаление значений с карты не уменьшит ее размер. Необходимо реализовать некоторую ручную логику калибровки карт.
рассмотрение памяти
часто бывает, что синхронизация, необходимая для конкретного ключа, недолговечна. Хранение вокруг выпущенных ключей может привести к чрезмерной трате памяти, что делает его нежизнеспособным.
вот реализация не внутренне держать вокруг выпущенных ключей.
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, и другие колокола и свистки
если требуется семантика блокировки повторного входа, они могут расширить вышеизложенное, обернув объект мьютекса в класс, который отслеживает фиксируя поток и запертый отсчет.
например:
private static class Lock { final CountDownLatch mutex = new CountDownLatch(1); final long threadId = Thread.currentThread().getId(); int lockedCount = 1; }
если кто-то хочет
lock()
чтобы вернуть объект, чтобы сделать релизы проще и безопаснее, это также возможность.собирая все это вместе, вот как может выглядеть класс:
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(); } }
и вот как это можно использовать:
try (KeyedLock lock = locks.acquire("SomeName")) { // do something critical here }
в ответ на предложение использовать новый MapMaker().makeComputingMap ()...
MapMaker ().makeComputingMap () является устаревшим по соображениям безопасности. Преемником является CacheBuilder. Со слабыми ключами / значениями, примененными к CacheBuilder, мы очень близки к решению.
проблема заключается в заметке в CacheBuilder.weakKeys():
when this method is used, the resulting cache will use identity (==) comparison to determine equality of keys.
это делает невозможным выбор существующей блокировки по строковому значению. Эрг.
(4 года спустя...) Мой ответ похож на user2878608, но я думаю, что в этой логике есть некоторые недостающие крайние случаи. Я также думал, что семафор предназначен для блокировки нескольких ресурсов сразу (хотя я полагаю, что использовать его для подсчета шкафчиков, как это тоже хорошо), поэтому вместо этого я использовал общий объект блокировки POJO. Я провел один тест на нем, который продемонстрировал, что каждый из крайних случаев существовал IMO и будет использовать его в моем проекте на работе. Надеюсь, это кому-то поможет. :)
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; } } } } }
Я создал tokenProvider на основе IdMutexProvider из Макдауэлл. Менеджер использует
WeakHashMap
который заботится об очистке неиспользуемых замков.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<String> myTokenProvider = new TokenMutexProvider<String>(); * * 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(); } } }
использование:
private final TokenMutexManager<String> myTokenManager = new TokenMutexManager<String>(); Mutex mutex = myTokenManager.getMutex("UUID_123456"); synchronized(mutex) { // your code here }
или лучше использовать целые числа?
private final TokenMutexManager<Integer> myTokenManager = new TokenMutexManager<Integer>(); Mutex mutex = myTokenManager.getMutex(123456); synchronized(mutex) { // your code here }
этот поток старый, но возможным решением является фреймворк 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(); }
вот простое и оптимизированное решение, которое также касается удаления используемых блокировок, но с накладными расходами на синхронизацию карты:
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(); }
}
многие реализации, но не похожие на мои.
называется моя реализация динамической блокировки как
ProcessDynamicKeyLock
потому что это один процесс блокировки, для любого объекта в качестве ключа (равно+хэш-код для уникальности).TODO: добавьте способ обеспечить фактическую блокировку, например,
ReentrantReadWriteLock
вместоReentrantLock
.реализация:
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(); } }
простой тест:
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(); } } }
ваша идея об общем статическом репозитории объектов блокировки для каждой ситуации верна.
Вам не нужно, чтобы сам Кэш был синхронизирован ... это может быть так же просто, как хэш-карта.потоки могут одновременно получить объект блокировки с карты. Фактическая логика синхронизации должна быть инкапсулирована внутри каждого такого объекта отдельно (см. java.утиль.параллельный пакет для этого - http://download.oracle.com/javase/6/docs/api/java/util/concurrent/locks/package-summary.html)
TreeMap потому что в HashMap размер внутреннего массива может только увеличиваться
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; } }