【问题标题】:Passing a Java socket from thread A to B将 Java 套接字从线程 A 传递到 B
【发布时间】:2021-02-13 17:49:27
【问题描述】:

在服务器中,有一个线程 A 监听传入的连接,通常永远循环。当一个连接被接受时,线程 A 创建一个任务(比如 Java 中的 Callable 类)并将其提交给 Executor。 这实际上意味着 A 丢失了对套接字的引用,而现在有一个线程 B(由 Executor 创建)来管理套接字。如果 B 遇到任何异常,它将关闭套接字,并且套接字作为操作系统资源不会被回收。

如果线程 B 启动,这一切都很好。但是如果执行器在 B 有机会被安排之前就被关闭了怎么办?

有人认为这是个问题吗?如果对套接字的引用因此丢失,垃圾收集器会关闭套接字吗?

【问题讨论】:

    标签: java multithreading sockets executor


    【解决方案1】:

    是的,这听起来像是个问题。

    操作系统最终可能会释放套接字(至少如果它是 TCP,据我所知),但这可能需要相对较长的时间。

    我不认为垃圾收集器在这种情况下发挥作用。至少对于线程来说不是这样,即使在代码中没有对它们的引用,它们在启动后通常也会继续运行(至少对于非守护线程来说是这样)。套接字的行为方式可能类似。

    如果您不能保证连接将被处理(通过在建立后立即开始处理 Thread 实例),那么您应该保留对套接字的引用并确保尽快关闭所有这些可能,这可能意味着在调用Executor.shutdown() 或类似方法之后。

    请注意,根据您要求 Executor 关闭的方式,它会处理或不处理已经提交执行但尚未启动的线程。所以一定要让你的代码有相应的行为。

    此外,如果您处理传入套接字连接的资源(可用线程)有限并且不希望它们增长太多,请考虑在被接受后立即关闭它们,以免它们堆积在未处理的等待队列中,如果这在您的项目中是可行的。然后,客户端可以稍后重试连接。如果您仍然需要在连接进入后立即使用它们,请考虑采用非阻塞 I/O 方法,这种方法往往会更好地扩展(并且达到一定程度)。

    【讨论】:

    • 我很高兴我不是唯一的偏执狂。我现有的代码正是这样做的。线程 A 保留一个引用,并等待直到线程 B 发出可以释放该引用的信号。如果 B 没有足够快地获取套接字,则 A 关闭套接字并杀死 B
    【解决方案2】:

    如果对套接字的引用因此丢失,垃圾收集器会关闭套接字吗?

    大概吧。但是垃圾收集器可能要到下周末才能运行:你不能依赖 GC 运行,几乎永远,只是因为“嘿,java 有一个垃圾收集器”。确实如此,而且在需要之前它不会启动。它可能根本不需要。

    依靠 GC 来关闭资源是让你的 VM 因使用过多系统资源而被操作系统杀死的好方法。

    真正的问题是:导致执行者关闭的因果过程是什么?

    如果有某种“取消所有打开的连接”按钮,并且您将其实现为单行:queue.shutdown(),那么,不 - 这不是一个好主意:您现在将依靠GC 来清理那些坏的套接字。

    我假设您的可调用对象看起来像:

    Socket socket = ....; // obtained from queue
    Callable<Void> socketHandler = () -> {
        try {
           // all actual handling code is here.
        } finally {
            socket.close();
        }
        return null;
    };
    

    那么是的,这是一个问题:如果可调用对象甚至从未启动,则 finally 块将不会运行。 (如果你没有finally,你有一个更大的问题——如果在处理它的过程中发生异常,那个套接字将不会被清理!)。

    一种出路是拥有一个套接字列表,抽象出队列本身,并让该抽象具有一个关闭方法,两者都会关闭队列并且关闭每个套接字,用 try/catch 块保护每个步骤(队列关闭以及所有 socket.close 命令),以确保这些步骤之一中的单个异常不会立即停止关闭过程。

    请注意,一堆处理程序可能仍在运行,因此像这样关闭套接字“从它们下面”会导致处理程序出现异常。如果您不希望这样,请关闭队列,然后等待终止(使用 try/catch 进行保护),然后然后关闭所有套接字。

    你可以关闭一个关闭的socket,也就是noop,不需要先检查,也不需要担心关闭大量已经关闭的socket的影响。

    但要担心将 obj ref 保留到无限增长的套接字列表。一旦一个套接字完全完成,就摆脱它 - 也可以从这个精选的“如果队列终止,你需要关闭的东西”列表中。

    当然,如果导致队列提前终止的唯一进程是因为您要关闭 VM,请不要担心。套接字与 VM 一起消失。事实上,没有必要关闭队列。如果您打算结束 VM,只需.. 结束它。立即:System.shutdown(0) 是你想要的。不存在“但是..我应该要求所有的东西都很好地关闭!”这样的东西。这就是你问的方式。需要清理资源的系统大多设计得很糟糕(将它们设计为不需要在 VM 关闭时进行清理。例如,所有资源都以这种方式工作),如果必须,请注册一个关闭挂钩。

    【讨论】:

    • “需要清理资源的系统大多设计得很糟糕。” 我不得不在我最近工作的基于 Apache MINA 的服务器上添加一个 acceptor.setReuseAddress(true); 所以一个在另一个进程结束后立即启动的服务器进程在尝试保留与以前相同的服务器端口时不会抛出 "Address already in use" 异常。如果我对您的理解正确,我不确定这与您的陈述有何关系。也许我错过了向它添加一些完成代码。但我认为这样做仍然算作“我应该要求所有的东西都很好地关闭!”
    • 获取您正在写信的j.i.Socket 实例,或者可能是FileOutputStream。如果你关闭了一个虚拟机(使用 CTRL+C 或System.shutdown()),那么虚拟机会“很好地”将这些东西返回给操作系统。您将不会刷新您的 FOS(这对于 FOS 来说是无关紧要的,但如果它被缓冲的输出流包装可能是一件事),但是,然后,VM 在写入文件的中途获得了 CTRL+Ced .当然它是不完整的:用户要求这个。如果您认为这不好,唯一的选择是完全忽略 CTRL+C,这更奇怪。关键是,文件不是“永远打开”。
    • @Piovezan 听起来您的基于 apache MINA 的服务器违反了此规则,并以某种方式设法保持资源“锁定”,而 VM 关闭并没有“解锁”它。这是一个糟糕的设计,但这可能不是 apache 的错。 CTRL+C 上的套接字“很好地清理”的部分原因是操作系统也对此负责。套接字属于一个进程。进程死亡?操作系统知道清理它。糟糕的设计:ExFAT 文件系统。好的设计:HFS+、btrfs、ext3 等:有期刊的东西。
    • @rzwitserloot - 感谢您的详细回答。我假设您提出一个套接字队列,因为您希望线程 A 将套接字放入队列并尽快恢复侦听。在我的代码中,我等待线程 B 发出信号(比如 10 毫秒)。没有排队。确实,如果 A 等待,则它不听,这会影响可用性。但是,如果新线程无法跟上它们的速度,那么减慢新连接的创建速度也是有意义的。你怎么看?
    • @claude socketserver 对象仍然打开并运行;它本身就像一个小队列,.accept() 将从队列中弹出顶部连接(如果当前没有“在线”连接,则等待连接)。因此,等待线程 B 首先开始是没有问题的。听起来是个不错的前进方向:)
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2021-09-15
    • 2017-06-26
    • 2015-03-16
    • 2010-11-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多