在我们的系统中,我们有一个方法,当用某个ID调用它时会做一些工作:
public void doWork(long id) { /* ... */ }
现在,可以针对不同的ID同时完成这项工作,但是如果使用2个线程使用相同的ID调用该方法,则一个线程应该阻塞直到它完成。
最简单的解决方案是将Map从Long ID映射到我们可以锁定的任意对象。我预见到的一个问题是,我们可以在系统中拥有大量的ID,而且这张地图每天都会不断增长。
理想情况下,我认为我们需要一个系统,我们每个线程都会获取一个锁定对象,在可能的情况下锁定,完成工作,然后发出信号表明我们已完成锁定。如果很明显没有其他人使用这个特定的锁,那么请从锁定图中安全地将其删除以防止内存泄漏。
我想这一定是一个非常常见的场景,所以我希望有一个现有的解决方案。有人知道吗?
答案 0 :(得分:15)
我不久前为自己发明了类似的东西。我将其称为等价类锁,意思是,它锁定了与给定事物相等的所有事物。您可以获取它from my github,并根据Apache 2许可证使用它,如果您愿意,或者只是阅读并忘记它!
答案 1 :(得分:8)
您可以尝试使用ReentrantLock,以便拥有Map<Long,Lock>
。现在在lock.release()之后你可以测试lock.hasQueuedThreads()。如果返回false,则可以从Map中删除它。
答案 2 :(得分:6)
你可以尝试下面的小'黑客'
String str = UNIQUE_METHOD_PREFIX + Long.toString(id);
synchornized(str.intern()) { .. }
100%保证返回相同的实例。
UNIQUE_METHOD_PREFIX
可以是硬编码常量,也可以使用:
StackTraceElement ste = Thread.currentThread().getStackTrace()[0];
String uniquePrefix = ste.getDeclaringClass() + ":" +ste.getMethodName();
这将保证锁定只发生在这种精确的方法上。这是为了避免死锁。
答案 3 :(得分:4)
我会说你已经有了解决方案。让懒惰的LockManager
和引用计数管理这些锁。然后在doWork
:
public void doWork(long id) {
LockObject lock = lockManager.GetMonitor(id);
try {
synchronized(lock) {
// ...
}
} finally {
lock.Release();
}
}
答案 4 :(得分:4)
首先:
你在这里谈论锁定条带设置。连续体的一端是所有ID的单个巨型锁,这将是简单且安全的,但不是并发的。另一端是每个id的锁定,这很容易(在某种程度上)并且安全且非常并发,但可能需要在内存中有大量“可锁定对象”(如果您还没有它们)。在中间的某个地方是为一系列ID创建锁的想法 - 这使您可以根据您的环境调整并发性,并选择内存和并发之间的权衡。
ConcurrentHashMap可用于实现此目的,因为CHM由段(子映射)内部组成,每个段有一个锁。这使您的并发性等于段的数量(默认为16但可配置)。
还有许多其他可能的解决方案可用于对ID空间进行分区并创建锁定集,但您对清理和内存泄漏问题敏感是正确的 - 在保持并发性的同时处理这一问题是一件棘手的事情。您需要对每个锁使用某种引用计数,并仔细管理旧锁的驱逐,以避免驱逐锁定过程中的锁。如果你走这条路线,使用ReentrantLock或ReentrantReadWriteLock(而不是在对象上同步),因为这可以让你明确地将锁作为一个对象进行管理,并使用它上面的额外方法。
在Java Concurrency in Practice第11.4.3节中还有一些内容和StripedMap示例。
答案 5 :(得分:0)
使用java.util.concurrent包中的SynchronizedHashMap或Collections.synchronizedMap(Map m)而不是使用不同步的检索和插入调用的普通HashMap是不够的?
类似的东西:
Map<Long,Object> myMap = new HashMap<Long,Object>();
Map<Long,Object> mySyncedMap=Collections.synchronizedMap(myMap);
答案 6 :(得分:0)
过早优化是邪恶的根源
使用(同步)地图尝试。
如果它变得太大,你可以定期清除它的内容。
答案 7 :(得分:0)
您可以创建一个列表或一组活动ID,并使用wait和notify:
List<Long> working;
public void doWork(long id) {
synchronized(working)
{
while(working.contains(id))
{
working.wait();
}
working.add(id)//lock
}
//do something
synchronized(working)
{
working.remove(id);//unlock
working.notifyAll();
}
}
问题解决了:
那里的问题:
答案 8 :(得分:0)
这是我使用规范化地图的地方,它会获取您的long
输入并返回一个规范的Long
对象,然后您可以使用该对象进行同步。我写过关于规范化地图here的文章;只需将String
替换为Long
(为了让您的生活更轻松,请让long
作为参数)。
一旦你有了规范化的地图,你就会写下这样的锁定守护代码:
Long lockObject = canonMap.get(id);
synchronized (lockObject)
{
// stuff
}
规范化地图将确保为相同的ID返回相同的lockObject
。当没有对lockObject
的活动引用时,它们将有资格进行垃圾回收,因此您不会用不必要的对象填充内存。
答案 9 :(得分:0)
我可能会迟到游戏,但是此解决方案不会泄漏任何内存,并且您不必记住要进行任何锁定释放:
Synchronizer<AccountId> synchronizer = new Synchronizer();
...
// first thread - acquires "lock" for accountId accAAA
synchronizer.synchronizeOn(accountId("accAAA"), () -> {
long balance = loadBalance("accAAA")
if (balance > 10_000) {
decrementBalance("accAAA", 10_000)
}
})
...
// second thread - is blocked while first thread runs (as it uses the same "lock" for accountId accAAA)
synchronizer.synchronizeOn(accountId("accAAA"), () -> {
long balance = loadBalance("accAAA")
if (balance > 2_000) {
decrementBalance("accAAA", 2_000)
}
})
...
// third thread - won't be blocked by previous threads (as it is for a different accountId)
synchronizer.synchronizeOn(accountId("accXYZ"), () -> {
long balance = loadBalance("accXYZ")
if (balance > 3_500) {
decrementBalance("accXYZ", 3_500)
}
})
要使用它,您只需添加一个依赖项:
compile 'com.github.matejtymes:javafixes:1.3.0'
答案 10 :(得分:-2)
我建议您使用java.util.concurrent中的实用程序,尤其是AtomicLong类。见related javadoc