前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >深入Hotspot源码与Linux内核理解NIO与Epoll

深入Hotspot源码与Linux内核理解NIO与Epoll

作者头像
黎明大大
发布2021-03-25 14:50:24
1.2K0
发布2021-03-25 14:50:24
举报
文章被收录于专栏:java相关资料java相关资料

1

前言

熟练掌握 BIO,NIO,AIO 的基本概念以及一些常见问题是你准备面试的过程中不可或缺的一部分,另外这些知识点也是学习 Netty 的基础吧。

2

IO模型

IO模型其实简单理解就是使用不同的通道进行接收和发送数据 ,目前JAVA总共支持三种IO模型,分别是BIO、NIO、AIO

3

IO概念区分

同步和异步概念

同步:同步就是发起一个请求后,被请求者未处理完业务逻辑之前,请求不会返回。

白话文理解:张三请求李四帮忙拿一个物品,在李四没有拿到物品之前,张三就会一直等待着,直到李四拿到物品交给了张三之后,张三才会离开。

异步:异步就是发起一个请求后,会立刻得到被请求者的响应,但是被请求者并没有返回结果,此时我们可以处理其他的请求,被请求者通常依靠事件,回调等机制来通知调用者其返回结果。

白话文理解:张三请求李四帮忙拿一个物品,并且告诉李四拿到物品之后直接来找张三,那么此时张三此刻就不用干巴巴的等着李四了,张三可以做着其他的事情,例如吃零食啊、看电视等,

直到李四拿到物品主动交到了张三手上,张三才会离开。

阻塞和非阻塞概念

阻塞:阻塞就是发起一个请求,调用者一直等待请求结果返回,也就是当前线程会被挂起,无法做其他的事情,只有当条件就绪才能继续。

白话文理解:最白话文理解就是公交车了,在公交车没到时间点之前,它是不会进行发车的,只有到点了,才会发动公交车出发。

非阻塞:非阻塞就是发起一个请求,调用者不用一直等着结果返回,可以先去干其他事情。

白话文理解:还是公交车的例子,不过这次换成了驾驶员,驾驶员没到点之前不会发送公交车,但是驾驶员可以在到点之前做些其他的事情,例如来一把紧张刺激的王者荣耀等

同步阻塞和非同步阻塞以及异步阻塞和异步非阻塞概念

这里说个小段子,相信大家就明白了

出场人物:公交车驾驶员张三(简称张三),会响铃的闹钟

1.张三干巴巴坐着公交车上,等待时间到点,才开始驾驶公交车出站。(同步阻塞)

2.张三不再傻傻的坐在公交车上等着了,它在未到点之前,先开了一把王者荣耀,且时不时的看时间到点了没。(同步非阻塞)

3.张三买了一个会响铃的闹钟,但还是干巴巴的坐着车上,等待时间到点,才开始驾驶公交车出站。(异步阻塞)

3.最后张三学聪明了,张三将闹铃设置发车时间点,到点了自动响铃,此刻张三可以任意做其他的事情了,直到闹铃响铃了,才上车驾驶公交车出站。

了解完以上的概念之后,我们在来看看几个IO模型的区别

1

BIO模型(Blocking IO)

同步阻塞IO模式,一个客户端连接对应一个线程进行处理

这种模型是单线程应用,服务端监听客户端连接,当监听到客户端连接后,会立马执行业务逻辑,在该线程请求还没处理完毕之前,后面进来的连接只能被阻塞着,什么事情都做不了。

我们看到上图就可以发现,当一个新连接被接入之后,其他的客户端连接全部都会被阻塞住,这种方式几个连接是没有问题,假如要某一个连接处理花了很长时间,只会导致客户端连接越来越多,这样服务端岂不是要炸掉么,那我们能够否寻找一个方法,将阻塞的连接与业务处理分离开来,各自处理各自的,这样当前连接的业务逻辑处理就不会影响新的请求进来了。

异步阻塞IO模式,多个客户端连接对应多个线程进行处理

这种IO模型是对上一个模型的优化,当一个新的连接被接收之后,会获取到当前的连接的socket,然后交给一个新的线程去处理,而主线程还是会继续接收新的连接,这样就能够解决同一时间只能处理一个新连接的问题,但是,明显有一个潜在的问题,小并发量的场景是没有问题的,但是一旦连接达到了几万甚至更高,那岂不是要开几万个线程去接入连接,这是个很致命的问题,所以我们需要将限制线程的数量,此时就会想到用线程池,我们来优化一下吧。

