ArrayIndexOutOfBoundsException,同时从Java 17中的hashmap获取值



我有一个用于多线程的静态HashMap<UUID, MyObject> ALL = new HashMap<>();

为了重现错误,我编写了以下代码:

HashMap<Integer, String> list = new HashMap<>();
list.put(1, "str 1");
list.put(2, "str 2");
new Thread(() -> {
while(true) {
ArrayList<String> val;
synchronized(list) {
val = new ArrayList<>(list.values());
}
System.out.println(val.toString());
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
new Thread(() -> {
while(true) {
list.put(new Random().nextInt(), "some str");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();

但是,几秒钟后(大约 10 秒),我在 Java 16 和 Java 17 中收到此错误:

java.lang.ArrayIndexOutOfBoundsException: Index 2 out of bounds for length 2
at java.util.HashMap.valuesToArray(HashMap.java:973) ~[?:?]
at java.util.HashMap$Values.toArray(HashMap.java:1050) ~[?:?]
at java.util.ArrayList.<init>(ArrayList.java:181) ~[?:?]

使用Java 8,我得到这个:

Exception in thread "Thread-0" java.util.ConcurrentModificationException
at java.util.HashMap$HashIterator.nextNode(HashMap.java:1473)
at java.util.HashMap$ValueIterator.next(HashMap.java:1502)
at java.util.AbstractCollection.toArray(AbstractCollection.java:141)
at java.util.ArrayList.<init>(ArrayList.java:178)

为了进行测试,我删除了synchronized关键字,然后在Java 17中重试,得到以下结果:

java.util.ConcurrentModificationException: null
at java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1631) ~[?:?]
at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:509) ~[?:?]
at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:499) ~[?:?]
at java.util.stream.ForEachOps$ForEachOp.evaluateSequential(ForEachOps.java:150) ~[?:?]
at java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateSequential(ForEachOps.java:173) ~[?:?]
at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234) ~[?:?]
at java.util.stream.ReferencePipeline.forEach(ReferencePipeline.java:596) ~[?:?]

这些错误似乎很奇怪,尤其是第一个错误。我怀疑它们来自 JRE 本身。我正在使用 Java 17.0.1 build 17.0.1+12-LTS-39。

如何从另一个线程获取所有值?

首先,你应该使用更好的变量名。 即使是完全没有信息性的名称也比使用list作为HashMap的变量名称更好。HashMap不是列表,当您迭代它时,它甚至不像(正确的)列表。 该变量名称只是误导。

因此,代码的问题在于它没有正确同步。 编写的版本在更新HashMap时使用synchronized,但在您访问它时不使用。 为了在关系需要使此代码正常工作之前正确发生,读取器和更新程序线程都需要使用synchronized

如果没有链发生,Java 内存模型不能保证一个线程执行的原始写入操作对另一个线程可见。 在这种情况下,这意味着读取器执行HashMap操作都可能遇到过时的值。 这可能会导致各种事情出错1,包括不正确的结果、ArrayIndexOutOfBoundsExceptions、NullPointerExceptions 甚至无限循环。

此外,如果您同时迭代和更新HashMap则可能会获得ConcurrentModificationException...即使操作的完成方式可确保在链存在之前发生

总之。。。此代码是错误的。

1 - 实际的故障模式和频率可能取决于诸如 JVM 版本、硬件(包括内核数)以及应用程序中发生的任何其他情况等因素。 您可以尝试调查行为的各种事情都可能导致故障改变......或者走开。


那么,如何解决它呢?

嗯,有两种方法:

  1. 确保读取器和更新程序线程都从synchronized块内部访问HashMap。 在读取器情况下,请确保将迭代地图值视图的整个操作放入synchronized块中。 (否则你会得到CME)

    缺点是阅读器会阻止更新程序,反之亦然。 这可能会导致任一线程中的"滞后"。 (这可能是您担心的更新程序。 对于该线程,"滞后"将与映射中的条目数成正比......以及您对地图条目执行的操作。

    这或多或少等同于使用Collections.synchronizedMap包装器。 您将获得相同数量的"滞后"。 请注意 javadoc 中关于使用同步映射包装器进行迭代的重要警告。 (查找"当务之急...">)

  2. HashMap更改为ConcurrentHashMap。 这将消除在synchronized块内执行操作的需要。ConcurrentHashMap类是线程安全的...从某种意义上说,您无需担心内存模型引起的异常和 heisenbug。

    缺点是迭代ConcurrentHashMap不会为您提供映射状态的干净快照。 如果某个条目在迭代开始时存在,并且在迭代结束时尚未删除,则保证可以看到它。 但是,如果添加或删除条目,您可能会或可能不会看到它们。


Map变量list声明为volatile并不能解决此问题。 这样做只会在读取和写入引用变量之前发生。 但它并没有在HashMap上的操作之间的关系之前发生任何事情. 因此,如果读取器和更新程序线程碰巧同时运行,就会发生不好的事情。

实际上,添加volatile会使问题发生的频率降低,并且更难重现或测试。 IMO,它使问题变得更糟

(此外,如果list是一个局部变量,就像在你的例子中一样,无论如何它都不能声明为volatile


Q:是否有具有O(1)操作的解决方案,可以为您提供干净的地图快照语义而没有延迟?

答:AFAIK,没有发明/发现这样的数据结构。 当然,Java SE 中没有具有这些属性的Map实现。

"我有一个用于多线程的static HashMap<UUID, MyObject> ALL = new HashMap<>();">

错误在哪里?? ;)(1. 静态 2.哈希映射(非线程安全) 3.多线程)

TLDR

尝试:

static Map<UUID, MyObject> ALL = java.util.Collections.synchronizedMap(new HashMap<>());

(到"在多线程中使用";)。

Javadoc: https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/Collections.html#synchronizedMap(java.util.Map)

最新更新