Netty Review - 探究Netty服务端主程序无异常退出的背后机制

news2025/2/27 16:45:53

文章目录

  • 概述
  • 故障场景
  • 尝试改进
  • 问题分析
    • 铺垫: Daemon线程
    • Netty服务端启动源码分析
    • 逻辑分析
  • 如何避免Netty服务端意外退出
  • 最佳实践

在这里插入图片描述


概述

在使用Netty进行服务端程序开发时,初学者可能会遇到各种问题,其中之一就是服务端意外退出的问题。这种问题可能会出现在程序启动后,没有发生任何异常的情况下,突然退出。导致这种情况发生的原因可能是代码中存在一些隐含的问题 。

接下来我们通过一个案例来演示一下这个问题

故障场景

package com.artisan.nettycase.a01exist;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;

/**
 * @author 小工匠
 * @version 1.0
 * @mark: show me the code , change the world
 */
public class ServerAbnormalExitExample {

    public static void main(String[] args) throws InterruptedException {

       // 创建两个事件循环组,bossGroup 用于接收客户端连接,workerGroup 用于处理客户端连接的读写事件
        EventLoopGroup bossGroup = new NioEventLoopGroup(1); // 用一个线程处理接收连接的事件
        EventLoopGroup workerGroup = new NioEventLoopGroup(4); // 用四个线程处理处理客户端连接的读写事件

        try {
            ServerBootstrap serverBootstrap = new ServerBootstrap();

            serverBootstrap.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class) // 设置服务端 Channel 的类型为 NIO,这里使用 NioServerSocketChannel
                    .option(ChannelOption.SO_BACKLOG, 1024) // 设置一些 TCP 的参数,这里设置了连接缓冲区大小
                    .handler(new LoggingHandler(LogLevel.INFO)) // 添加一个日志处理器,用于打印一些调试日志
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
                            ChannelPipeline pipeline = socketChannel.pipeline();
                            pipeline.addLast(new LoggingHandler(LogLevel.INFO)); // 添加一个日志处理器,用于打印客户端的请求日志
                        }
                    });
            // 同步的方式绑定服务端监听端口
            ChannelFuture future = serverBootstrap.bind(9000).sync(); // 绑定端口并启动服务端

            // 等待服务端监听端口关闭
            future.channel().closeFuture().sync();
        } finally {
            // 优雅地关闭事件循环组
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }

    }
}

运行程序,结果如下:

在这里插入图片描述

尝试改进

发现没有监听CloseFuture,于是对代码进行修改,

// 同步的方式绑定服务端监听端口
            ChannelFuture channelFuture = serverBootstrap.bind(9000).sync();

            channelFuture.channel().closeFuture().addListener(new ChannelFutureListener() {
                @Override
                public void operationComplete(ChannelFuture channelFuture) throws Exception {
                    // 模拟业务代码
                    System.out.println(Thread.currentThread().getName() + " --- " + channelFuture.channel().toString() + "链路关闭");
                }
            });

还会发生服务器套接字直接关闭、进程退出的问题 。

在这里插入图片描述


问题分析

铺垫: Daemon线程

Java中的"Daemon"线程(守护线程)是一种特殊类型的线程,其特点是当所有的非守护线程都结束时,它会自动退出。相对于普通线程(非守护线程),守护线程更像是一种服务提供者,它们在后台默默地执行一些任务,而不会阻止JVM的正常关闭。

守护线程的特点如下:

  1. 在创建线程时指定为守护线程: 可以通过Thread类的setDaemon(boolean on)方法将线程设置为守护线程,其中on参数为true表示将线程设置为守护线程,为false表示设置为普通线程。

  2. 守护线程的生命周期受主线程的影响: 当所有的非守护线程结束时,守护线程会自动退出。这意味着,如果所有的非守护线程都结束了,即使守护线程还有未完成的任务,JVM也会立即退出。

  3. 通常用于执行后台任务: 由于守护线程的特性,通常用于执行一些后台任务,比如垃圾回收器、JVM监控等。

  4. 不能持有关键资源: 由于守护线程会在JVM退出时自动终止,因此不适合持有关键资源,比如文件或者数据库连接等。因为它们可能会在守护线程尚未执行完毕时被关闭,从而导致程序出现异常。

  5. 守护线程与非守护线程的区别: 主要区别在于JVM的退出条件,非守护线程结束时不会影响JVM的退出,而守护线程结束时可能会导致JVM立即退出。

