下面的代码段从并行流的itemsById
块更新了一个非线程安全的映射(forEach
不是线程安全的)。
// Update stuff in `itemsById` by iterating over all stuff in newItemsById:
newItemsById.entrySet()
.parallelStream()
.unordered()
.filter(...)
.forEach(entry -> {
itemsById.put(entry.getKey(), entry.getValue()); <-- look
});
对我来说,这看起来不是线程安全的,因为并行流将同时在多个线程中调用forEach
块,因此在同一个线程中调用itemsById.put(..)
时间,itemsById
不是线程安全的。 (但是,对于ConcurrentMap,我认为代码是安全的)
我写信给同事:“请注意,当您插入新数据时,地图可能会分配新的内存。这可能不是线程安全的,因为集合不是线程安全的。 - 是否写入不同的来自许多线程的密钥,是线程安全的,依赖于实现,我想。我不会选择依赖它。“
然而,他说上面的代码是线程安全的。 - 是吗?((请注意:我认为这个问题不太局部化。实际上现在使用Java 8我认为很多人会做类似的事情:parallelStream()...foreach(...)
然后它可能对线程安全问题有所了解,对很多人而言))
答案 0 :(得分:3)
你是对的:这段代码不是线程安全的,取决于Map
实现和竞争条件可能会产生任何随机效果:正确的结果,数据的静默丢失,一些异常或无限循环。您可以像这样轻松检查:
int equal = 0;
for(int i=0; i<100; i++) {
// create test input map like {0 -> 0, 1 -> 1, 2 -> 2, ...}
Map<Integer, Integer> input = IntStream.range(0, 200).boxed()
.collect(Collectors.toMap(x -> x, x -> x));
Map<Integer, Integer> result = new HashMap<>();
// write it into another HashMap in parallel way without key collisions
input.entrySet().parallelStream().unordered()
.forEach(entry -> result.put(entry.getKey(), entry.getValue()));
if(result.equals(input)) equal++;
}
System.out.println(equal);
在我的机器上,此代码通常打印20到40而不是100的内容。如果我将HashMap
更改为TreeMap
,它通常会因NullPointerException
失败或陷入无限循环内部TreeMap
实施。
答案 1 :(得分:1)
我不是关于流的专家,但我认为这里没有花哨的同步,因此我不会考虑将itemsById
并行添加到线程安全中。
可能发生的事情之一将是无限循环,因为如果两个元素碰巧最终都在同一个桶中,则底层列表可能会混乱并且元素可能在一个循环中相互引用(A.next = B,B.next = A)。 ConcurrentHashMap
可以通过同步存储桶上的写访问来防止这种情况,即除非元素最终位于同一存储桶中,否则它不会阻塞,但如果它们执行,则添加是顺序的。
答案 2 :(得分:0)
此代码不是线程安全的。
forEach和peek等操作是针对副作用而设计的;一个 返回void的lambda表达式,例如调用的表达式 System.out.println,只能有副作用。 即便如此,你 应谨慎使用forEach和peek操作;如果你使用一个 这些操作使用并行流,然后Java运行时可以 调用您指定为其参数的lambda表达式 同时来自多个线程 。