优化异步阻塞IO模式,多个客户端连接对应一个线程池进行处理

这个模型是JDK1.4之前,没有NIO的时候的一个经典Socket模型,服务端接收到客户端新连接会后,将Socket连接以及业务逻辑包装为任务提交到线程池,由线程池开始执行,同时服务端继续接收新连接!这样能够解决上一步因为线程爆炸所引发的问题,但是我们回想下线程池的的提交步骤:当核心线程池满了之后会将任务放置到队列,当队列满了之后,会占用最大线程数的数量继续开启线程,当达到最大线程数的时候开始拒绝策略! 证明我最大的并发数只有1500个,其余的都在队列里面占1024个,假设现在的连接数是1w个,并且使用的是丢弃策略,那么会有近6000的连接任务被丢弃掉,而且1500个线程,线程之间的切换也是一个特别大的开销!这是一个致命的问题!

上述的三种模型除了有上述的问题之外,还有一个特别致命的问题,他是阻塞的!

在哪里阻塞的呢?

  • 连接的时候,当没有客户端连接的时候是阻塞的!没有客户端连接的时候,线程只能傻傻的阻塞在哪里等待新连接接入!
  • 等待数据写入的时候是阻塞的,当一个新连接接入后但是不写入数据,那么线程会一直等待数据写入,直到数据写入完成后才会停止阻塞! 假设我们使用 优化后的伪异步线程模型 ,1000个连接可能只有 100个连接会频繁写入数据,剩余900个连接都很少写入,那么就会有900个线程在傻傻等待客户端写入数据,所以,这也是一个很严重的性能开销!

现在我们总结一下上述模型的问题:

1.线程开销浪费严重!

2.线程间的切换频繁,效率低下!

3.read/write执行的时候会进行阻塞!

4.accept会阻塞等待新连接

那么,我们是否有一种方案,用很少的线程去管理成千上万的连接,read/write会阻塞进程,那么就会进入到下面的模型

1

NIO模型(Non Blocking IO)

同步非阻塞IO模式就必须使用java NIO来实现了,看一段简单的代码:

public class NioServer {
    // 保存客户端连接
    static List<SocketChannel> channelList = new ArrayList<>();
    public static void main(String[] args) throws IOException, InterruptedException {
        // 创建NIO ServerSocketChannel,与BIO的serverSocket类似
        ServerSocketChannel serverSocket = ServerSocketChannel.open();
        serverSocket.socket().bind(new InetSocketAddress(9000));
        // 设置ServerSocketChannel为非阻塞
        serverSocket.configureBlocking(false);
        System.out.println("服务启动成功");
        while (true) {
            // 非阻塞模式accept方法不会阻塞,否则会阻塞
            // NIO的非阻塞是由操作系统内部实现的,底层调用了linux内核的accept函数
            SocketChannel socketChannel = serverSocket.accept();
            if (socketChannel != null) { // 如果有客户端进行连接
                System.out.println("连接成功");
                // 设置SocketChannel为非阻塞
                socketChannel.configureBlocking(false);
                // 保存客户端连接在List中
                channelList.add(socketChannel);
            }
            // 遍历连接进行数据读取
            Iterator<SocketChannel> iterator = channelList.iterator();
            while (iterator.hasNext()) {
                SocketChannel sc = iterator.next();
                ByteBuffer byteBuffer = ByteBuffer.allocate(128);
                // 非阻塞模式read方法不会阻塞,否则会阻塞
                int len = sc.read(byteBuffer);
                // 如果有数据,把数据打印出来
                if (len > 0) {
                    System.out.println("接收到消息:" + new String(byteBuffer.array()));
                } else if (len == -1) { // 如果客户端断开,把socket从集合中去掉
                    iterator.remove();
                    System.out.println("客户端断开连接");
                }
            }
        }
    }
}

上述代码我们可以看到一个关键的逻辑:serverSocketChannel.configureBlocking(false); 这里被设置为非阻塞的时候无论是 accept还是read/write都不会阻塞!具体的为什么会非阻塞,我放到文章后面说,我们看一下这种的实现逻辑有什么问题!

看这里,我们似乎的确使用了一条线程处理了所有的连接以及读写操作,但是假设我们有10w连接,活跃连接(经常read/write)只有1000,但是我们这个线程需要每次要轮询10w条数据处理,极大的消耗了CPU!

我们期待什么?期待的是,每次轮询值轮询有数据的Channel, 没有数据的就不管他,比如刚刚的例子,只有1000个活跃连接,那么每次就只轮询这1000个,其他的有读写的数据就轮询,没读写就不轮询!

