我想实现以下逻辑:
-使用以下结构
//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保留,因此我不能用队列或其他内容替换地图。
有什么想法吗? 谢谢!
答案 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;
}
}
}