Java 集合:NPE,即使没有添加空值

问题描述 投票:0回答:2

我最近收到了 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
放入不存在的集合中?

java multithreading collections nullpointerexception
2个回答
0
投票

正如您所提到的,您必须在初始化程序中使用

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"));

希望对您有帮助。


0
投票

只是为了解释为什么 null 是可能的

以下参考的是jdk-21+35

new ArrayList<>(_connections)
中,会调用
_connections.toArray()
-> HashSet#toArray()(LinkedHashSet 扩展了 HashSet)
-> LinkedHashMap#keysToArray()(因为 LinkedHashSet 由 LinkedHashMap 支持)

    // 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
可能会被抛出。

© www.soinside.com 2019 - 2024. All rights reserved.