多路复用模型

多路复用模型是JAVA NIO 推荐使用的经典模型,内部通过 Selector进行事件选择,Selector事件选择通过系统实现,具体流程看一段代码:

public class NioSelectorServer {
    public static void main(String[] args) throws IOException, InterruptedException {
        // 创建NIO ServerSocketChannel
        ServerSocketChannel serverSocket = ServerSocketChannel.open();
        serverSocket.socket().bind(new InetSocketAddress(9000));
        // 设置ServerSocketChannel为非阻塞
        serverSocket.configureBlocking(false);
        // 打开Selector处理Channel,即创建epoll
        Selector selector = Selector.open();
        // 把ServerSocketChannel注册到selector上,并且selector对客户端accept连接操作感兴趣
        serverSocket.register(selector, SelectionKey.OP_ACCEPT);
        System.out.println("服务启动成功");
        while (true) {
            // 阻塞等待需要处理的事件发生
            selector.select();
            // 获取selector中注册的全部事件的 SelectionKey 实例
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = selectionKeys.iterator();
            // 遍历SelectionKey对事件进行处理
            while (iterator.hasNext()) {
                SelectionKey key = iterator.next();
                // 如果是OP_ACCEPT事件,则进行连接获取和事件注册
                if (key.isAcceptable()) {
                    ServerSocketChannel server = (ServerSocketChannel) key.channel();
                    SocketChannel socketChannel = server.accept();
                    socketChannel.configureBlocking(false);
                    // 这里只注册了读事件,如果需要给客户端发送数据可以注册写事件
                    socketChannel.register(selector, SelectionKey.OP_READ);
                    System.out.println("客户端连接成功");
                } else if (key.isReadable()) {  // 如果是OP_READ事件,则进行读取和打印
                    SocketChannel socketChannel = (SocketChannel) key.channel();
                    ByteBuffer byteBuffer = ByteBuffer.allocate(128);
                    int len = socketChannel.read(byteBuffer);
                    // 如果有数据,把数据打印出来
                    if (len > 0) {
                        System.out.println("接收到消息:" + new String(byteBuffer.array()));
                    } else if (len == -1) { // 如果客户端断开连接,关闭Socket
                        System.out.println("客户端断开连接");
                        socketChannel.close();
                    }
                }
                //从事件集合里删除本次处理的key,防止下次select重复处理
                iterator.remove();
            }
        }
    }
}

相比上面的同步非阻塞IO,这里多了一个selector选择器,能够对关注不同事件的Socket进行注册,后续如果关注的事件满足了条件的话,就将该socket放回到到里面,等待客户端轮询!

NIO 有三大核心组件:Channel(通道), Buffer(缓冲区),Selector(多路复用器)

1.channel 类似于流,每个 channel 对应一个 buffer缓冲区,buffer 底层就是个数组

2.channel 会注册到 selector 上,由 selector 根据 channel 读写事件的发生将其交由某个空闲的线程处理

3.NIO 的 Buffer 和 channel 都是既可以读也可以写

NIO底层在JDK1.4版本是用linux的内核函数select()或poll()来实现,跟上面的NioServer代码类似,selector每次都会轮询所有的sockchannel看下哪个channel有读写事件,有的话就处理,没有就继续遍历,JDK1.5开始引入了epoll基于事件响应机制来优化NIO,首先我们会将我们的SocketChannel注册到对应的选择器上并选择关注的事件,后续操作系统会根据我们设置的感兴趣的事件将完成的事件SocketChannel放回到选择器中,等待用户的处理!那么它能够解决上述的问题吗?

答案肯定是可以的,因为上面的一个同步非阻塞I/O痛点在于CPU总是在做很多无用的轮询,在这个模型里被解决了!这个模型从selector中获取到的Channel全部是就绪的,后续也就是说他每次轮询都不会做无用功!

深入底层源码分析

如果要深入分析NIO的底层我们需要逐步的分析,首先,我们需要了解一种叫做select()函数的模型,它是什么呢?他也是NIO所使用的多路复用的模型之一,是JDK1.4的时候所使用的一种模型,它是epoll模型之前所普遍使用的一种模型,他的效率不高,但是当时被普遍使用,后来才会被人优化为epoll!

在上述代码中,有三个点比较主要,我这里先罗列出来,方便后面梳理逻辑,这几个点在后面源码中会逐步分析到位

  • Selector.open()
  • serverSocket.register()
  • selector.select()

