基于研究的代码地址

https://github.com/ljz0721cx/netty-std

了解守护线程

守护线程是运行在程序后台的线程。通常守护线程是由JVM创建,用于辅助用户活着JVM工作,GC就是一个典型的守护线程。用户也可以手动的创建守护线程。我们一般程序中使用的主线程不是守护线程,Daemon线程在java里边的定义是,如果虚拟机中只有Daemon线程运行,则虚拟机退出。
看以下例子:

public class JvmServer {
    public static void main(String[] args) throws InterruptedException {
        long starttime = System.nanoTime();
        Thread t = new Thread(new Runnable() {
            public void run() {
                try {
                    TimeUnit.DAYS.sleep(Long.MAX_VALUE);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "Daemon-T");
        //这里设置为守护线程
        t.setDaemon(true);
        t.start();
        TimeUnit.SECONDS.sleep(15);
        System.out.println("系统退出,程序执行" + (System.nanoTime() - starttime) / 1000000000 + "s");
    }
}

//main方法执行完成后jvm退出,
系统退出,程序执行15s

这里设置为守护线程,所以在执行完main方法后,进程中只留下守护线程,所以虚拟机会退出

public class JvmServer {
    public static void main(String[] args) throws InterruptedException {
        long starttime = System.nanoTime();
        Thread t = new Thread(new Runnable() {
            public void run() {
                try {
                    TimeUnit.DAYS.sleep(Long.MAX_VALUE);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "Daemon-T");
        //这里设置为非守护线程
        t.setDaemon(false);
        t.start();
        TimeUnit.SECONDS.sleep(15);
        System.out.println("系统退出,程序执行" + (System.nanoTime() - starttime) / 1000000000 + "s");
    }
}

//main方法执行完成,但是进程并没有退出
系统退出,程序执行15s

以上两个例子可以说明:

  • 虚拟机中只有当所有的非守护线程都结束时候,虚拟机才会结束
  • main线程运行结束,如果此时运行的其他线程全部是Daemon线程,JVM会使这些线程停止,同时退出。

Netty的NioEventLoop了解

public class ExitServer {
    static Logger logger= Logger.getLogger(ExitServer.class);
    public static void main(String[] args) throws InterruptedException {
        NioEventLoopGroup bossGroup = new NioEventLoopGroup(1);
        NioEventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .option(ChannelOption.SO_BACKLOG, 100)
                    .handler(new LoggingHandler(LogLevel.INFO))
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
                            ChannelPipeline c = socketChannel.pipeline();
                            c.addLast(new LoggingHandler((LogLevel.INFO)));
                        }
                    });
            ChannelFuture ch=b.bind(8080).sync(); //使用同步的方式绑定服务监听端口
        } finally {
            /*这里注释关闭线程组
             bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();*/
        }
    }
}

执行后没有任何异常,程序退出了
通过b.bind(8080).sync().channel()方法绑定服务端口,并不是在调用方的线程中执行,而是通过NioEventLoop线程执行。最终的执行结果其实是调用了java NIO Socket的端口绑定操作,端口绑定执行完成,main函数就不会阻塞,如果后续没有同步代码,main线程就会退出,JVM进启动堆栈信息如下:
在这里插入图片描述
我们期望的情况并没有发生。可以看到线程中还有nio的相关线程。
在这里插入图片描述
将上边的代码中finally中的代码注释去掉再运行,发现main方法执行完后,一会jvm进程就会退出。
调用b.bind(8080).sync().channel()之后,尽管它会同步阻塞,等待端口绑定结果,但是端口绑定执行非常快,完成后传给你续就继续执行,程序在finally里执行了bossGroup.shutdownGracefully();
和workerGroup.shutdownGracefully();它同时会关闭服务端的TCP连接接入线程池(bossGroup)和处理客户端网络I/O读写工作线程池(workerGroup),关闭之后,NioEventLoop线程退出,整个非守护线程就全部执行完毕了。此时main函数住线程已经执行完成,jvm就会退出,但是退出为什么没有出现异常呢?是由于调用了优雅退出接口shutdownGracefully,所以整个退出过程没有发生异常。
通过上边的案例我们可得出以下总结:

  • NioEventLoop是非守护线程。
  • NioEventLoop运行之后,不会主动退出。
  • 只有调用shutdown系列方法,NioEventLoop才会退出。
  • Netty是一个异步非阻塞框架

Netty同步调用

Netty是一个异步非阻塞的通信框架,所有的I/O操作都是异步的,Netty的ChannelFuture机制。但是为了更方便使用满足一些场景下使用同步阻塞等待I/O操作结果,所以提供了ChannelFuture,主要提供以下两种:

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

注册监听器GenericFutureListener方式

public class ExitServer {
    static Logger logger= Logger.getLogger(ExitServer.class);
    public static void main(String[] args) throws InterruptedException {
        NioEventLoopGroup bossGroup = new NioEventLoopGroup(1);
        NioEventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .option(ChannelOption.SO_BACKLOG, 100)
                    .handler(new LoggingHandler(LogLevel.INFO))
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
                            ChannelPipeline c = socketChannel.pipeline();
                            c.addLast(new LoggingHandler((LogLevel.INFO)));
                        }
                    });
            ChannelFuture ch=b.bind(8080);
            //这里使用注册监听器方式
            ch.channel().closeFuture().addListener(new ChannelFutureListener() {
                public void operationComplete(ChannelFuture channelFuture) throws Exception {
                    //业务逻辑处理代码,此处省略,如果这里阻塞就会一直阻塞
                    //如果不实现就会退出main
                    logger.info(channelFuture.channel().toString()+"链路关闭");
                }
            });
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}

