我最近收到了 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
。
现在出现
c
的 NPE,它一定是之前添加到 _connections
的值 - 但怎么会变成 null
?
要明确的是,我不是在寻找解决方案 - 我在流中添加了
filter(Objects::nonNull)
(或者我可以在初始化程序中使用 Collections.synchronizedSet()
),并且它现在保证可以工作。
这怎么可能发生?是的,多线程访问几乎可以搞砸一切,但是将
null
放入不存在的集合中?
正如您所提到的,您必须在初始化程序中使用
Collections.synchronizedSet()
。
操作不同线程中未同步的集合可能会(肯定会)弄乱
LinkedHashSet
内部状态。 (即条目之间的双重链接等)
另一个好的(可能是最好的)方法是将您的
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);
}
public Set <TcpIpConnection> getConnections() {
return Collections.unmodifiableSet(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"));
希望对您有帮助。
以下参考的是jdk-21+35
在
new ArrayList<>(_connections)
中,会调用 // HashSet#toArray()
@Override
public Object[] toArray() {
return map.keysToArray(new Object[map.size()]);
}
在这里我们可以看到一个Object[]被构造成长度为
map.size()
,并且这个Object[]将被复制到ArrayList中。
// 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[] 设置值。 因此,在并发环境中,当我们调用
HashSet#toArray()
时,映射大小可能为 2,但是当我们转到 LinkedHashMap#keysToArray()
时,映射大小可能为 1(某些线程删除了中间的元素),因此可能存在一些 null Object[] 中的元素。反之亦然 ArrayIndexOutOfBound
可能会被抛出。