ConcurrentSkipListMap如何使删除和添加调用原子

时间:2017-09-19 19:08:19

标签: java multithreading java-8 java.util.concurrent

我有N个线程添加值和一个删除线程。我正在考虑如何同步添加到现有值列表和删除列表的最佳方法。

我想以下案例是可能的:

 thread 1 checked condition containsKey, and entered in else block
 thread 2 removed the value
 thread 1 try to add value to existing list, and get returns null

我认为我可以使用的唯一方法是通过地图值进行同步,在我们的情况下是添加时和删除时的列表

    private ConcurrentSkipListMap<LocalDateTime, List<Task>> tasks = new ConcurrentSkipListMap<>();

    //Thread1,3...N
    public void add(LocalDateTime time, Task task) {
        if (!tasks.containsKey(time)) {
            tasks.computeIfAbsent(time, k -> createValue(task));
        } else {
             //potentially should be synced
            tasks.get(time).add(task);
        }
    }
    private List<Task> createValue(Task val) {
        return new ArrayList<>(Arrays.asList(val));
    }

    //thread 2
   public void remove()
    while(true){
        Map.Entry<LocalDateTime, List<Task>> keyVal = tasks.firstEntry();
        if (isSomeCondition(keyVal)) {
            tasks.remove(keyVal.getKey());
            for (Task t : keyVal.getValue()) {
                //do task processing
            }
        }
    }
   }

2 个答案:

答案 0 :(得分:3)

关于 真正倾向于使用add的{​​{1}}部分,但文档非常明确 - 说它不能保证以原子方式发生。

我会将merge替换为add,而将替换为

merge

同样适用于SomeLock lock ... public void add(LocalDateTime time, Task task) { lock.lock(); tasks.merge... lock.unlock(); } 方法。但是,如果你在锁定下做事,首先就不需要remove

另一方面,如果你可以改为ConcurrentSkipListMap - 它有ConcurrentHashMap,例如原子。

答案 1 :(得分:1)

您的remove()方法应该做什么并不完全清楚。在它的当前形式中,它是一个无限循环,首先,它将遍历头部元素并移除它们,直到头部元素不满足条件,然后,它将重复轮询该头部元素并重新评估条件。除非,它设法删除所有元素,在这种情况下,它将以一个例外拯救。

如果要处理当前在地图中的所有元素,您可以简单地循环它,弱一致的迭代器允许您在修改它时继续;您可能会注意到正在进行的并发更新。

如果只想处理匹配的头元素,则必须向其中插入条件,返回调用者或​​将线程置于休眠状态(或更好地添加通知机制),以避免重复烧毁CPU测试失败(甚至在地图为空时抛出)。

除此之外,当您确保功能之间没有干扰时,您可以使用ConcurrentSkipListMap实施操作。假设remove应该处理所有当前元素一次,实现可能看起来像

public void add(LocalDateTime time, Task task) {
    tasks.merge(time, Collections.singletonList(task),
        (l1,l2) -> Stream.concat(l1.stream(),l2.stream()).collect(Collectors.toList()));
}

public void remove() {
    for(Map.Entry<LocalDateTime, List<Task>> keyVal : tasks.entrySet()) {
        final List<Task> values = keyVal.getValue();
        if(isSomeCondition(keyVal) && tasks.remove(keyVal.getKey(), values)) {
            for (Task t : values) {
                //do task processing
            }
        }
    }
}

关键是地图中包含的列表永远不会被修改。如果没有先前的映射,merge(time, Collections.singletonList(task), …操作甚至会存储单个任务的不可变列表。如果存在先前的任务,则合并函数(l1,l2) -> Stream.concat(l1.stream(),l2.stream()).collect(Collectors.toList())将创建新列表而不是修改现有列表。当列表变得更大时,这可能会对性能产生影响,尤其是在争用情况下必须重复操作时,但这是不需要锁定或额外同步的代价。

remove操作使用remove(key, value)方法,只有在地图的值仍与预期值匹配时才会成功。这取决于以下事实:我们的方法都没有修改地图中包含的列表,但在合并时用新的列表实例替换它们。如果remove(key, value)成功,则可以处理列表;此时,它不再包含在地图中。请注意,在评估isSomeCondition(keyVal)期间,列表仍包含在地图中,因此,isSomeCondition(keyVal) 不得修改它,但我认为应该是这种情况无论如何,对于像isSomeCondition这样的测试方法。当然,在isSomeCondition中评估列表也依赖于从不修改列表的其他方法。