You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Java集合疑问:未添加null值却出现NPE的原因探究

多线程环境下LinkedHashSet迭代时出现NullPointerException的原因

问题描述

我最近在一个原本认为不可能出现NPE的场景中遇到了该异常。这个类运行在高并发多线程程序中,我知道多线程环境下什么都可能发生,但仍对此感到困惑。

该类定义了如下字段:

private final Set<TcpIpConnection> _connections = new LinkedHashSet<>();

在整个类中,这个集合仅在两处被操作:

// 某个方法
TcpIpConnection tcpipConnection = new ServerConnection(clientSocket, _channels, MyClass.this);
_connections.add(tcpipConnection);
// 另一个方法
_connections.remove(connection);

显然,这里没有任何可能向集合中添加null值,且该集合始终在类内部使用,从未对外暴露。

但现在我的测试用例有时会在以下语句中抛出NPE,这是类中唯一使用_connections的其他语句:

new ArrayList<>(_connections).stream().forEach(c -> c.close("Server down"));

如你所见,我已经通过先将集合复制为ArrayList来避免ConcurrentModificationException。

现在NPE出现在变量c上,而c应该是之前添加到_connections中的值——但它怎么会变成null呢?

需要说明的是,我并非寻求解决方案——我已经在流中添加了filter(Objects::nonNull)(或者也可以在初始化时使用Collections.synchronizedSet()),现在问题已得到解决。

这种情况究竟是如何发生的?没错,多线程访问几乎会搞砸一切,但怎么会在原本没有null的集合中出现null呢?

原因分析

核心问题在于**LinkedHashSet本身不是线程安全的集合**,即使没有主动添加null值,并发的读(迭代复制)和写(add/remove)操作也会破坏其内部的数据结构,导致迭代器返回null。

具体来说:

  • LinkedHashSet基于双向链表实现,迭代器遍历完全依赖链表节点的引用关系。
  • 当一个线程执行new ArrayList<>(_connections)时,会启动集合的迭代器遍历所有元素;与此同时,另一个线程执行remove操作,会修改链表中对应节点的前后引用。
  • 这种并发修改会导致迭代器的遍历逻辑出现异常:比如迭代器刚拿到某个节点的引用,该节点就被移除,链表的引用链被打断,迭代器后续遍历可能拿到已经被清理的节点引用(表现为null);或者因为节点的next/prev被篡改,遍历到不存在的位置返回null。

虽然LinkedHashSet的迭代器是fail-fast设计,但fail-fast的触发条件是检测到modCount和expectedModCount不一致,这并不是100%能捕获所有并发修改场景的——在某些时序下,并发修改不会触发fail-fast,而是导致迭代器返回异常值(比如null),最终引发NPE。

内容的提问来源于stack exchange,提问作者slarti76

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.06 15:46:20