Java中的可中断网络I/O.

Bas*_*ass 6 java io multithreading nio

在Java 1.4+中,有3种方法可以中断在套接字I/O上阻塞的流:

  1. 如果套接字是使用常规java.net.Socket(InetAddress, int)构造函数创建的,我可以从单独的线程中关闭它.结果,SocketException在被阻塞的线程中抛出了a .
  2. 如果套接字是使用创建的SocketChannel.open(...).socket()(非阻塞I/O) - 再次,可以从单独的线程关闭它,但现在AsynchronousCloseException在被阻塞的线程中抛出了一个不同的异常(an ).
  3. 此外,如果使用非阻塞I/O,则可以通过ClosedByInterruptException抛出来中断被阻塞的线程.使用旧式Java I/O时中断被阻塞的线程对该线程没有影响.

问题:

  1. 在使用旧式I/O时,是否从单独的线程关闭套接字线程安全?如果没有,有哪些替代方案?
  2. 在使用NIO时,是否从单独的线程关闭套接字/通道线程安全
  3. Socket.close()使用NIO而不是常规IO时,行为有什么不同吗?
  4. 除了通过简单地中断线程来终止阻塞的I/O操作(以便我不再需要保持对套接字的引用)之外,使用NIO进行网络是否有任何好处?

Ser*_*nov 1

聚会有点晚了,答案已经涵盖了主题,但我想我仍然可以添加一些有用的东西

\n\n

我将尽力弄清楚 API 规范做出了哪些保证,以及特定于实现的内容(使用 Oracle 的 Java 8 源代码了解详细信息)。例如,如果某些东西在 Oracle 的 Java 8 中是安全的,但 API 不能保证这一点,那么它可能会在另一个供应商的实现或不同版本的 Java 中突然崩溃。

\n\n

TL;DR:即使对于基于阻塞流的 IO,NIO 恕我直言也要好得多。只要确保使用Socket.getInputStream()andSocket.getOutputStream()而不是Channels.newInputStream()and即可Channels.newOutputStream()。并针对可能的 API 规范违规(例如抛出或null意外返回错误异常)进行防御性编码。

\n\n
    \n
  1. 是的,老派Socket.close()是线程安全的。

    \n\n

    API方面:引用文档,\xe2\x80\x9c当前在此套接字上的I/O操作中被阻止的任何线程都会抛出SocketException.\xe2\x80\x9d 它并没有说\xe2\x80\x9csafe\xe2 \x80\x9d 任何地方,但如果没有线程安全,这样的保证是不可能的。

    \n\n

    实现方面:Oracle 的 Java 8Socket实现在线程安全方面相当糟糕。在随机的地方使用或不使用许多不同的锁。一个主要问题是是否可以在两个线程之间拆分同一套接字上的读取和写入。这似乎是可行的,但 API 规范并不能保证,而且代码看起来随时可能会随机中断。但就close()其本身而言,它有自身的锁定和一些内部锁的保护Socket,使其非常安全。

    \n\n

    不过,有一个(明显的)警告适用:close()它本身是线程安全的,但为了使其按预期工作,在访问套接字实例时必须遵循一般线程安全规则,也就是说,它应该正确发布到线程执行close().

  2. \n
  3. NIO 通常是非常线程安全的,无论是在规范中还是在实际实现中。close()没有什么不同。就实现而言,SocketChannel.socket()返回一个实例,SocketAdaptor该实例是适配器模式的一种情况,使 a 适应SocketChannelAPI SocketSocketNIO 根本不使用旧的实现,所有Socket操作都委托给底层SocketChannel

  4. \n
  5. API 在这里没有什么帮助,因为SocketChannel.socket()\xe2\x80\x9cofficially\xe2\x80\x9d 返回对 a 的引用Socket,其文档根本没有提到 NIO。一方面,考虑到向后兼容性和接口编程,它应该是这样的。另一方面,实现方面在SocketAdaptor适应接口方面做得很差Socket,至少在 Oracle 的 Java 8 中是这样。例如,并SocketInputStream没有真正尝试将异常包装在其Socket等效项中。这就是为什么你会看到AsynchronousCloseException文档承诺SocketException抛出 a 的原因。

    \n\n

    好消息是 NIO 实现总体上更好。因此,只要您不介意抛出错误的异常(为什么有人会关心IOException它是什么样的?),NIOSocket就会完成它的工作。就目前而言close(),它只是关闭关联的通道,因此您是否调用Socket.close()或并不重要SocketChannel.close()

  6. \n
  7. @Peter Lawrey 很好地涵盖了 NIO 与 IO 的一般优缺点。因此,我不会一般性地讨论 NIO 与 IO ,而是假设我们使用基于阻塞流的 IO。在这种情况下,蔚来有以下优点:

    \n\n
      \n
    • 它是完全线程安全的。我已经提到了并行读取和写入(异步协议的典型情况,当您发送长数据流并且必须准备好从进程的另一端接收消息时)。虽然它看起来可以与 IO 一起工作,但它保证可以与 NIO 一起工作。

    • \n
    • 您提到了通过中断线程来关闭套接字的能力,但它经常被低估。Thread.interrupt()人们可能认为和 之间没有太大区别Socket.close(),但事实上确实如此。如果你的线程不仅仅做 IO,你必须调用两者(顺序重要吗?不是很明显。)另一个考虑因素:如果你使用Executors线程(你应该这样做),它们对你的套接字一无所知,但是他们知道有关中断线程的一切。使用NIO,您只需取消一个Futureshutdown()一个Executor。对于 IO,您还必须以某种方式处理您的套接字。

    • \n
    \n\n

    缺点:

    \n\n
      \n
    • 将 NIO 与 IO 混合起来可能会很棘手。您可能认为这SocketChannel.socket().getInputStream()相当于Channels.newInputStream(SocketChannel). 嗯,事实并非如此。前者支持SocketTimeoutException,后者不支持(它只是永远阻塞)。当您应该接收心跳消息并在它们停止发送时关闭连接时,这对于协议来说非常重要。

    • \n
    • NIO 违反 IO API 规范的另一种情况是Socket.getRemoteSocketAddress()/ SocketChannel.getRemoteAddress()。根据Socket文档,\xe2\x80\x9c如果套接字在关闭之前已连接,则此方法将在套接字关闭后继续返回连接的地址。\xe2\x80\x9d 好吧,对于 NIO 来说,情况并非如此。SocketChannel.getRemoteAddress()会抛出ClosedChannelException,正如它应该的那样,但Socket.getRemoteSocketAddress()会返回null而不是正确的地址。这看起来似乎没什么大不了的,但它可能会在您最意想不到的地方导致 NPE。

    • \n
  8. \n
\n