【问题标题】:Java 8 non-blocking read has race condition?Java 8 非阻塞读取有竞争条件?
【发布时间】:2015-06-26 12:28:19
【问题描述】:

现在这个问题困扰了我一段时间。

在我工作的应用程序中,我在非阻塞模式下使用 SocketChannel 与嵌入式设备进行通信。 现在我收到偶尔损坏的数据。 在某些 PC 上它不会发生,现在它发生在我的身上。 但是当我在程序中改变太多时,问题就消失了。

这么多可能会产生影响。时机,网络接口硬件,win7,java版本,公司防火墙,...

数据读取归结为这段代码:

byteBuffer.compact();
socketChannel.read(byteBuffer); // <<< problem here ?
byteBuffer.flip();
if( byteBuffer.hasRemaining() ){
    handleData( byteBuffer );
}

This is run in the same thread as the writing, when the selector wakes up and the interest op OP_READ is set.

此代码是唯一引用 byteBuffer 的地方。写入时,socketChannel 仅在同一线程中使用。

我对代码进行了检测,因此当错误发生时,我可以打印出最后几个 read() 调用的内容。同时我分析了Wireshark上的网络流量。我添加了很多断言来检查字节缓冲区的完整性。

在 Wireshark 中,接收到的流看起来不错。没有 DUP-ACK 或其他可疑的东西。最后一次 read() 调用与 Wireshark 中的数据完全匹配。

在 Wireshark 中,我看到许多小型 TCP 帧以 10 毫秒的间隔接收 90 字节的有效载荷数据。通常,Java 线程在数据刚到达时也会读取所有 10 毫秒的数据。

当涉及到问题时,Java 线程有点延迟,因为读取发生在 300 毫秒后,读取返回大约 3000 字节,这是合理的。但数据已损坏。

数据看起来像,如果它被复制到缓冲区并且同时接收的数据已经覆盖了第一个数据。

现在我不知道如何继续。我无法创建一个小例子,因为这种情况很少发生,而且我不知道需要的确切条件。

谁能给个提示?

如何证明它是不是Java lib?

哪些条件也很重要?

谢谢 弗兰克

2015 年 6 月 29 日:

现在我能够构建一个用于复制的示例。

有一个Sender 和一个Receiver 程序。

Sender 正在使用阻塞 IO,首先等待连接,然后每 2ms 发送 90 个字节的块。前 4 个字节是运行计数器,其余不设置。 Sender 使用 setNoTcpDelay(true)。

接收器正在使用非阻塞 IO。首先它连接到发送器,然后在选择键准备好时读取通道。有时,读取循环会执行 Thread.sleep(300)。

如果它们通过环回在同一台 PC 上运行,这对我来说一直有效。如果我将 Sender 放在另一台 PC 上,直接通过 LAN 连接,则会触发错误。用 Wireshark 检查,流量和发送的数据看起来不错。

要运行,首先在一台 PC 上启动 Sender,然后(在编辑主机地址后)启动 Receiver。

只要它有效,它大约每 2 秒打印一行。如果失败,它会打印有关最近 5 次 read() 调用的信息。

我发现是什么触发:

  1. 发送方已配置setNoTcpDelay(true)
  2. 接收方有时在执行 read() 之前有一个 Thread.sleep(300)。

谢谢 弗兰克

【问题讨论】:

  • 虽然 Java 中可能存在错误,但这种可能性极小……鉴于大量其他 Java 8 程序员(显然)没有遇到此类问题。它更有可能是您的代码中的错误。该怎么办?好吧,如果您的代码太难简化为 MCVE,并且太大而无法向我们展示,那么最好的办法是让同事帮助您。
  • 如果代码全部在一个线程上执行,那么它不太可能是正常意义上的“竞态条件”。 (我不知道这是否有帮助......)
  • 从您提到的情况来看 - 可能存在问题 - 您使用数据的方式。这只是一个猜测。出于某种原因,您提到的内容未解释 - 当读取数据的 java 线程在 300 毫秒后被安排时,您收到了很多 90 字节的数据包背靠背。如果您假设以 90 字节的块读取它们,请明确以 90 字节读取。然后消耗更多 - 当它们超过 90 个字节时。我不知道确切的 API,但可能这应该指向某个方向?
  • 也许问题是你读到buf而不是byteBuffer?说真的,我们应该如何在不知道您对缓冲区的真正用途的情况下为您提供帮助? IE。 handleData( byteBuffer ) 是做什么的?
  • 但是当我在程序中改变太多时,问题就消失了。这是你能得到的最模棱两可和不具体的。

标签: java multithreading tcp java-8 nonblocking


【解决方案1】:
        buf.order(ByteOrder.BIG_ENDIAN);

这是默认设置。删除这个。

        buf.clear();

缓冲区已经是空的,因为你刚刚分配了它。删除这个。

        buf.limit(0);

在 clear() 之后以及初始分配之后,限制已经为零。删除这个。

        while( true ) {

这里应该有一个 select() 调用。

            Iterator<SelectionKey> it = selector.selectedKeys().iterator();
            // ...
            if( key == keyData && key.isConnectable() ) {
                    ch.finishConnect();

此方法可以返回 false。你没有处理那个案子。

            // ...
            if( key == keyData && key.isReadable() ) {

                    // ...
                    readPos += ch.read(buf);

完全不正确。您完全忽略了 read() 返回 -1 的情况,这意味着对等方已断开连接。在这种情况下,您必须关闭频道。

            // without this Thread.sleep, it would not trigger the error

所以?一分钱不是掉了吗? 消除睡眠。这完全没有意义。 select() 将阻塞直到数据到达。它不需要你的帮助。这种睡眠简直是浪费时间。

            if( rnd.nextInt(20) == 0 ) {
                Thread.sleep(300);
            }

删除这个。

            selector.select();

这应该在循环的顶部,而不是底部。

【讨论】:

  • 你知道这是一个最小的例子吗? read() -> -1 的情况无关紧要。字节顺序/清除/限制可能是多余的,但它仍然没有错。 Thread.sleep 这里的重点。因为我想触发在更复杂的程序中看到的问题。我知道这里不需要。如果选择结束,第一次迭代将有一个空的 selectedkey 集。
  • stackoverflow 不允许我粘贴代码。因此我通过链接发布。
  • 来自 ByteBuffer.allocate 的 javadoc:“新缓冲区的位置将为零,其限制将是其容量”。这意味着需要限制(0)调用
【解决方案2】:

原来是驱动问题,至少看起来是这样。

我使用了 USB 转以太网适配器“D-Link E-DUB100 Rev A”。
由于wireshark显示正确的数据,我想消除硬件可能的故障原因。
但同时我尝试了“D-Link E-DUB100 Rev C1”,问题就消失了。
所以我认为这是 D-Link 为 Rev A 提供的驱动程序中的一个问题。对于 Rev C1,它可能使用没有这个问题的系统驱动程序。

感谢大家花时间阅读我的问题。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2012-02-28
    • 2023-03-23
    • 1970-01-01
    • 2016-07-22
    • 2011-03-20
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多