如何在Java中使列表的大小小于零?

huangapple go评论65阅读模式
英文:

How can List have size less than zero in java?

问题

我们在我们的Java应用程序日志中发现了这个错误:
java.lang.IndexOutOfBoundsException:索引:0,大小:-1

在 java.util.LinkedList.checkPositionIndex(LinkedList.java:560) 处进行了检查
在 java.util.LinkedList.listIterator(LinkedList.java:867) 处获取了列表迭代器
在 java.util.AbstractList.listIterator(AbstractList.java:299) 处获取了列表迭代器
在 java.util.AbstractSequentialList.iterator(AbstractSequentialList.java:239) 处获取了顺序访问列表的迭代器
在 //遍历列表的foreach循环中

LinkedList怎么可能出现大小小于零的情况?如果该列表不是线程安全的,是否可能是并发问题?无论如何,我们都无法复现这个问题。

英文:

We found this error in our java application log:
java.lang.IndexOutOfBoundsException: Index: 0, Size: -1

at java.util.LinkedList.checkPositionIndex(LinkedList.java:560)
at java.util.LinkedList.listIterator(LinkedList.java:867)
at java.util.AbstractList.listIterator(AbstractList.java:299)
at java.util.AbstractSequentialList.iterator(AbstractSequentialList.java:239)
at //foreach loop over list

How is this possible that LinkedList has size less than zero? Could it be concurrency issue if that list is not thread safe? We were not able to reproduce it anyway.

答案1

得分: 4

以下是翻译好的内容:

异常跟踪中的 -1 是由 outOfBoundsMsg 私有方法产生的,该方法只是在其中放置了 size 字段。

size 字段通过相关方法中的 size++size-- 进行修改。

剩下的解释如下:

  1. 虚拟机损坏。这似乎不太可能。
  2. 内存损坏。这似乎不太可能。
  3. 并发问题。

这是一个链表(查看堆栈跟踪),它不是线程安全的(实际上,只有非常少数的用例中链表是正确答案,如果你在你的编程生涯中从未使用过链表,那么你可能正好使用得适量)。

并发问题可以很容易地解释(任何给定的字段可能存在,也可能不存在 - 完全取决于虚拟机和当前的月相,就像薛定谔的猫一样:每个线程都有一个瞬时的副本,它们会不时地进行修改,偶尔会进行同步(通过任意方式解决冲突))。

根据虚拟机的设置,你应该编写代码,以便你无法观察到这一点;而不是编写能够预测任意同步时刻的代码,这是不可能的。

与大多数源于并发问题的问题一样,要可靠地重现这种情况是非常困难的。它取决于你的 Winamp 播放的是哪首音乐等等。

要修复它:你可以尝试用 synchronized() 块保护对此列表的所有访问,或者只需在 Collections.synchronizedList 中包装该列表,但尽管这将避免这些奇怪的情况,其中字段最终包含无效值,但这不太可能真正解决任何问题。像这样的代码:

if (!list.contains(a)) list.add(a);

永远无法正确工作,即使列表是“同步的”:同步意味着任何单个调用都被视为原子操作,但这是对列表的两次调用。其他代码可以在 contains 调用和 add 调用之间的中间添加 “a”:这就是为什么同步列表很少是你想要的。

更有可能的是,你想要一个更适合该任务的列表,可能来自 java.util.concurrent 包。可能你希望在线程之间进行所有通信,通过支持事务或以其他方式设计用于线程间通信的通道来进行。考虑“数据库,如 PostgreSQL,使用事务” 或 “消息队列系统,如 RabbitMQ”。

以下是如何以并发方式处理此类数据的示例:

synchronized(list) {
    if (!list.contains(a)) list.add(a);
}

假设对该列表的所有访问都受到同步保护,上述代码将起作用,并且永远不会导致 “a” 出现两次。

或者,考虑你的数据结构。在这个假设的例子中,如果目标不是在列表中重复相同的项,使用集合会更合理。确保你使用集合公开的 “原语”:

Set<String> set = ConcurrentHashMap.newKeySet();
...
set.add(a); // 安全,不需要同步块

或者另一个使用映射的例子:

Map<Integer, List<String>> m = new ConcurrentHashMap<>();

// 不安全:
if (!m.containsKey(1)) m.put(1, new ArrayList<String>());

// 安全:
m.computeIfAbsent(1, a -> new ArrayList<String>());
英文:

The '-1' in that exception trace is produced by the outOfBoundsMsg private method, which just puts the field size in there.

The size field is modified via size++ and size-- in the relevant methods.

That leaves the following explanations:

  1. A corrupt VM. That seems.. unlikely.
  2. corrupted memory. That seems.. unlikely.
  3. Concurrency issue.

It's LinkedList (see stack trace), which is not thread safe (in fact, there are very few use cases where linkedlist is the right answer, if you never use LinkedList in your programming career you're probably using it about the right amount).

The concurrency issue is trivially explained (any given field may, or may not - entirely up to the VM and the phase of the moon right now, exist the way schroedinger's cat does: Each thread has a fleeting copy of it that they modify, synchronizing it up (with conflicts resolved arbitrarily) from time to time).

The way the VM is set up, you're supposed to write code so that this cannot be observable to you; not to write code that for example predicts the arbitrary sync moments, which you can't.

As with most issues stemming from concurrency issues, it is extremely difficult to reliably reproduce stuff like this. It depends on which music was playing in your winamp and whatnot.

To fix it:. You could try to guard all access to this list with a synchronized() block, or just wrap the list in Collections.synchronizedList, but whilst that will avoid these bizarre situations where fields end up containing invalid values, that is unlikely to truly solve anything. Code like this:

if (!list.contains(a)) list.add(a);

can never work right, even if the list is 'synchronized': Synchronized means any single call is considered atomic, but that's 2 calls to list. Other code is free to add 'a' in the middle of the contains call and the add call: Hence why synchronized lists are rarely what you wanted.

More likely you want a list that is more suitable to the job, probably from the java.util.concurrent package. Possibly you want to do all communication between threads through a channel that supports transactions or otherwise is designed to be good for inter-thread comms. Think 'database, such as postgres, using transactions' or 'message queue system such as rabbitmq'.

To show how to work with data like this in a concurrent fashion:

synchronized(list) {
    if (!list.contains(a)) list.add(a);
}

Assuming all accesses to this list are done with a synchronized guard, the above will work, and will never result in a being in the list twice.

Or, think about your data structures. In this hypothetical example, a set feels more logical if the aim is not to have the same item in the list twice. Make sure you use the 'primitives' that the collection exposes:

Set&lt;String&gt; set = ConcurrentHashMap.newKeySet();
...
set.add(a); // safe, no need for a synchronized block

or another example with map:

Map&lt;Integer, List&lt;String&gt;&gt; m = new ConcurrentHashMap&lt;&gt;();

//unsafe:
if (!m.containsKey(1)) m.put(1, new ArrayList&lt;String&gt;());

// safe:
m.computeIfAbsent(1, a -&gt; new ArrayList&lt;String&gt;());

huangapple
  • 本文由 发表于 2020年5月19日 19:24:11
  • 转载请务必保留本文链接:https://go.coder-hub.com/61889853.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定