接下来,我们就一起走进源码,看它最后优化的epoll模型是如何实现的。

在代码中找到Selector.open()方法,我们进入到它的内部方法中

我们看到open方法返回的是Selector类型,这个我们先不管,我们先进入到SelectorProvider的provider()方法

我们先看到图中的provider方法里面加了一个同步锁,内部有好几个逻辑判断,但都不是我们要看的重点,看到我标记的地方,它最后调用了DefaultSelectorProvider里面的create方法,这里也稍微注意下我贴的代码,这里的provider这个变量它是一个抽象类,继续跟进去看,看他内部逻辑是什么?

//这里的SelectorProvider 它是个抽象类
public abstract class SelectorProvider {
    private static final Object lock = new Object();
    private static SelectorProvider provider = null;
 
    ......
}

咦,我们看到create方法内,它返回的是WindowsSelectorProvider对象,而它这里命名是以Windows开头难道说从这个地方开始会区分系统进行执行业务逻辑?我们先保留这个疑惑,待会再来看看怎么回事

在上图中,我们知道create()方法返回是WindowsSelectorProvider类型了,再往下面跟下去已经没意义了,因为我们还无法确定它是否区分系统来执行逻辑,我们回到前面再来看看openSelector()它的这个无参构造方法实现了些啥

public static Selector open() throws IOException {
    return SelectorProvider.provider().openSelector();
}

我们看到上图中,在openSelector方法内部,它最后返回的是对象是调用的WindowsSelectorImpl的实现类,从命名上我们大概能够猜测的出它是windwos的Selector的逻辑实现了,那最后返回的也是WindowsSelectorImpl类型,也没有跟进去的必要,但是到了此处并没有找到我们想要的答案

带着这个疑惑,我们就来看看Hotspot源码里面它到底是如何实现的,我们搜索DefaultSelectorProvider这个类,至于为什么是搜索这个类,原因很简单,我们前面在出现分歧的地方是在DefaultSelectorProvider.create() 开始的,那从图中看到我这里搜索出3个同样的文件它们出现在不同的目录下,分别是solaris、windows以及macosx,那么意味着该类有三套不同的实现逻辑且我们还了解到一个信息,就是JDK它的跨平台特性在这里就也许能够证明到,因为我们平常开发的项目都是部署在linux环境中的,所以就直接点进solaris的DefaultSelectorProvider类啦

回到前面的思路,通过DefaultSelectorProvider调用的create()方法,我们就直接看到这个方法,里面有个判断指向linux系统,在linux判断内部返回了createProvider()方法且传入了一个EPollSelectorProvider类路径,好家伙,相信看到传入的是这个路径,大概就知道Epoll主题马上就要开始了,我们看到图中它的createProvider()方法就在上方,该方法很简单就是将传入的EPollSelectorProvider类路径,通过反射生成了一个新的对象返回出去,为什么而返回的是EPollSelectorProvider呢?带着这个疑惑,我们就去定位到这个类的位置

看到下图中的openSelector()方法是否有种似曾相识的感觉,对!你的感觉没错,这个方法在我们前面是有分析到,它最终定位到是windows的实现类,我这里黏上一波代码回顾一下,我们来看一下它们的区别,在windows环境下它返回的是WindowsSelectorImpl(),而在linux环境下如图中返回的是EPollSelectorImpl(),看名字知道后缀带了Impl,那么该类八九不离十就是处理Epoll模型的逻辑了,我们跟进去看一下

// 这是windows平台上看到的源码
public AbstractSelector openSelector() throws IOException {
    return new WindowsSelectorImpl(this);
}

跟进来后,先看到我方框标记的构造方法处,它里面好几行代码大部分不是我们的重点代码,关注到我方框标记的地方,它new了一个新的EPollArrayWrapper的对象,他是干嘛的呢?我们继续进入到它的内部瞧一瞧

跟进去之后,看到第一行代码,它的注释有说到,epolloCreate()方法的功能是创建epoll文件描述符,文件描述符?这是个什么鬼,我来给大家解释一下,文件描述符是linux内核为了高效管理已被打开的文件所创建的索引,用于指向被打开的文件,所有执行I/O操作的系统调用都通过文件描述符;文件描述符是一个简单的非负整数,用以表明每个被进程打开的文件。程序刚刚启动时,第一个打开的文件是0,第二个是1,以此类推。也可以理解为文件的身份ID,看完这段解释是不是还有点懵?没关系,这个地方我们先有个概念就行,如果还想深入了解的话可以自行百度一下,那我们再往下走,进入到epollCreate()这个方法,看它在哪里调用

