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

文章目录

  • 概述
  • 故障场景
  • 尝试改进
  • 问题分析
    • 铺垫: 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的异步非阻塞模型可以提高系统的性能和并发能力,同时避免出现意外退出和性能问题。

在这里插入图片描述

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

基于机器学习的工业用电量预测完整代码数据

视频讲解: 毕业设计:算法+系统基于机器学习的工业用电量预测完整代码数据_哔哩哔哩_bilibili 界面展示: 结果分析与展示: 代码: from sklearn import preprocessing import random from sklearn.model_selection import train_test_split from sklearn.preprocessing…

oracle基础-多表关联查询 备份

一、概述 在实际应用系统开发中会设计多个数据表&#xff0c;每个表的信息不是独立存在的&#xff0c;而是若干个表之间的信息存在一定的关系&#xff0c;当用户查询某一个表的信息时&#xff0c;很可能需要查询关联数据表的信息&#xff0c;这就是多表关联查询。SELECT语句自身…

【NR技术】 3GPP支持无人机服务的关键性能指标

1 性能指标概述 5G系统传输的数据包括安装在无人机上的硬件设备(如摄像头)收集的数据&#xff0c;例如图片、视频和文件。也可以传输一些软件计算或统计数据&#xff0c;例如无人机管理数据。5G系统传输的业务控制数据可基于应用触发&#xff0c;如无人机上设备的开关、旋转、升…

数字化解决方案的设计与实现:提升业务效率与用户体验

摘要&#xff1a;随着数字化时代的到来&#xff0c;越来越多的企业和组织开始寻求数字化解决方案来提升业务效率和改善用户体验。本文将探讨数字化解决方案的设计与实现过程&#xff0c;并介绍一些关键的技术和策略。 ## 引言 在当今竞争激烈的商业环境中&#xff0c;企业和组…

深度学习图像算法工程师--面试准备(2)

深度学习面试准备 深度学习图像算法工程师–面试准备&#xff08;1&#xff09; 深度学习图像算法工程师–面试准备&#xff08;2&#xff09; 文章目录 深度学习面试准备前言一、Batch Normalization(批归一化)1.1 具体步骤1.2 BN一般用在网络的哪个部分 二、Layer Normaliza…

个人健康管理系统|基于微信小程序的个人健康管理系统设计与实现(源码+数据库+文档)

个人健康管理小程序目录 目录 基于微信小程序的个人健康管理系统设计与实现 一、前言 二、系统设计 三、系统功能设计 1、用户信息管理 2 运动教程管理 3、公告信息管理 4、论坛信息管理 四、数据库设计 1、实体ER图 五、核心代码 六、论文参考 七、最新计算机毕设…

Java对接腾讯云直播示例

首先是官网的文档地址 云直播 新手指南 可以发现它这个主要是按流量和功能收费的 价格总览 流量这里还只收下行的费用&#xff0c;就是只收观看消耗的流量费 其它的收费就是一些增值业务费 &#xff08;包括直播转码、直播录制、直播截图、直播审核、智能鉴黄、实时监播、移动直…

【JavaEE初阶 -- 多线程】

认识线程&#xff08;Thread&#xff09;Thread类及常见方法 1.认识线程&#xff08;Thread&#xff09;1.1 线程1.2 进程和线程的关系和区别1.3 Java的线程和操作系统线程的关系1.4 创建线程 2. Thread类及常用的方法2.1 Thread的常见构造方法2.2 Thread的几个常见属性2.3 启动…

java SSM厂房管理系统myeclipse开发mysql数据库springMVC模式java编程计算机网页设计

一、源码特点 java SSM厂房管理系统是一套完善的web设计系统&#xff08;系统采用SSM框架进行设计开发&#xff0c;springspringMVCmybatis&#xff09;&#xff0c;对理解JSP java编程开发语言有帮助&#xff0c;系统具有完整的源代码和数据库&#xff0c;系统主要采用B/S…

10 | MySQL为什么有时候会选错索引?

