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>() {@Overrideprotected 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() {@Overridepublic void operationComplete(ChannelFuture channelFuture) throws Exception {// 模拟业务代码System.out.println(Thread.currentThread().getName() + " --- " + channelFuture.channel().toString() + "链路关闭");}});
还会发生服务器套接字直接关闭、进程退出的问题 。

问题分析
铺垫: Daemon线程
Java中的"Daemon"线程(守护线程)是一种特殊类型的线程,其特点是当所有的非守护线程都结束时,它会自动退出。相对于普通线程(非守护线程),守护线程更像是一种服务提供者,它们在后台默默地执行一些任务,而不会阻止JVM的正常关闭。
守护线程的特点如下:
-
在创建线程时指定为守护线程: 可以通过
Thread类的setDaemon(boolean on)方法将线程设置为守护线程,其中on参数为true表示将线程设置为守护线程,为false表示设置为普通线程。 -
守护线程的生命周期受主线程的影响: 当所有的非守护线程结束时,守护线程会自动退出。这意味着,如果所有的非守护线程都结束了,即使守护线程还有未完成的任务,JVM也会立即退出。
-
通常用于执行后台任务: 由于守护线程的特性,通常用于执行一些后台任务,比如垃圾回收器、JVM监控等。
-
不能持有关键资源: 由于守护线程会在JVM退出时自动终止,因此不适合持有关键资源,比如文件或者数据库连接等。因为它们可能会在守护线程尚未执行完毕时被关闭,从而导致程序出现异常。
-
守护线程与非守护线程的区别: 主要区别在于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...");}
}

我们可以知道: 守护线程是在所有非守护线程结束时自动退出的。因此,如果主线程退出,而守护线程是唯一剩下的线程,那么守护线程也会立即退出。所以,即使是守护线程,当所有非守护线程都退出时,它也会终止。
故结论如下:
-
在Java虚拟机中,即使主线程(通常是main线程)结束,只要还有活跃的非守护线程(用户线程)在运行,虚拟机进程仍然会保持活跃状态。只有当所有的非守护线程都结束时,虚拟机的进程才会结束。
-
当主线程(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进程退出。
主要的原因有两点:
-
端口绑定操作执行非常快:尽管调用
bootstrap.bind(PORT).sync()会同步阻塞主线程,等待端口绑定的结果,但是由于端口绑定操作执行非常快速,一旦完成,程序就会继续向下执行。 -
调用
shutdownGracefully()方法:在finally块中调用了bossGroup.shutdownGracefully()和workerGroup.shutdownGracefully(),这两个方法会关闭服务端的TCP连接接入线程池和处理客户端网络I/O读写的工作线程池。当这两个线程池都关闭后,NioEventLoop线程也会退出,整个系统的非守护线程执行完成。因为主线程也早已执行完毕,所以JVM进程会退出。
当我们尝试
channelFuture.channel().closeFuture().addListener(new ChannelFutureListener() {@Overridepublic 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服务端意外退出问题的分析,我们可以采取不同的修改策略来防止这种情况的发生。
- 监听
NioServerSocketChannel的关闭事件并同步阻塞main函数:
// 监听NioServerSocketChannel的关闭事件并同步阻塞main函数
channelFuture.channel().closeFuture().sync();
这种方法会在NioServerSocketChannel关闭时阻塞主线程,直到关闭事件发生。这样可以保证主线程在服务端关闭之前不会退出,从而确保服务端的正常运行。
启动服务后,再次观察线程dump

搞个线程DUMP看一下



- 在链路关闭时再释放线程池和连接句柄:
channelFuture.channel().closeFuture().addListener(new ChannelFutureListener() {@Overridepublic void operationComplete(ChannelFuture channelFuture) throws Exception {// 模拟业务代码System.out.println(Thread.currentThread().getName() + " --- " + channelFuture.channel().toString() + "链路关闭");boss.shutdownGracefully();worker.shutdownGracefully();}
});
这种方法会在链路关闭时异步执行释放线程池和连接句柄的操作。通过添加监听器,可以在关闭事件发生时执行相应的操作,从而避免在主线程中主动调用shutdownGracefully()方法导致的意外退出问题。
最佳实践
在实际项目中这些错误可能会导致服务端意外退出或者线程阻塞等问题。 建议如下
错误用法:这种用法会导致调用方的线程一直被阻塞,直到服务端监听句柄关闭。
- 初始化 Netty 服务端。
- 同步阻塞等待服务端端口关闭
- 释放 I/0 线程资源和句柄等
- 调用方线程被释放。
正确用法:服务端启动之后注册监听器监听服务端句柄关闭事件,待服务端关闭之后
异步调用 shutdownGracefull释放资源,这样调用方线程就可以快速返回,不会被阻塞。
- 初始化 Netty 服务端。
- 绑定监听端口。
- 向CloseFuture注册监听器,在监听器中释放资源
- 调用方线程返回。
推荐通过调用EventLoopGroup的shutdownGracefully方法来优雅地关闭服务端,以完成内存队列中积压消息的处理、链路的关闭和EventLoop线程的退出。这样可以实现停机不中断业务。 (单靠Netty框架可能无法完全保证服务的可靠性,需要应用程序的其他配合来实现。)
总的来说,正确理解和使用Netty的异步特性是非常重要的。合理地利用Netty的异步非阻塞模型可以提高系统的性能和并发能力,同时避免出现意外退出和性能问题。

相关文章:
Netty Review - 探究Netty服务端主程序无异常退出的背后机制
文章目录 概述故障场景尝试改进问题分析铺垫: Daemon线程Netty服务端启动源码分析逻辑分析 如何避免Netty服务端意外退出最佳实践 概述 在使用Netty进行服务端程序开发时,初学者可能会遇到各种问题,其中之一就是服务端意外退出的问题。这种问…...
【兔子机器人】修改GO电机id(软件方法、硬件方法)
一、硬件方法 利用上位机直接修改GO电机的id号: 打开调试助手,点击“调试”,查询电机,修改id号,即可。 但先将四个GO电机连接线拔掉,不然会将连接的电机一并修改。 利用24V电源给GO电机供电。 二、软件方…...
Spring MVC | Spring MVC 的“核心类” 和 “注解”
目录: Spring MVC 的“核心类” 和 “注解” :1.DispatcherServlet (前端控制器)2.Controller 注解3.RequestMapping 注解3.1 RequestMapping 注解的 “使用”标注在 “方法” 上标注在 “类” 上 3.2 RequestMapping 注解的 “属性” 4.组合注解4.1 请求处理方法的…...
Python 创建PPT
本篇为如何使用Python来创建ppt文件。 创建PPT 安装必要的库 命令如下: pip install python-pptx 安装过程: 创建ppt文件 在当前目录下创建一个test的ppt文件。其中包含两页,分别使用了不同的布局。 第一页设置了标题和内容。第二页只设…...
【工具】Git的24种常用命令
相关链接 传送门:>>>【工具】Git的介绍与安装<< 1.Git配置邮箱和用户 第一次使用Git软件,需要告诉Git软件你的名称和邮箱,否则无法将文件纳入到版本库中进行版本管理。 原因:多人协作时,不同的用户可…...
rabbitmq 基本总结
rabbitmq 的基本概念 vhost、broker、producer、 consumer、 exchange、 queue、 routing key rabbitmq 常用的队列类型,工作队列(简单队列),pub/sub, routing key, topic 模式 <dependency><groupId>com.rabbitmq&l…...
7、Copmose自定义颜色和主题切换
Copmose自定义颜色和主题切换 一起颜色的设置的都是在res/values/colors里面去做颜色, 但是当使用compose的时候,抛弃了使用了ui.theme底下的Color.kt和Theme.kt 但是默认使用的是MaterialTheme主题,里面的颜色字段不能定义,因此…...
js-判断变量是否定义
if (typeof myVar undefined) {// myVar (未定义) 或 (已定义但未初始化) } else {// myVar (已定义和已初始化) } 参考 https://www.cnblogs.com/redFeather/p/17662966.html...
视频远程监控平台EasyCVR集成后播放只有一帧画面的原因排查与解决
智慧安防视频监控平台EasyCVR能在复杂的网络环境中(专网、局域网、广域网、VPN、公网等)将前端海量的设备进行统一集中接入与视频汇聚管理,平台可支持的接入协议包括:国标GB28181、RTSP/Onvif、RTMP,以及厂家的私有协议…...
Pulsar 社区周报 | No.2024.03.08 Pulsar-Spark Connector 助力实时计算
关于 Apache Pulsar Apache Pulsar 是 Apache 软件基金会顶级项目,是下一代云原生分布式消息流平台,集消息、存储、轻量化函数式计算为一体,采用计算与存储分离架构设计,支持多租户、持久化存储、多机房跨区域数据复制,…...
Redis--线程模型详解
Redis线程模型 Redis内部使用的文件事件处理器(基于Reactor模式开发的)file event handler是单线程的,所以Redis线程模型才叫单线程模型,它采用IO多路复用机制同时监听多个socket,当被监听的socket准备好执行accep、r…...
[备赛笔记]——5G大唐杯(5G考试等级考考试基础试题)
个人名片: 🦁作者简介:学生 🐯个人主页:妄北y 🐧个人QQ:2061314755 🐻个人邮箱:2061314755qq.com 🦉个人WeChat:Vir2021GKBS 🐼本文由…...
【解读】OWASP 大语言模型(LLM)安全测评基准V1.0
大语言模型(LLM,Large Language Model)是指参数量巨大、能够处理海量数据的模型, 此类模型通常具有大规模的参数,使得它们能够处理更复杂的问题,并学习更广泛的知识。自2022 年以来,LLM技术在得到了广泛的应…...
java数据结构与算法刷题-----LeetCode77. 组合
java数据结构与算法刷题目录(剑指Offer、LeetCode、ACM)-----主目录-----持续更新(进不去说明我没写完):https://blog.csdn.net/grd_java/article/details/123063846 文章目录 1. 递归实现 解题思路 这种题只能暴力求解,枚举所有可…...
网络安全运营的工作内容(附资料下载)
【推荐】最新网络安全运营方案和实践合集(共80多份).zip 网络安全运营的工作内容是一个多层次、多维度的体系,涵盖了多个关键领域以确保网络环境的稳定和安全。以下是一些主要的工作内容: 安全策略制定与实施: 制定网…...
华为OD面试分享13(2024年)
华为OD面经 二战失败选手,双非一本部门目标院校,数学与应用数学专业,无相关工作经验也没有什么拿得出手的项目。3月中旬开始重新学java(大学里有学过一个学期的java,很水)。期间经常通宵肝,学习框架、刷leedcode,可能是因为数学专业出身,数据结构和算法这一块学起来并…...
Android14之解决报错:No module named sepolgen(一百九十二)
简介: CSDN博客专家,专注Android/Linux系统,分享多mic语音方案、音视频、编解码等技术,与大家一起成长! 优质专栏:Audio工程师进阶系列【原创干货持续更新中……】🚀 优质专栏:多媒…...
数电学习笔记——逻辑函数的代数法化简
目录 逻辑函数的化简原则 与或逻辑的化简 1、吸收律(1) ( ABABA) 2、吸收律(2)(3)( AABA;AABAB) 3、多余项定律( ABACBCABAC) 4、拆项法 5、添项法 逻辑函数的化简原则 (1)逻辑函数所用的门最少 (2)各个门的输入端要少 (3)逻辑电路所用的级数要少 (4)逻辑…...
react实战——react旅游网
慕课网react实战 搭建项目问题1.按照官网在index.tsx中引入antd出错?2.typescript中如何使用react-router3.react-router3.1 V63.2 V53.3V6实现私有路由 4.函数式组件接收props参数时定义数据接口?5.使用TypeScript开发react项目:6.要使一个组…...
ChatGPT 串接到 Discord - 团队协作好助理
ChatGPT 串接到 Discord - 团队协作好助理 ChatGPT 是由 OpenAI 开发的一个强大的语言模型,本篇文章教你如何串接 Discord Bot ,协助团队在工作上更加高效并促进沟通与协作。使 ChatGPT 发挥出最大的功效,进一步提升工作效率和团队协作能力。…...
CTF show Web 红包题第六弹
提示 1.不是SQL注入 2.需要找关键源码 思路 进入页面发现是一个登录框,很难让人不联想到SQL注入,但提示都说了不是SQL注入,所以就不往这方面想了 先查看一下网页源码,发现一段JavaScript代码,有一个关键类ctfs…...
ssc377d修改flash分区大小
1、flash的分区默认分配16M、 / # df -h Filesystem Size Used Available Use% Mounted on /dev/root 1.9M 1.9M 0 100% / /dev/mtdblock4 3.0M...
《通信之道——从微积分到 5G》读书总结
第1章 绪 论 1.1 这是一本什么样的书 通信技术,说到底就是数学。 那些最基础、最本质的部分。 1.2 什么是通信 通信 发送方 接收方 承载信息的信号 解调出其中承载的信息 信息在发送方那里被加工成信号(调制) 把信息从信号中抽取出来&am…...
微服务商城-商品微服务
数据表 CREATE TABLE product (id bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 商品id,cateid smallint(6) UNSIGNED NOT NULL DEFAULT 0 COMMENT 类别Id,name varchar(100) NOT NULL DEFAULT COMMENT 商品名称,subtitle varchar(200) NOT NULL DEFAULT COMMENT 商…...
Python如何给视频添加音频和字幕
在Python中,给视频添加音频和字幕可以使用电影文件处理库MoviePy和字幕处理库Subtitles。下面将详细介绍如何使用这些库来实现视频的音频和字幕添加,包括必要的代码示例和详细解释。 环境准备 在开始之前,需要安装以下Python库:…...
数据库分批入库
今天在工作中,遇到一个问题,就是分批查询的时候,由于批次过大导致出现了一些问题,一下是问题描述和解决方案: 示例: // 假设已有数据列表 dataList 和 PreparedStatement pstmt int batchSize 1000; // …...
佰力博科技与您探讨热释电测量的几种方法
热释电的测量主要涉及热释电系数的测定,这是表征热释电材料性能的重要参数。热释电系数的测量方法主要包括静态法、动态法和积分电荷法。其中,积分电荷法最为常用,其原理是通过测量在电容器上积累的热释电电荷,从而确定热释电系数…...
Kubernetes 网络模型深度解析:Pod IP 与 Service 的负载均衡机制,Service到底是什么?
Pod IP 的本质与特性 Pod IP 的定位 纯端点地址:Pod IP 是分配给 Pod 网络命名空间的真实 IP 地址(如 10.244.1.2)无特殊名称:在 Kubernetes 中,它通常被称为 “Pod IP” 或 “容器 IP”生命周期:与 Pod …...
鸿蒙Navigation路由导航-基本使用介绍
1. Navigation介绍 Navigation组件是路由导航的根视图容器,一般作为Page页面的根容器使用,其内部默认包含了标题栏、内容区和工具栏,其中内容区默认首页显示导航内容(Navigation的子组件)或非首页显示(Nav…...
Springboot 高校报修与互助平台小程序
一、前言 随着我国经济迅速发展,人们对手机的需求越来越大,各种手机软件也都在被广泛应用,但是对于手机进行数据信息管理,对于手机的各种软件也是备受用户的喜爱,高校报修与互助平台小程序被用户普遍使用,为…...
