NIO2 CompletionHandler 的线程安全

Posted

技术标签:

【中文标题】NIO2 CompletionHandler 的线程安全【英文标题】:Thread-safety of NIO2 CompletionHandler 【发布时间】:2021-10-26 08:50:17 【问题描述】:

以下代码是线程安全的吗?如果是这样,什么保证ByteBuffer实例安全发布到执行CompletionHandler的线程?

AsynchronousSocketChannel channel = ...
ByteBuffer buf = ByteBuffer.allocate(1024);
channel.read(buf, null, new CompletionHandler<Integer, Void>() 

    //"completed" can be executed by a different thread than channel.read()
    public void completed(Integer result, Void attachment)                 
        buf.flip(); //Can buf be safely accessed here? If so, why?   
        //...          
    

    public void failed(Throwable exc, Void attachment) 
       //...
    
);

【问题讨论】:

这可以编译吗? buf 必须是 final 或此处的“有效最终”。在任何情况下,您都不应该继续重新分配读取缓冲区。在频道的整个生命周期内使用相同的密钥,并将其保存在密钥附件中或通过密钥附件保存。 @user207421 是的,它可以编译,因为buf 在这种情况下实际上是最终的。关于缓冲区分配 - 这是用于问题目的的示例代码。 【参考方案1】:

对所有 JVM 和所有平台都有效的权威参考

我所知道的唯一这样的权威来源是 javadocs(1、2、3)。

不幸的是,正如您亲眼所见,它们不包含对线程安全的明确和明确的保证。

这意味着代码不是线程安全的。


IMO 应在 javadoc 中为 the method、the method's class 或 CompletionHandler 提供保证——然后我们可以确定它们已在所有 JVM 和所有平台上实现(并将在未来保持实现)。

但如果你真的想要,你可以从不同的 javadocs 中的多个地方“编译”一个线程安全证明:

AsynchronousSocketChannel.read(...):

handler 参数是一个完成处理程序,在读取操作完成(或失败)时调用。

java.nio.channels:

为了资源共享,异步通道绑定到一个异步通道组。组具有关联的 ExecutorService,任务被提交到该 ExecutorService 以处理 I/O 事件并分派到完成处理程序,这些处理程序使用在组中的通道上执行的异步操作的结果。

ExecutorService:

内存一致性影响:线程中的操作在将 Runnable 或 Callable 任务提交给 ExecutorService 之前发生 - 在该任务采取的任何操作之前

因此,我们得到 I/O 的每个操作都读取到 ByteBuffer发生在CompletionHandler 的第一个操作 => 这意味着代码是线程安全的。

像上面这样的 IMO“编译证明”太脆弱了,我个人认为代码不是线程安全的。

【讨论】:

【参考方案2】:

只需添加to the answer above,您可能更愿意通过attachment 方法的attachment 参数将ByteBuffer 传递给CompletionHandler(就像在various examples 中完成的那样):

AsynchronousSocketChannel channel = ...
ByteBuffer buf = ByteBuffer.allocate(1024);
channel.read(buf, buf, new CompletionHandler<>() 

    public void completed(Integer result, ByteBuffer buf)                 
        buf.flip();
        //...          
    

    public void failed(Throwable exc, ByteBuffer buf) 
       //...
    
);

由于attachmentread(...) 的显式参数,它必须安全地发布到CompletionHandler 的线程。 不幸的是,我在 javadocs 中没有看到任何明确保证此安全发布发生在read(...) 完成之后。

【讨论】:

【参考方案3】:

我在 javadocs 中没有看到任何关于 ByteBuffer 内部状态的明确保证,在 read() 操作中使用,在 read() 完成时调用的 CompletionHandler 内部可见。

所以没有 100% 的保证。

但我想说的是,期望这是真的是常识。 原因很简单:

    期待CompletionHandler 看到“read()”所做的更改是正常的 CompletionHandler 可以在不同的线程中异步运行这一事实——这是read() 内部实现的一个细节。因此,read() 关心的是确保在这种情况下安全发布该内容。

但如果您不确定/不相信,并且您不开发具有纳秒级延迟的应用程序 - 只需添加您自己的额外同步 - 您将 100% 确定您的程序正常运行。

【讨论】:

感谢您的意见。我倾向于同意这是线程安全的,并且我意识到我可以使用锁来确保它是线程安全的。然而,问题不在于这个。它是关于 什么保证 跨 JVM 的这种行为。操作系统和 CPU 架构。

以上是关于NIO2 CompletionHandler 的线程安全的主要内容,如果未能解决你的问题,请参考以下文章

AIO概述

JDK AIO编程

在 java.nio2 中将路径设置为只读的正确方法

Java NIO2 File API介绍

什么是NIO2

Java 7 异步 NIO2 服务器上的连接被拒绝