前面我们介绍过索引&#xff0c;你已经知道了在 MySQL 中一张表其实是可以支持多个索引的。但是&#xff0c;你写 SQL 语句的时候&#xff0c;并没有主动指定使用哪个索引。也就是说&#xff0c;使用哪个索引是由 MySQL 来确定的。 不知道你有没有碰到过这种情况&#xff0c;一…

Java17 --- springCloud之LoadBalancer

目录 一、LoadBalancer实现负载均衡 1.1、创建两个相同的微服务 1.2、在客户端80引入loadBalancer的pom 1.3、80服务controller层&#xff1a; 一、LoadBalancer实现负载均衡 1.1、创建两个相同的微服务 1.2、在客户端80引入loadBalancer的pom <!--loadbalancer-->&…

Qt学习-22 <QTreeWidget QTreeView>

—均为学习笔记&#xff0c;如有错误请指出 一、QTreeWidget 1. 样式展示&#xff1a; ① ② 2. 样式代码&#xff1a; ① //treeWidget树控件的使用//设置水平头//QStringList() 创建匿名对象&#xff0c;省略起名的操作ui->treeWidget->setHeaderLabels(QString…

对中国境内所有地区KFC门店基本信息的统计(简略版)

我们要获取每个地区的kfc信息就要先获取中国一共有哪些地区 中国所有城市名称获取 import requests from lxml import etreewith open(f./省份.txt, w) as fp:fp.write() with open(f./城市.txt, w) as fp:fp.write()url1http://www.kfc.com.cn/kfccda/storelist/index.aspx#…

高度塌陷问题及解决

什么情况下产生 (when 父盒子没有定义高度&#xff0c;但是子元素有高度&#xff0c;希望用子盒子撑起父盒子的高度&#xff0c;但是子盒子添加了浮动属性之后&#xff0c;父盒子高度为0 <template><div class"father"><div class"son"&…

【兔子机器人】修改GO电机id(软件方法、硬件方法)

一、硬件方法 利用上位机直接修改GO电机的id号&#xff1a; 打开调试助手&#xff0c;点击“调试”&#xff0c;查询电机&#xff0c;修改id号&#xff0c;即可。 但先将四个GO电机连接线拔掉&#xff0c;不然会将连接的电机一并修改。 利用24V电源给GO电机供电。 二、软件方…

LoadBalancer (本地负载均衡)

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

Linux文件和文件夹操作

前言&#xff1a; 相较于前面背诵的诸多内容&#xff0c;可能现在的部分就需要多多的练习了&#xff0c;难度也慢慢提升。 那就大家一起慢慢努力吧&#xff01;&#xff01;&#xff01;&#xff01;&#xff01; 目录 一、Linux目录结构 &#xff08;一&#xff09;Window…

2024年AI辅助研发趋势深度分析

2024 年 AI 辅助研发趋势 随着人工智能技术的持续发展与突破&#xff0c;2024年AI辅助研发正成为科技界和工业界瞩目的焦点。从医药研发到汽车设计&#xff0c;从软件开发到材料科学&#xff0c;AI正逐渐渗透到研发的各个环节&#xff0c;变革着传统的研发模式。在这一背景下&a…

果蔬作物疾病防治系统|基于Springboot的果蔬作物疾病防治系统设计与实现(源码+数据库+文档)

果蔬作物疾病防治系统目录 目录 基于Springboot的果蔬作物疾病防治系统设计与实现 一、前言 二、系统设计 三、系统功能设计 1、果蔬百科列表 2、公告信息管理 3、公告类型管理 四、数据库设计 1、实体ER图 五、核心代码 六、论文参考 七、最新计算机毕设选题推…

最简单的基于 FFmpeg 的内存读写的例子:内存转码器

最简单的基于 FFmpeg 的内存读写的例子&#xff1a;内存转码器 最简单的基于 FFmpeg 的内存读写的例子&#xff1a;内存转码器正文源程序结果工程文件下载参考链接 最简单的基于 FFmpeg 的内存读写的例子&#xff1a;内存转码器 参考雷霄骅博士的文章&#xff0c;链接&#xf…