我正在尝试演示使用具有多个并发任务的普通Map
的问题。以下示例(编译和运行)旨在显示Map
失败:
import java.util.*;
import java.util.stream.*;
import java.util.concurrent.*;
class BreakMap2 implements Runnable {
private Map<Integer, Integer> map;
public BreakMap2(Map<Integer, Integer> map) {
this.map = map;
}
@Override
public void run() {
while(true) {
int key = ThreadLocalRandom.current().nextInt(10_000);
if(map.containsKey(key)) {
assert map.get(key) == key;
}
map.put(key, key);
}
}
}
public class MapBreaker2 {
public static void main(String[] args) {
Map<Integer, Integer> map = new HashMap<>();
IntStream.range(0, 1000)
.mapToObj(i -> new BreakMap2(map))
.map(CompletableFuture::runAsync)
.collect(Collectors.toList())
.forEach(CompletableFuture::join);
}
}
这不能证明问题(它不会失败)。我怎样才能更有效地做到这一点?是否存在快速可靠的失败方法?
为了澄清,我试图说明如何将多个任务写入Map
并非设计用于并发使用是不安全的。我正在尝试创建一些因为并发访问而向Map
显示错误写入的内容。
编辑:我已经简化了示例,所以它现在只运行直到你点击Control-C。我想要的是没有停止该计划。
答案 0 :(得分:1)
目前尚不清楚你试图挑起哪种“不正确的写作”。
数据结构的并发更新可能会出现各种各样的问题。 HashMap
的非同步并发更新的一个臭名昭着的问题,确实出现在实际应用程序中,HashMap.get
被卡在无限循环中,当然,无论如何无论如何运行的程序都无法实现发现它。
您唯一要测试的是存储的Integer
与assert map.get(key) == key;
的值。这不会测试对象标识(否则由于未指定的自动对象标识而注定要失败)盒装值),而是包含int
值,它可以从final
字段的保证中受益,即使它们的所有者对象是通过数据竞争发布的。因此,您无法在此处看到未初始化的值,并且很难想象任何可能遇到错误值的情况。
由于您存储的值不受数据争用影响的不可变对象,因此只有Map
本身的结构更新才会生效。但这些很少见。您使用Map
和0
之间的随机密钥在无限循环中填充10000
,因此一旦遇到该范围内的所有一万个不同的密钥,就不会再发生结构变化。机会很好,即使在下一个任务开始工作之前,第一个异步任务也会达到该状态。即使存在短暂的重叠阶段,在该时间窗口中遇到数据竞争的可能性也很低。
在那个短时间窗口之后,你只是替换现有映射的值,如上所述,对象不受数据竞争影响,并且由于它们代表相同的盒装值,JVM甚至可以优化整个更新。
如果您想要一个失败可能性较高的程序,可以尝试以下操作。它执行从一个Map
到另一个public class MapBreaker2 {
public static void main(String[] args) throws InterruptedException {
int threadCount = 2; // try varying that number
Map<Integer, Integer> source = IntStream.range(0, 10_000)
.boxed().collect(Collectors.toMap(i->i, i->i));
System.out.println("trying to copy "+source.size()+" mappings without synchonizing");
Map<Integer, Integer> target = new HashMap<>();
Callable<?> job=() -> {
while(!source.isEmpty()) {
int key = ThreadLocalRandom.current().nextInt(10_000);
Integer value=source.remove(key);
if(value!=null)
target.put(key, value);
}
return null;
};
ExecutorService pool = Executors.newCachedThreadPool();
pool.invokeAll(Collections.nCopies(threadCount, job));
pool.shutdown();
System.out.println(target.size());
assert source.isEmpty();
assert target.size()==10_000;
}
}
的简单天真转移,探测条目并将其放入目标地图(如果存在)。它看起来很简单,并且确实可以在线程数为1的情况下顺利运行,但在使用任何其他线程数时,在大多数环境中都会出现严重故障。
session.setAttribute("j_password", password);
但应该强调的是,没有同步的多线程仍然是不可预测的,因此它可能在一个或另一个测试运行中没有明显错误的情况下运行...