哎呀,好家伙,这个方法居然被native修饰过了,那么意味着该方法是调用了c语言或c++语言写的程序了,不过问题不大,我们既然拿到hotspot源码了,那就能够找到它的源码所在。

搜索C的代码其实也是有技巧的,可以通过方法名下划线调用的方法名即可找到C的代码

然后我们看到第一行代码,epoll_create(256) 它返回是一个int类型,而epfd这个变量名称就是epoll file descriptor的简写,当我们再想去查看这个方法的时候,发现在代码里面是搜索不到的,因为这个其实是调用linux内核里面的函数了,都已经看到这么底层了,不看到它的这个函数的用法实属不甘心,好在linux是开源的,而我们正好可以通过linux的man命令来查看这个函数的用法

进入到linux系统,通过man epoll_create 这个命令,我们就能查看这个函数的操作手册了,看到开头那一串英文说明,说打开epoll文件描述符,那这里是不是就很好的印证了前面所说的,在linux环境下所有函数等一切皆是文件,下面这么多内容,我这里就不翻译啦,大家伙感兴趣的话,可以自己在linux系统上通过man命令查看它的手册哈

回过头来,看到我前面说的 int epfd = epoll_create(256);那它到底干嘛用的呢?

它其实就是创建了一个epoll实例,并返回一个非负数作为文件描述符,用于对epoll接口的所有后续调用。参数size代表可能会容纳size个描述符,但size不是一个最大值,只是提示操作系统它的数量级,现在这个参数基本上已经弃用了。

好啦,我们上面分析了这么多,就为了分析Selector.open()方法的作用,那我这里简单总结一下它的作用

它的作用就是在linux的内核创建了个Epoll实例,也就是创建了个多路复用器

那么接下来继续分析第二个核心方法,serverSocket.register()

看到下图,我这边就直接在windwos平台的源码走到底层了,可以看到我停留的这个方法是在WindwosSelectorImpl实现类对吧,但是对应到linux环境下的话,就是EpollSelectorImpl相信大家还有印象吧

我们在Hotspot源码中找到了Linux下的这个注册实现方法,该方法最最最核心的一处代码就是 pollWrapper.add(fd) 了,这里我解释一下这串代码的意思,fd就是socket或socketChannel,那这两个在linux环境下这两个也是作为文件描述符来操作的,pollWrapper这个在前面是有分析到的,它底层就是将它包装成类似于集合,然后我们每次来了一个新的socket或socketChannel都是添加到里面去。

总结一下serverSocket.register()方法的作用

它的作用就是将channel注册到多路复用器上

分析最后一个核心方法,selector.select()

我们进入到select()内部直接定位到底部的doSelect方法,图中它是在WindowsSelectorImpl类中,老套路我们直接跑到EpollSelectorImpl类中找对应的方法

EpollSelectorImpl的doSelect()方法,里面最重要的一行代码就是pollWrapper.poll(),那么pollWrapper前面已经出现了两次了,它底层就是类似于一个集合里面存储的是socketChannel,然后看到poll,这个很好理解就是将集合的所有数据全部取出来,那它是如何将数据取出来的呢?往下看

注意,上图中pollWrapper它是一个类,所以需要找到EPollArrayWrapper这个类,然后在该类搜索poll方法,而下图中poll方法中我标记的前两行代码是比较核心的代码,先关注这两行重点即可,它第一行代码在当前类中调用了另外一个方法,这个方法就在下面,我们看看它具体实现了什么功能

还是老样子,看到我标记的这一行,我们再跟进去epollCtl()它的方法的实现

好家伙,又是一个被native修饰的本地方法,那这个方法到底干啥的,我们只能在linux里面查看这个函数的意思了,我这就不贴图了,我先来解释这个方法的几个参数的意思

  • epfd:就是epoll对应的文件描述符
  • opcode:对应fd的操作动作,分为(EPOLL_CTL_ADD:注册新的fd到epfd中,并关联事件event;EPOLL_CTL_MOD:修改已经注册的fd的监听事件;EPOLL_CTL_DEL:从epfd中移除fd,并且忽略掉绑定的event,这时event可以为null;)
  • fd:则是我们的socketChannel
  • events:就是事件动作,分为(events有很多可选值,这里只举例最常见的几个:EPOLLIN :表示对应的文件描述符是可读的;EPOLLOUT:表示对应的文件描述符是可写的;EPOLLERR:表示对应的文件描述符发生了错误;成功则返回0,失败返回-1)

