在id上同步删除



我需要确保多个线程不会同时试图访问相同的资源。我有一堆这样的资源,所以我想为每个资源有一个单独的锁对象(而不是一个全局锁),这样线程就不会不必要地阻塞彼此。

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将无限增长。是否有一种方法可以在线程完成时删除哈希键而不会搞砸并发性?

如果您知道某个线程是最后一个请求特定锁的线程,那么您可以只使用locks.remove(id, id)。否则,你需要对两个事件进行原子更新,而这两个事件不容易同步在一起。

如果你释放了一个锁,然后删除了一个锁,另一个线程可能会获取你释放的锁,而另一个线程会使一个新的锁对象对原来的锁对象无关。在这种情况下,可能会出现两个线程同时调用getFromDataBase(id)cache.store(p)的情况。如果您先删除然后释放,则可能会有另一个线程等待您释放旧锁,而新线程已经创建了新锁。同样的碰撞也可能发生。

本质上,你不可能在不向系统添加新锁的情况下自动释放锁并从HashMap中删除它。在这种情况下,您要么最终使用全局锁(这会破坏特定于散列的锁的加速),要么需要为每个页面添加一个额外的锁,这本身就会有相同的删除问题。或者,如果你可以访问HashMap的内部,你可以尝试一些棘手的桶锁暴露给你的高层逻辑,尽管我不建议尝试实现这样的事情。

限制hashmap大小的一个解决方案是使用固定大小的映射。将Integer id修改为较大的数字(10,000),并使用修改后的数字作为唯一的锁id。两个哈希冲突并要求对不同页面使用相同锁的概率非常小,而且锁所消耗的内存有一个硬上限。在这种情况下,您实际上不需要HashMap,因为您可以将Integer锁定对象预先分配到静态const数组中,并直接从id对象请求mod哈希值。

在锁外读取缓存也要小心,除非缓存本身被保护不允许同时读写,因为一个线程可能正在写,而另一个线程可能按照问题中指定的方式读取代码。

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

最新更新