来看个代码:

public class DaemonThreadExample {
    public static void main(String[] args) {
        Thread daemonThread = new Thread(() -> {
            while (true) {
                System.out.println("Daemon Thread is running...");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        // 将线程设置为守护线程
        daemonThread.setDaemon(true);

        // 启动守护线程
        daemonThread.start();

        // 主线程休眠一段时间
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("Main thread is exiting...");
    }
}

在这里插入图片描述

我们可以知道: 守护线程是在所有非守护线程结束时自动退出的。因此,如果主线程退出,而守护线程是唯一剩下的线程,那么守护线程也会立即退出。所以,即使是守护线程,当所有非守护线程都退出时,它也会终止

故结论如下:

  1. 在Java虚拟机中,即使主线程(通常是main线程)结束,只要还有活跃的非守护线程(用户线程)在运行,虚拟机进程仍然会保持活跃状态。只有当所有的非守护线程都结束时,虚拟机的进程才会结束。

  2. 当主线程(main线程)结束时,如果此时运行的其他线程全部是守护线程(Daemon线程),那么虚拟机会停止这些守护线程并退出。但是,如果此时正在运行的其他线程中有非守护线程,那么虚拟机将等待所有的非守护线程结束后才会退出。这意味着虚拟机会等待所有的非守护线程退出,不会因为主线程结束而立即退出。


Netty服务端启动源码分析

Netty Review - 服务端channel注册流程源码解析

在这里插入图片描述

通过分析源码我们可以知道: 在Netty中,当调用bootstrap.bind(port).sync().channel()方法时,确实不是在调用方的线程(比如main线程)中执行,而是通过Netty的NioEventLoop线程执行。这是因为Netty采用了异步的事件驱动模型,在调用bind方法时,实际上是注册了一个事件监听器,在后续端口绑定完成时会通过NioEventLoop线程执行相应的逻辑。


最终的执行结果其实就是调用了Java NIOSocket的端口绑定操作:

javaChannel().socket().bind(localAddress, config.getBacklog());

在Netty中,NioEventLoop是一个事件循环,负责处理网络事件,包括接受连接、读写数据等。每个NioEventLoop都绑定了一个线程,它会不断地从事件队列中取出事件,并处理这些事件。因此,当调用bootstrap.bind(port).sync().channel()方法时,实际上是将端口绑定操作放入了NioEventLoop的事件队列中,由NioEventLoop线程来执行。这样做的好处是可以避免阻塞调用方的线程,提高了程序的并发性能。


逻辑分析

我们知道: 端口绑定操作执行完成之后,main函数就不会阻塞,如果后续没有同步代码,main线程就会退出。

那我们思考一个问题: main线程退出是否意味着JVM进程一定退出吗?

并非如此,只有所有非守护线程全部执行完成,进程才会退出。

我们通过打印线程名称来看一下

System.out.println(Thread.currentThread().getName() + " --- " + channelFuture.channel().toString() + "链路关闭");

在这里插入图片描述
当然了,也可以通过Jconsole、jvisualvm、jmc等工具来观察 。


通过对 NioEventLoop源码进行分析,可以明确如下几点。

  • NioEventLoop是非守护线程
  • NioEventLoop运行之后,不会主动退出
  • 只有调用shutdown系列方法,NioEventLoop才会退出

我们写的程序在调用Netty的shutdownGracefully()方法后,导致NioEventLoop线程退出,从而整个系统的非守护线程都执行完成,而主线程也早已执行完毕,因此JVM进程退出。

主要的原因有两点:

  1. 端口绑定操作执行非常快:尽管调用bootstrap.bind(PORT).sync()会同步阻塞主线程,等待端口绑定的结果,但是由于端口绑定操作执行非常快速,一旦完成,程序就会继续向下执行。

  2. 调用shutdownGracefully()方法:在finally块中调用了bossGroup.shutdownGracefully()workerGroup.shutdownGracefully(),这两个方法会关闭服务端的TCP连接接入线程池和处理客户端网络I/O读写的工作线程池。当这两个线程池都关闭后,NioEventLoop线程也会退出,整个系统的非守护线程执行完成。因为主线程也早已执行完毕,所以JVM进程会退出。


当我们尝试

channelFuture.channel().closeFuture().addListener(new ChannelFutureListener() {
                @Override
                public void operationComplete(ChannelFuture channelFuture) throws Exception {
                    // 模拟业务代码
                    System.out.println(Thread.currentThread().getName() + " --- " + channelFuture.channel().toString() + "链路关闭");
                }
            });

也依然无法阻值JVM退出,虽然增加了服务端连接关闭的监听事件之后,不会阻塞mainO)线程的执行,端口绑定成功之后,main线程继续向下执行,由于在finally中增加了线程池关闭代码,NioEventoop 线程主动退出,系统中没有正在运行的非守护线程了,所以JVM 进程退出。


Netty是一个异步非阻塞的通信框架,所有的IO操作都是异步的,但是为了方便使用,例如在有些场景下应用需要同步阻塞等待一些I/O操作的结果,所以提供了ChannelFuture,它主要提供以下两种能力。

  • 通过注册监听器GenericFutureListener,可以异步等待 I/O执行结果
  • 通过sync或者await,主动阻塞当前调用方的线程,等待操作结果,也就是通常
    说的异步转同步。

针对这个问题,重点在于理解Netty的异步非阻塞通信机制和ChannelFuture机制。Netty提供了ChannelFuture机制,通过注册监听器或者阻塞等待操作结果,可以实现异步转同步的操作。

因此,在使用Netty时,需要合理地处理异步操作,以充分利用Netty的优势,并避免出现意外退出的情况。


如何避免Netty服务端意外退出

通过对Netty服务端意外退出问题的分析,我们可以采取不同的修改策略来防止这种情况的发生。

  1. 监听NioServerSocketChannel的关闭事件并同步阻塞main函数:
// 监听NioServerSocketChannel的关闭事件并同步阻塞main函数
channelFuture.channel().closeFuture().sync();

这种方法会在NioServerSocketChannel关闭时阻塞主线程,直到关闭事件发生。这样可以保证主线程在服务端关闭之前不会退出,从而确保服务端的正常运行。

启动服务后,再次观察线程dump

在这里插入图片描述

搞个线程DUMP看一下

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

  1. 在链路关闭时再释放线程池和连接句柄:
channelFuture.channel().closeFuture().addListener(new ChannelFutureListener() {
    @Override
    public void operationComplete(ChannelFuture channelFuture) throws Exception {
        // 模拟业务代码
        System.out.println(Thread.currentThread().getName() + " --- " + channelFuture.channel().toString() + "链路关闭");
        boss.shutdownGracefully();
        worker.shutdownGracefully();
    }
});

这种方法会在链路关闭时异步执行释放线程池和连接句柄的操作。通过添加监听器,可以在关闭事件发生时执行相应的操作,从而避免在主线程中主动调用shutdownGracefully()方法导致的意外退出问题。


最佳实践

在实际项目中这些错误可能会导致服务端意外退出或者线程阻塞等问题。 建议如下

错误用法:这种用法会导致调用方的线程一直被阻塞,直到服务端监听句柄关闭。

  1. 初始化 Netty 服务端。
  2. 同步阻塞等待服务端端口关闭
  3. 释放 I/0 线程资源和句柄等
  4. 调用方线程被释放。

正确用法:服务端启动之后注册监听器监听服务端句柄关闭事件,待服务端关闭之后
异步调用 shutdownGracefull释放资源,这样调用方线程就可以快速返回,不会被阻塞。

  1. 初始化 Netty 服务端。
  2. 绑定监听端口。
  3. 向CloseFuture注册监听器,在监听器中释放资源
  4. 调用方线程返回。

推荐通过调用EventLoopGroupshutdownGracefully方法来优雅地关闭服务端,以完成内存队列中积压消息的处理、链路的关闭和EventLoop线程的退出。这样可以实现停机不中断业务。 (单靠Netty框架可能无法完全保证服务的可靠性,需要应用程序的其他配合来实现。)

总的来说,正确理解和使用Netty的异步特性是非常重要的。合理地利用Netty的异步非阻塞模型可以提高系统的性能和并发能力,同时避免出现意外退出和性能问题。

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1504580.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

真实案例分享:MOS管电源开关电路,遇到上电冲击电流超标

做硬件&#xff0c;堆经验。 分享一个案例&#xff1a;MOS管电源开关电路&#xff0c;遇到上电冲击电流超标&#xff0c;怎么解决的呢&#xff1f; 下面是正文部分。 —— 正文 —— 最近有一颗用了挺久的MOSFET发了停产通知&#xff0c;供应链部门找到我们研发部门&#xff0c…

KEIL 5.38的ARM-CM3/4 ARM汇编设计学习笔记10 - STM32的SDIO学习2

KEIL 5.38的ARM-CM3/4 ARM汇编设计学习笔记10 - STM32的SDIO学习2 一、问题回顾二、本次的任务三、 需要注意的问题3.1 Card Identification Mode时的时钟频率3.2 CMD0指令的疑似问题3.3 发送带参数的ACMD41时要注意时间时序和时效3.4 CPSM的指令发送问题3.5 调试过程中的SD卡的…

伪分布Hadoop的安装与部署

1.实训目标 &#xff08;1&#xff09;熟悉掌握使用在Linux下安装JDK。 &#xff08;2&#xff09;熟悉掌握使用在Linux下安装Hadoop。 &#xff08;3&#xff09;熟悉掌握使用配置SSH免密登录。 2.实训环境与软件 环境 版本 说明 Windows 10系统 64位 操作电脑配置 …

【ENVI精讲】处理专题五:基于像元二分模型的植被覆盖度反演

一、专题概述 植被覆盖度是指植被&#xff08;包括叶、茎、枝&#xff09;在地面的垂直投影面积占统计区总面积的百分比。植被覆盖度常用于植被变化、生态环境研究、水土保持、气候等方面。植被覆盖度数据来源于地理遥感生态网平台。 二、像元二分法模型 像元二分模型是一种…

什么是自动化测试?什么情况下使用?

什么是自动化测试? 自动化测试是指把以人为驱动的测试行为转化为机器执行的过程。实际上自动化测试往往通过一些测试工具或框架&#xff0c;编写自动化测试脚本&#xff0c;来模拟手工测试过程。比如说&#xff0c;在项目迭代过程中&#xff0c;持续的回归测试是一项非常枯燥…

蓝桥集训之序列

蓝桥集训之序列 核心思想&#xff1a;多路归并 每次将两个序列合并 –> 两序列n2个和中最小的n个 构成新序列 第一行都是加b1 每次在最外面的元素中取最小(优先队列) #include<iostream>#include<algorithm>#include<cstring>#include<queue>#incl…

ChatGPT 控制机器人的基本框架

过去的一年&#xff0c;OpenAI的chatGPT将自然语言的大型语言模型&#xff08;LLM&#xff09;推向了公众的视野&#xff0c;人工智能AI如一夜春风吹遍了巴黎&#xff0c;全世界都为AI而疯狂。 OpenAI ChatGPT是一个使用人类反馈进行微调的预训练生成文本模型。不像以前的模型主…

LoadBalancer (本地负载均衡)

1.loadbalancer本地负载均衡客户端 VS Nginx服务端负载均衡区别 Nginx是服务器负载均衡&#xff0c;客户端所有请求都会交给nginx&#xff0c;然后由nginx实现转发请求&#xff0c;即负载均衡是由服务端实现的。 loadbalancer本地负载均衡&#xff0c;在调用微服务接口时候&a…

云计算项目十一:构建完整的日志分析平台

检查k8s集群环境&#xff0c;master主机操作&#xff0c;确定是ready 启动harbor [rootharbor ~]# cd /usr/local/harbor [rootharbor harbor]# /usr/local/bin/docker-compose up -d 检查head插件是否启动&#xff0c;如果没有&#xff0c;需要启动 [rootes-0001 ~]# system…

VARMA(Vector Auto Regressive Moving Average) in Time Series Modelling

what is VARMA? ARIMA是针对单一变量进行建模的方法,当我们需要进行多变量时序建模时,需要使用VAR and VMA and VARMA模型。 VAR:Vector Auto-Regressive,a generalization of the auto-regressive model for multivariate time series where the time series is station…

【重新定义matlab强大系列十七】Matlab深入浅出长短期记忆神经网络LSTM

&#x1f517; 运行环境&#xff1a;Matlab &#x1f6a9; 撰写作者&#xff1a;左手の明天 &#x1f947; 精选专栏&#xff1a;《python》 &#x1f525; 推荐专栏&#xff1a;《算法研究》 #### 防伪水印——左手の明天 #### &#x1f497; 大家好&#x1f917;&#x1f91…

音视频按照时长分类小工具

应某用户的需求&#xff0c;编写了这款根据音视频时长分类小工具。 实际效果如下&#xff1a; 显示的是时分秒&#xff1a; 核心代码&#xff1a; MediaInfo MI; if (MI.Open(strPathInput.c_str()) 0){return -1;}_tstring stDuration MI.Get(stream_t::Stream_Audio,0,_T…

【Flink】Flink 的八种分区策略(源码解读)

Flink 的八种分区策略&#xff08;源码解读&#xff09; 1.继承关系图1.1 接口&#xff1a;ChannelSelector1.2 抽象类&#xff1a;StreamPartitioner1.3 继承关系图 2.分区策略2.1 GlobalPartitioner2.2 ShufflePartitioner2.3 BroadcastPartitioner2.4 RebalancePartitioner2…

手机APP测试——如何进行安装、卸载、运行?

手机APP测试——主要针对的是安卓( Android )和苹果IOS两大主流操作系统,主要考虑的就是功能性、兼容性、稳定性、易用性、性能等测试&#xff0c;今天先来讲讲如何进行安装、卸载、运行的内容。 一、App安装 1、点击运行APP安装包,检测安装包是否正常; . 2、进入[安装向导]…

Java17 --- SpringCloud之OpenFeign

目录 一、OpenFeign实现服务调用 1.1、创建openfeign微服务 二、Openfeign超时控制 2.1、全局默认配置 2.2、单个微服务配置 三、重试机制 四、替换openfeign默认的HttpClient 五、请求响应压缩 六、日志打印 一、OpenFeign实现服务调用 1.1、创建openfeign微服…

LLM长上下文外推方法

现在的LLM都集中在卷上下文长度了&#xff0c;最新的Claude3已经支持200K的上下文&#xff0c;见&#xff1a;cost-context。下面是一些提升LLM长度外推能力的方法总结&#xff1a; 数据工程 符尧大佬的最新工作&#xff1a;Data Engineering for Scaling Language Models to …

[虚拟机保护逆向] [HGAME 2023 week4]vm

[虚拟机保护逆向] [HGAME 2023 week4]vm 虚拟机逆向的注意点&#xff1a;具体每个函数的功能&#xff0c;和其对应的硬件编码的*长度* 和 *含义*&#xff0c;都分析出来后就可以编写脚本将题目的opcode转化位vm实际执行的指令 &#xff1a;分析完成函数功能后就可以编写脚本输出…

c++ primer plus 笔记 第十六章 string类和标准模板库

string类 string自动调整大小的功能&#xff1a; string字符串是怎么占用内存空间的&#xff1f; 前景&#xff1a; 如果只给string字符串分配string字符串大小的空间&#xff0c;当一个string字符串附加到另一个string字符串上&#xff0c;这个string字符串是以占用…

Spring web开发(入门)

1、我们在执行程序时&#xff0c;运行的需要是这个界面 2、简单的web接口&#xff08;127.0.0.1表示本机IP&#xff09; package com.example.demo;import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestCont…

代码学习记录15

随想录日记part15 t i m e &#xff1a; time&#xff1a; time&#xff1a; 2024.03.09 主要内容&#xff1a;今天的主要内容是二叉树的第四部分&#xff0c;主要涉及平衡二叉树的建立&#xff1b;二叉树的路径查找&#xff1b;左叶子之和&#xff1b;找树左下角的值&#xff…