epollCtl的这个函数,我们需要了解一点关于硬件的知识,中断操作。

中断操作我们用白话文来说清楚它,就拿生活中打电话的例子来说,张三打电话给李四,那打完电话的之后是有一个挂断电话的操作对吧,那么中断操作就相当于打电话挂断的意思,在硬件中每接收到一个socketChannel,它就会发生一次中断的操作,那么这个中断操作之后就会将这次的文件描述符等信息添加到pollWrapper这个集合中。

那么来总结一下这个

使用文件描述符epfd引用的epoll实例,对目标文件描述符fd执行op操作,简单理解呢就是每次来了一个新的事件,那么该事件在进入到硬件层面的时候会触发中断操作,然后会由硬件将事件添加到rdlist就绪列表中,我们后续会直接从就绪列表中拉取事件,这样就不会导致cpu极限消耗的问题

回过头来再来看一下第二行代码,这个就很精髓了啊,当就绪列表中没有事件的时候就将线程阻塞,防止CPU空轮询,集合中有新的事件就放开线程执行事件

我这里上一个图,来总结一下NIO的它的大概执行流程

总结:NIO整个调用流程就是Java调用了操作系统的内核函数来创建Socket,获取到Socket的文件描述符,再创建一个Selector对象,对应操作系统的Epoll描述符,将获取到的Socket连接的文件描述符的事件绑定到Selector对应的Epoll文件描述符上,进行事件的异步通知,这样就实现了使用一条线程,并且不需要太多的无效的遍历,将事件处理交给了操作系统内核(操作系统中断程序实现),大大提高了效率。

1

AIO模型(Asynchronous IO)

异步非阻塞模型是用户应用只需要发出对应的事件,并注册对应的回调函数,由操作系统完成后,回调回调函数,完成具体的数据操作!先看一段代码

public class AIOServer {
    public static void main(String[] args) throws Exception {
        final AsynchronousServerSocketChannel serverChannel =
                AsynchronousServerSocketChannel.open().bind(new InetSocketAddress(9000));
        serverChannel.accept(null, new CompletionHandler<AsynchronousSocketChannel, Object>() {
            @Override
            public void completed(AsynchronousSocketChannel socketChannel, Object attachment) {
                try {
                    System.out.println("2--"+Thread.currentThread().getName());
                    // 再此接收客户端连接,如果不写这行代码后面的客户端连接连不上服务端
                    serverChannel.accept(attachment, this);
                    System.out.println(socketChannel.getRemoteAddress());
                    ByteBuffer buffer = ByteBuffer.allocate(1024);
                    socketChannel.read(buffer, buffer, new CompletionHandler<Integer, ByteBuffer>() {
                        @Override
                        public void completed(Integer result, ByteBuffer buffer) {
                            System.out.println("3--"+Thread.currentThread().getName());
                            buffer.flip();
                            System.out.println(new String(buffer.array(), 0, result));
                            socketChannel.write(ByteBuffer.wrap("HelloClient".getBytes()));
                        }
                        @Override
                        public void failed(Throwable exc, ByteBuffer buffer) {
                            exc.printStackTrace();
                        }
                    });
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            @Override
            public void failed(Throwable exc, Object attachment) {
                exc.printStackTrace();
            }
        });
        System.out.println("1--"+Thread.currentThread().getName());
        Thread.sleep(Integer.MAX_VALUE);
    }
}

AIO总结:整体逻辑就是,告诉系统我要关注一个连接的事件,如果有连接事件就调用我注册的这个回调函数,回调函数中获取到客户端的连接,然后再次注册一个read请求,告诉系统,如果有可读的数据就调用我注册的这个回调函数,当存在数据的时候,执行read回调,并写出数据。

最后来一张BIO、 NIO、 AIO 图对比:

为什么Netty使用NIO而不是AIO?

在Linux系统上,AIO的底层实现仍使用Epoll,没有很好实现AIO,因此在性能上没有明显的优势,而且被JDK封装了一层不容易深度优化,Linux上AIO还不够成熟。Netty是异步非阻塞框架,Netty在NIO上做了很多异步的封装。

我是黎明大大,我知道我没有惊世的才华,也没有超于凡人的能力,但毕竟我还有一个不屈服,敢于选择向命运冲锋的灵魂,和一个就是伤痕累累也要义无反顾走下去的心。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2021-03-19,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 黎明大大 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档