通过sync的方式阻塞当前调用方的线程

/**
 * 程序意外退出现象
 * Created by lijianzhen1 on 2018/12/25.
 */
public class ExitServer {
    static Logger logger= Logger.getLogger(ExitServer.class);
    public static void main(String[] args) throws InterruptedException {

        NioEventLoopGroup bossGroup = new NioEventLoopGroup(1);
        NioEventLoopGroup workerGroup = new NioEventLoopGroup();

        try {
            ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .option(ChannelOption.SO_BACKLOG, 100)
                    .handler(new LoggingHandler(LogLevel.INFO))
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
                            ChannelPipeline c = socketChannel.pipeline();
                            c.addLast(new LoggingHandler((LogLevel.INFO)));
                        }
                    });

            ChannelFuture ch=b.bind(8080);
           //关闭同步调用,不会执行到finally
            ch.channel().closeFuture().sync();
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}

以上两种方式都可以看到main函数处于阻塞状态,这样shutdownGracefully就不会执行了,程序也不再会退出。
在这里插入图片描述
打印main线程堆栈,发现main函数阻塞在CloseFuture中,等待Channel关闭。
在这里插入图片描述

实际项目中优化使用

在实际项目中我们不会使用main函数直接调用Netty服务端,业务往往是通过某种容器(Tomcat,SpringBoot等)拉起进程,然后通过容器来启动初始化各种业务资源,因此不需要担心Netty服务端会意外的退出,启动netty服务端比较容易犯的错误是采用同步调用netty,导致初始化netty服务端的业务线程被阻塞,这种方式会导致调用方线程一直被阻塞,直到服务端监听句柄关闭。举例如下:
避免使用下边的方式。

  • 初始化netty服务端
  • 同步阻塞等待服务端口关闭
  • 释放I/O资源和句柄等
  • 调用方线程被释放

以上没有发挥异步的优势,正确使用如下:

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

具体代码案例

public class ExitServer {
    static Logger logger = Logger.getLogger(ExitServer.class);

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

        final NioEventLoopGroup bossGroup = new NioEventLoopGroup(1);
        final NioEventLoopGroup workerGroup = new NioEventLoopGroup();

        try {
            ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .option(ChannelOption.SO_BACKLOG, 100)
                    .handler(new LoggingHandler(LogLevel.INFO))
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
                            ChannelPipeline c = socketChannel.pipeline();
                            c.addLast(new LoggingHandler((LogLevel.INFO)));
                        }
                    });

            ChannelFuture ch = b.bind(8080).sync();
            ch.channel().closeFuture().addListener(new ChannelFutureListener() {
                @Override
                public void operationComplete(ChannelFuture future) throws Exception {
                    //业务逻辑处理代码,此处省略...
                    logger.info(future.channel().toString() + " 链路关闭");
                    bossGroup.shutdownGracefully();
                    workerGroup.shutdownGracefully();
                }
            });
            //链路关闭触发closeFuture的监听,等待服务关闭之后异步调用优雅释放资源,这样线程就不会阻塞
            ch.channel().close();
        } finally {
        //这里将操作放在监听器中
            //bossGroup.shutdownGracefully();
            //workerGroup.shutdownGracefully();
        }
    }
}

