从 Java 中的 hashmap 获取值时出现 ArrayIndexOutOfBoundsException 17

ArrayIndexOutOfBoundsException while getting values from hashmap in Java 17

我有一个用于多线程的静态 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.

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

"I have a static HashMap<UUID, MyObject> ALL = new HashMap<>(); which is used in multi-threading"

哪里出错了!?? ;) (1. static 2. HashMap (非线程安全) 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)

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

所以你的代码的问题是它没有正确同步。所写的版本在更新 HashMap 时使用 synchronized,但在您访问它时却没有。要获得正确的 happens before 关系需要使此代码工作,reader 和更新程序线程都需要使用 synchronized.

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

此外,如果您同时迭代和更新一个 HashMap,您可能会得到一个 ConcurrentModificationException ... 即使以确保 [=76] 的方式完成操作=]发生在链存在之前。

简而言之...这段代码是错误的。

1 - 实际故障模式和频率可能取决于多种因素,例如您的 JVM 版本、您的硬件(包括内核数量)以及您的应用程序中发生的任何其他事情。您可以尝试调查行为的各种事情 有可能 使故障发生变化......或消失。


那么,你该如何解决呢?

嗯,有两种方法:

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

    缺点是 reader 会阻止更新程序,反之亦然。这可能导致任一线程中的“滞后”。 (这可能是您担心的更新程序。对于该线程,“滞后”将与地图中的条目数成正比......以及您对地图条目的处理。)

    这或多或少等同于使用 Collections.synchronizedMap 包装器。您将获得相同数量的“滞后”。请注意 javadoc 中有关使用同步地图包装器进行迭代的重要警告。 (寻找 "It is imperatively that ..."

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

    缺点是迭代 ConcurrentHashMap 不会为您提供地图状态的清晰快照。如果一个条目在迭代开始时存在并且在迭代结束时没有被删除,那么您一定会看到它。但是,如果添加或删除条目,您可能会看到也可能不会看到它们。


Map 变量 list 声明为 volatile 无法解决此问题。这样做只会在 之前为引用变量的读取和写入提供 发生。但是它没有给出 HashMap 上的操作之间的任何 happens before 关系。因此,如果 reader 和更新程序线程同时发生 运行,就会发生不好的事情。

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

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


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

A:据我所知,还没有发明/发现这样的数据结构。当然,在 Java SE 中没有 Map 具有这些属性的实现。