Java:获取+清除地图原子

时间:2019-01-08 12:27:37

标签: java collections concurrency

我想实现以下逻辑:

-使用以下结构

//Map<String, CopyOnWriteArrayList> keeping the pending updates 
//grouped by the id of the updated object
final Map<String, List<Update>> updatesPerId = new ConcurrentHashMap<>();

-n生产者将向updatesPerId映射添加更新(对于相同的ID,可以同时添加2个更新)

-一个TimerThread会不时运行,并且必须处理收到的更新。像这样:

 final Map<String, List<Update>> toBeProcessed = new HashMap<>(updatesPerId);
 updatesPerId.clear();
 // iterate over toBeProcessed and process them

是否有任何方法可以使此逻辑线程安全,而无需同步生产者的添加逻辑和timerThread(consumer)的逻辑?我正在考虑一个原子性的clear + get,但似乎ConcurrentMap没有提供类似的东西。 另外,我不得不提到,更新应该由更新的对象ID保留,因此我不能用队列或其他内容替换地图。

有什么想法吗? 谢谢!

3 个答案:

答案 0 :(得分:4)

您可以利用ConcurrentHashMap.compute executes atomically这个事实。

您可以像这样放入updatesPerId

updatesPerId.compute(k, (k, list) -> {
  if (list == null) list = new ArrayList<>();
  // ... add to the list

  // Return a non-null list, so the key/value pair is stored in the map.
  return list;
});

这不是不是,先使用computeIfAbsent,然后将其添加到返回值中,该返回值不是原子的。

然后在您的线程中删除内容:

for (String key : updatesPerId.keySet()) {
  List<Update> list = updatesPerId.put(key, null);
  updatesPerId.compute(key, (k, list) -> {
    // ... Process the contents of the list.

    // Removes the key/value pair from the map.
    return null;
  });
}

因此,如果您碰巧尝试一次在两个地方都处理该密钥,则将密钥添加到列表(或处理该密钥的所有值)可能会阻塞;否则,它将不会被阻止。


编辑:如@StuartMarks所指出,最好先将所有内容从地图中取出,然后再进行处理,以避免阻塞试图添加的其他线程:

Map<String, List<Update>> newMap = new HashMap<>();
for (String key : updatesPerId.keySet()) {
  newMap.put(key, updatesPerId.remove(key));
}
// ... Process entries in newMap.

答案 1 :(得分:2)

我建议使用LinkedBlockingQueue而不是CopyOnWriteArrayList作为地图值。使用COWAL,添加会变得越来越昂贵,因此添加N个元素会导致N ^ 2性能。 LBQ加法为O(1)。此外,LBQ具有drainTo,可在此处有效使用。您可以这样做:

final Map<String, Queue<Update>> updatesPerId = new ConcurrentHashMap<>();

制作人:

updatesPerId.computeIfAbsent(id, LinkedBlockingQueue::new).add(update);

消费者:

updatesPerId.forEach((id, queue) -> {
    List<Update> updates = new ArrayList<>();
    queue.drainTo(updates);
    processUpdates(id, updates);
});

这与您的建议有些不同。该技术处理每个id的更新,但允许生产者在此过程中继续向地图添加更新。这样就为每个ID在地图中留下了地图条目和队列。如果这些ID最终被大量重复使用,则地图条目的数量将达到高水位。

如果新的ID不断出现,而旧的ID被废弃,则地图将持续增长,这可能不是您想要的。如果是这种情况,您可以使用Andy Turner's answer中的技术。

如果使用者确实需要快照并清除整个地图,我认为您必须使用锁定,这是您想要避免的。

答案 2 :(得分:1)

  

是否有任何方法可以使此逻辑线程安全,而无需同步生产者的添加逻辑和timerThread(consumer)的逻辑?

简而言之,不是-取决于您所说的“同步”。

最简单的方法是将Map包装到您自己的类中。

class UpdateManager {
    Map<String,List<Update>> updates = new HashMap<>();
    public void add(Update update) {
        synchronized (updates) {
            updates.computeIfAbsent(update.getKey(), k -> new ArrayList<>()).add(update);
        }
    }
    public Map<String,List<Update>> getUpdatesAndClear() {
        synchronized (updates) {
            Map<String,List<Update>> copy = new HashMap<>(updates);
            updates.clear();
            return copy;
        }
    }
}