提问人:slarti76 提问时间:11/8/2023 最后编辑:Mark Rotteveelslarti76 更新时间:11/14/2023 访问量:79
Java 集合:NPE,即使从未添加过 null 值
Java Collections: NPE even though no null value was ever added
问:
我最近在我绝对确定这是不可能的情况下获得了 NPE。该类用于一个非常多线程的程序中,所以我知道你应该期望基本上任何事情都是可能的,但仍然如此。
因此,该类定义了以下字段:
private final Set<TcpIpConnection> _connections = new LinkedHashSet<>();
在整个类中,这个集合只在两个地方作:
// some method
TcpIpConnection tcpipConnection = new ServerConnection(clientSocket, _channels, MyClass.this);
_connections.add(tcpipConnection);
// some other method
_connections.remove(connection);
所以我想你会同意,不可能在集合中添加一个。是的,这套设备留在课堂上,永远不会在外面扩散。null
但是现在我有一个测试用例,它有时会在以下语句中因 NPE 而失败,这是类中唯一使用的其他语句:_connections
new ArrayList<>(_connections).stream().forEach(c -> c.close("Server down"));
正如你所看到的,我已经通过首先创建集合的本地副本来阻止 .ConcurrentModificationException
ArrayList
现在 NPE 出现 ,它必须是之前添加的值 - 但这怎么可能变成 ?c
_connections
null
需要明确的是,我不是在寻找解决方案——我在流中添加了(或者我可以在初始化器中使用),并且它现在可以保证工作。filter(Objects::nonNull)
Collections.synchronizedSet()
这怎么可能呢?是的,多线程访问几乎可以搞砸所有事情,但是将一个不存在的集合放在一个集合中?null
答:
正如你提到的,你必须在你的初始值设定项中使用 a。但这还不够:迭代器不安全。Collections.synchronizedSet()
操作在不同线程中未同步的集合可能会弄乱(肯定会)内部状态。(即条目之间的双链接等)LinkedHashSet
一个好方法是将 your 封装到另一个类中,并提供同步方法来添加和删除连接,并且至少返回 Set 的线程安全副本。Set
可能是这样的:
class ConnectionManager
{
public static ConnectionManager instance = new ConnectionManager();
private Set<TcpIpConnection> connections = new LinkedHashSet<>();
private ConnectionManager { } // protection against external construction
public static ConnectionManager getInstance() {
return instance;
}
public synchronized void add(TcpIpConnection connection) {
this.connections.add(connections);
}
public synchronized void remove(TcpIpConnection connection) {
this.connections.remove(connections);
}
// returns a synchronized copy and unmodifiable Set (thanx to Holger remark)
public synchronized Set <TcpIpConnection> getConnections() {
return Collections.unmodifiableSet(new LinkedHashSet(this.connections));
}
}
然后在您的代码中:
TcpIpConnection tcpipConnection = new ServerConnection(clientSocket, _channels, MyClass.this);
ConnectionManager.getInstance().add(tcpipConnection);
// or
ConnectionManager.getInstance().remove(tcpipConnection);
并使用您的流:
ConnectionManager.getInstance()
.getConnections()
.stream()
.forEach(c -> c.close("Server down"));
评论
Collections.synchronizedSet()
ConcurrentLinkedHashSet
ConcurrentLinkedHashSet
Collections.synchronizedSet()
synchronized(set) { … }
_connections = Collections.synchronizedSet(…); … ArrayList<TcpIpConnection> workingCopy; synchronized(set) { workingCopy = new ArrayList<>(_connections); } workingCopy.stream().forEach(c -> c.close("Server down"));
只是为了解释为什么 null 是可能的
以下是参考 jdk-21+35
在 中,它将调用
_connections.toArray() -> HashSet#toArray()(因为 LinkedHashSet 扩展了 HashSet) -> LinkedHashMap#keysToArray()(因为 LinkedHashSet 由 LinkedHashMap 支持) new ArrayList<>(_connections)
// HashSet#toArray()
@Override
public Object[] toArray() {
return map.keysToArray(new Object[map.size()]);
}
在这里,我们可以看到一个 Object[] 是用 length 构造的,这个 Object[] 将被复制到 ArrayList。map.size()
// LinkedHashMap#keysToArray()
final <T> T[] keysToArray(T[] a) {
return keysToArray(a, false);
}
final <T> T[] keysToArray(T[] a, boolean reversed) {
Object[] r = a;
int idx = 0;
if (reversed) {
for (LinkedHashMap.Entry<K,V> e = tail; e != null; e = e.before) {
r[idx++] = e.key;
}
} else {
for (LinkedHashMap.Entry<K,V> e = head; e != null; e = e.after) {
r[idx++] = e.key;
}
}
return a;
}
以上是我们如何为 Object[] 设置值。
因此,在并发环境中,当我们调用 时,映射大小可能是 2,但是当我们转到 时,映射大小可能是 1(中间的一些线程删除了元素),因此 Object[] 中可能有一些 null 元素。反之亦然。HashSet#toArray()
LinkedHashMap#keysToArray()
ArrayIndexOutOfBound
结论
在多线程环境中工作时,永远不要假设任何非线程安全类的行为,有 0 保证。
评论
null
null
head
e.after
size
null
null
评论
ArrayList
new ArrayList<>(_connections)
ConcurrentModificationException
ConcurrentModificationException
LinkedHashSet
null
null
null
_connections.add(tcpipConnection)
null
tcpipConnection
null
null
get
HashMap
null