我需要确保多个线程不会同时尝试访问同一个资源。我有一堆这些资源,所以我想为每个资源(而不是一个全局锁)设置一个单独的锁对象,这样线程就不会不必要地阻塞它们。
Eddie使用https://stackoverflow.com/a/659939/82156中的ConcurrentMap.putIfAbsent()
为此提供了一个很好的解决方案。
// From https://stackoverflow.com/a/659939/82156
public Page getPage(Integer id) {
Page p = cache.get(id);
if (p == null) {
synchronized (getCacheSyncObject(id)) {
p = getFromDataBase(id);
cache.store(p);
}
}
}
private ConcurrentMap<Integer, Integer> locks = new ConcurrentHashMap<Integer, Integer>();
private Object getCacheSyncObject(final Integer id) {
locks.putIfAbsent(id, id);
return locks.get(id);
}
然而,该实现的一个问题是hashmap将无限增长。有没有办法在线程完成时删除哈希键而不会搞砸并发性?
答案 0 :(得分:1)
如果您知道特定线程是请求特定锁定的最后一个线程,那么您可以使用locks.remove(id, id)
。否则,您需要对两个无法轻松同步的事件进行原子更新。
如果你释放然后删除一个锁,另一个线程可以获取你释放的锁,而另一个线程使一个新的锁对象无视原始的锁对象。在这种情况下,最终可能会有两个线程同时调用getFromDataBase(id)
或cache.store(p)
。如果您删除然后释放,则可能有另一个线程等待您释放旧锁,而新线程已经创建了新锁。可能会发生同样的碰撞。
基本上,如果不向系统添加新锁,则无法以原子方式释放锁并将其从HashMap中删除。在这种情况下,您最终会得到一个全局锁定 - 这会阻止特定于哈希的锁定的加速 - 或者您需要为每个页面提供额外的锁定,这些锁定本身会有相同的删除问题。或者,如果你可以访问HashMap的内部,你可以尝试一些棘手的桶锁定暴露你的更高级别的逻辑,但我不建议尝试实现这样的事情。
限制散列映射大小的一种解决方案是使用固定大小的映射。使用大数(10,000)修改您的Integer id,并使用修改后的数字作为唯一锁定ID。两个哈希冲突并且对不同页面需要相同锁定的可能性非常小,并且锁定所占用的内存上有一个硬帽。在这种情况下,您实际上不需要HashMap,因为您可以将Integer锁定对象预先分配到静态const数组中,并直接从id对象请求mod哈希。
另外,你应该小心在锁之外进行缓存读取,除非缓存本身受到保护而不能同时读写,因为线程可能正在写入,而另一个读取的方式是在问题中指定代码的方式。
答案 1 :(得分:1)
Pyrce让我意识到并不是每个资源都有自己的锁定对象。相反,我可以使用可以在资源之间共享的100个锁对象池。这样,锁定对象的集合不会无限制地增长,但我仍然可以通过删除单个全局锁来获得我希望获得的大部分并发优势。
这意味着我不再需要使用ConcurrentHashMap
而只需使用一个急切初始化的简单数组。
// getPage() remains unchanged
public Page getPage(Integer id) {
Page p = cache.get(id);
if (p == null) {
synchronized (getCacheSyncObject(id)) {
p = getFromDataBase(id);
cache.store(p);
}
}
}
private int MAX_LOCKS = 100;
private Object[] locks = new Object[MAX_LOCKS];
{
for(int i=0; i<locks.length; ++i)
locks[i] = new Object();
}
private Object getCacheSyncObject(final Integer id) {
return locks[ id % MAX_LOCKS ];
}