从 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<>());
(“在多线程中使用”;)。
首先,你应该使用更好的变量名。即使是完全没有信息的名称也比使用 list
作为 HashMap
的变量名要好。 HashMap
不是一个列表,当你迭代它时,它甚至不像一个(正确的)列表。该变量名称只是误导。
所以你的代码的问题是它没有正确同步。所写的版本在更新 HashMap
时使用 synchronized
,但在您访问它时却没有。要获得正确的 happens before 关系需要使此代码工作,reader 和更新程序线程都需要使用 synchronized
.
如果 发生在 链之前,Java 内存模型不能保证一个线程执行的原始写操作对另一个线程可见。在这种情况下,这意味着 reader 执行的 HashMap
操作可能会遇到 stale 值。这可能会导致各种错误1,包括不正确的结果、ArrayIndexOutOfBoundsException
s、NullPointerException
s 甚至无限循环。
此外,如果您同时迭代和更新一个 HashMap
,您可能会得到一个 ConcurrentModificationException
... 即使以确保 [=76] 的方式完成操作=]发生在链存在之前。
简而言之...这段代码是错误的。
1 - 实际故障模式和频率可能取决于多种因素,例如您的 JVM 版本、您的硬件(包括内核数量)以及您的应用程序中发生的任何其他事情。您可以尝试调查行为的各种事情 有可能 使故障发生变化......或消失。
那么,你该如何解决呢?
嗯,有两种方法:
确保 reader 和更新程序线程都从 synchronized
块内访问 HashMap
。在 reader 的情况下,请务必将迭代地图值视图的整个操作放入 synchronized
块中。 (否则你会得到 CME 的)
缺点是 reader 会阻止更新程序,反之亦然。这可能导致任一线程中的“滞后”。 (这可能是您担心的更新程序。对于该线程,“滞后”将与地图中的条目数成正比......以及您对地图条目的处理。)
这或多或少等同于使用 Collections.synchronizedMap
包装器。您将获得相同数量的“滞后”。请注意 javadoc 中有关使用同步地图包装器进行迭代的重要警告。 (寻找 "It is imperatively that ...")
将 HashMap
更改为 ConcurrentHashMap
。这将消除在 synchronized
块内执行操作的需要。 ConcurrentHashMap
class 是线程安全的...从某种意义上说,您无需担心内存模型引发的异常和 heisenbugs。
缺点是迭代 ConcurrentHashMap
不会为您提供地图状态的清晰快照。如果一个条目在迭代开始时存在并且在迭代结束时没有被删除,那么您一定会看到它。但是,如果添加或删除条目,您可能会看到也可能不会看到它们。
将 Map
变量 list
声明为 volatile
无法解决此问题。这样做只会在 之前为引用变量的读取和写入提供 发生。但是它没有给出 HashMap
上的操作之间的任何 happens before 关系。因此,如果 reader 和更新程序线程同时发生 运行,就会发生不好的事情。
在实践中,添加 volatile
会使问题发生的频率降低,并且更难重现或测试。 IMO,它使问题变得更糟 。
(此外,如果 list
是一个局部变量,就像在您的示例中那样,它无论如何都不能声明为 volatile
。)
问:是否有 O(1)
操作的解决方案可以为您提供无延迟的干净地图快照语义?
A:据我所知,还没有发明/发现这样的数据结构。当然,在 Java SE 中没有 Map
具有这些属性的实现。
我有一个用于多线程的静态 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<>());
(“在多线程中使用”;)。
首先,你应该使用更好的变量名。即使是完全没有信息的名称也比使用 list
作为 HashMap
的变量名要好。 HashMap
不是一个列表,当你迭代它时,它甚至不像一个(正确的)列表。该变量名称只是误导。
所以你的代码的问题是它没有正确同步。所写的版本在更新 HashMap
时使用 synchronized
,但在您访问它时却没有。要获得正确的 happens before 关系需要使此代码工作,reader 和更新程序线程都需要使用 synchronized
.
如果 发生在 链之前,Java 内存模型不能保证一个线程执行的原始写操作对另一个线程可见。在这种情况下,这意味着 reader 执行的 HashMap
操作可能会遇到 stale 值。这可能会导致各种错误1,包括不正确的结果、ArrayIndexOutOfBoundsException
s、NullPointerException
s 甚至无限循环。
此外,如果您同时迭代和更新一个 HashMap
,您可能会得到一个 ConcurrentModificationException
... 即使以确保 [=76] 的方式完成操作=]发生在链存在之前。
简而言之...这段代码是错误的。
1 - 实际故障模式和频率可能取决于多种因素,例如您的 JVM 版本、您的硬件(包括内核数量)以及您的应用程序中发生的任何其他事情。您可以尝试调查行为的各种事情 有可能 使故障发生变化......或消失。
那么,你该如何解决呢?
嗯,有两种方法:
确保 reader 和更新程序线程都从
synchronized
块内访问HashMap
。在 reader 的情况下,请务必将迭代地图值视图的整个操作放入synchronized
块中。 (否则你会得到 CME 的)缺点是 reader 会阻止更新程序,反之亦然。这可能导致任一线程中的“滞后”。 (这可能是您担心的更新程序。对于该线程,“滞后”将与地图中的条目数成正比......以及您对地图条目的处理。)
这或多或少等同于使用
Collections.synchronizedMap
包装器。您将获得相同数量的“滞后”。请注意 javadoc 中有关使用同步地图包装器进行迭代的重要警告。 (寻找 "It is imperatively that ...")将
HashMap
更改为ConcurrentHashMap
。这将消除在synchronized
块内执行操作的需要。ConcurrentHashMap
class 是线程安全的...从某种意义上说,您无需担心内存模型引发的异常和 heisenbugs。缺点是迭代
ConcurrentHashMap
不会为您提供地图状态的清晰快照。如果一个条目在迭代开始时存在并且在迭代结束时没有被删除,那么您一定会看到它。但是,如果添加或删除条目,您可能会看到也可能不会看到它们。
将 Map
变量 list
声明为 volatile
无法解决此问题。这样做只会在 之前为引用变量的读取和写入提供 发生。但是它没有给出 HashMap
上的操作之间的任何 happens before 关系。因此,如果 reader 和更新程序线程同时发生 运行,就会发生不好的事情。
在实践中,添加 volatile
会使问题发生的频率降低,并且更难重现或测试。 IMO,它使问题变得更糟 。
(此外,如果 list
是一个局部变量,就像在您的示例中那样,它无论如何都不能声明为 volatile
。)
问:是否有 O(1)
操作的解决方案可以为您提供无延迟的干净地图快照语义?
A:据我所知,还没有发明/发现这样的数据结构。当然,在 Java SE 中没有 Map
具有这些属性的实现。