系统退出时,建议通过调用EventloopGroup的shutdownGracefully来完成内存队列中积压消息的处理,链路关闭和EventLoop线程的退出,实现停机不中断业务。也就是所谓的优雅停机。

Netty优雅退出机制

强制退出对软件来说就好比服务器突然断电,会导致一系列不确定的问题。

  • 比如缓存中的数据还没有持久化到硬盘中,导致数据丢失了。 正在进行文件写操作,没有更新完成,突然停止,导致文件损坏。
  • 线程的消息队列中收到的请求消息还没来得及处理,导致请求消息丢失。
  • 数据库操作已经完成,例如账户余额更新,准备返回应答消息给客户端时,消息在发生队列突然停止没有返回。
  • 句柄资源没有及时释放等其他问题。

Java优雅退出通常是通过注册Jdk的ShutsownHook来实现,当系统接收到退出指令时,首先标记系统处于退出状态,不再接收新的消息,然后将积压的消息处理完,最后调用资源回收将接口资源收回。
通过JDK的ShutdownHook实现优雅退出代码如下:

public class JdkShutDown {
    public static void main(String[] args) throws InterruptedException {
        Runtime.getRuntime().addShutdownHook(new Thread(()->  {
            System.out.println("开始执行shutdown");
            System.out.println("jdk通过 ShutdownHook 优雅关闭");
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("结束执行shutdown");
        }));
        TimeUnit.SECONDS.sleep(7);
        System.exit(0);
    }
}

//执行调用结果
开始执行shutdown
jdk通过 ShutdownHook 优雅关闭
结束执行shutdown
Process finished with exit code 0

除了注册ShutdownHook,还可以通过监听信号量并注册SingalHandler的方式实现优雅退出。

public class JvmSignalHandler {
    public static void main(String[] args) {
    //这里我使用的mac,如果使用的linux系统使用跟进TERM,相当于执行kill pid。不是强制刹死,如果你使用的eclipse和idea时候点一下红按钮一样的效果,记住不要点两下。
        Signal sig = new Signal("INT"); 
        Signal.handle(sig,(s)->{
            System.out.println("signal handle start ... ");
            try {
                TimeUnit.SECONDS.sleep(Integer.MAX_VALUE);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        new Thread(()->{
            try {
                System.out.println("main中的线程执行");
                TimeUnit.SECONDS.sleep(Integer.MAX_VALUE);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"Daemon-T").start();
    }
}

启动后并没有发现SIGINT handler的线程
点击关闭后,看到出现了SIGINT handler的线程,关闭停止时候触发。
在这里插入图片描述
具体阻塞线程的堆栈信息如下。
在这里插入图片描述
放入ShutdownHook看看执行的效果。

public class JvmSignalHandler {
    public static void main(String[] args) {
        Signal sig = new Signal("INT");
        Signal.handle(sig,(s)->{
            System.out.println("signal handle start ... ");
            try {
                TimeUnit.SECONDS.sleep(Integer.MAX_VALUE);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        Runtime.getRuntime().addShutdownHook(new Thread(()->{
            System.out.println("shutdownHook excute start ...");
            System.out.println("Netty NioEventLoopGroup shutdownGracefully ...");
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },""));

        new Thread(()->{
            try {
                System.out.println("main中的线程执行");
                TimeUnit.SECONDS.sleep(Integer.MAX_VALUE);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"Daemon-T").start();
    }
}

重复上边的操作,看到执行的结果并没有什么变化,由于SignalHander发生了阻塞,导致ShutdownHook无法执行,因此没有打印ShutdownHook执行相关日志。如果SignalHander执行操作比较耗时,建议异步或者放到ShutdownHook中。

Netty优雅退出

Netty优雅退出的接口和总的入口是EventLoopGroup,调用它的shutdownGracefully方法,代码就是之前使用过的,主要有以下几个方面来保证Netty的优雅退出。

  1. 不接收新的处理消息,将线程设置为ST_SHUTING_DOWN。
  2. 退出前将发送队列中尚未发送或者正在发送的消息处理完毕,把已经到期或在退出超时之前到期的定时任务执行完成,把用户注册到NIO线程的退出Hook任务执行完成。
  3. 所有Channel的释放,多路复用器的注册和关闭,所欲队列和定时任务的清空取消,最后是EventLoop线程退出。
  4. jvm的shutdownHook被触发之后,调用所有EventLoopGroup实例的shutdownGracefully方法进行优雅退出。由于Netty自身对优雅退出有完美支持。
Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