您无需尝试将大小限制为任意值-事实证明,您可以完成这种“锁定处理程序”惯用语,而仅存储当前在映射中锁定的键的 确切 数目。
这个想法是使用一个简单的约定:成功地 将 映射 添加 到映射计数为“锁定”操作,而将其删除则作为“解锁”操作。巧妙地避免了在某些线程仍处于锁定状态和其他竞争条件的情况下删除映射的问题。
此时,value
映射中的in仅用于阻止使用相同密钥到达的其他线程,并且需要等待直到删除映射为止。
这是一个示例1,其中包含CountDownLatch
而不是Lock
作为地图值:
public void handle(Key key) throws InterruptedException {
CountDownLatch latch = new CountDownLatch(1);
// try to acquire the lock by inserting our latch as a
// mapping for key
while(true) {
CountDownLatch existing = lockMap.putIfAbsent(key, latch);
if (existing != null) {
// there is an existing key, wait on it
existing.await();
} else {
break;
}
}
try {
externalSystem.process(key);
} finally {
lockMap.remove(key);
latch.countDown();
}
}
在此,映射的生存期只有保持锁定的时间。映射将永远不会有比同时请求不同密钥更多的条目。
与您的方法的区别在于,不会“重用”映射- 每个handle
调用都会创建一个新的闩锁和映射。由于您已经在进行昂贵的原子操作,因此这实际上不太可能会变慢。另一个缺点是,在有许多等待线程的情况下,当闩锁递减计数时,所有 线程 都会 被唤醒,但是只有一个线程能够成功放入新的映射并因此获得锁-其余的则重新进入新锁的睡眠状态。
您 可以 构建此版本的另一个版本,该版本在线程进入并等待现有映射时重新使用映射。基本上,解锁线程只是对等待线程之一进行“切换”。只有一个映射将用于等待相同键的整个线程集- 它按顺序移交给每个线程。该大小仍受限制,因为没有更多线程在等待给定的映射,因此仍将其删除。
要实现这一点,您CountDownLatch
可以用一个可以计算等待线程数的映射值代替。当线程进行解锁时,它首先检查是否有线程在等待,如果有,则唤醒它进行切换。如果没有线程在等待,它将“销毁”对象(即设置一个标志,表明该对象不再在映射中)并将其从映射中删除。
您需要在适当的锁定下进行上述操作,并且有一些棘手的细节。在实践中,我发现上面简短而甜美的示例非常有用。
1即时编写,未经编译且未经测试,但该想法有效。