Java NIO

了解java的NIO,需要先了解同步异步以及阻塞非阻塞的概念,同步/异步,阻塞/非阻塞

NIO就是采用的同步非阻塞这种组合方式。或简单一点,采用的是IO复用的策略,可以使用一个线程管理多个IO连接。

BIO

常见使用方式

传统的BIO是同步阻塞的方式,因此,在服务器中常见的使用方式是:

  • 来一个请求创建一个线程,阻塞的等待网络IO的数据。
  • 使用一个线程池,来一个请求就从线程池里取出来一个线程,阻塞的等待网络IO的数据。

两种方式的图例:

BIO面临的问题

上面的方案可能会出现的问题是

  • 针对第一种方式,如果短时间内qps过高,可能会导致线程数过多,拖垮服务器。
  • 针对第二种方式,现在一般用的http1.1支持长连接,若系统中有大量的长连接没有释放,依然在阻塞的等待网络IO,就会导致线程池资源慢慢被消耗调,最终可能导致线程池满无法提供服务。

总结一下,上述两点问题的原因,其共同点是可能会有很多的空闲线程阻塞的等待IO,导致服务器以各种表现形式没有办法继续对外提供服务。

NIO

NIO的IO多路复用

因此,如果是同步非阻塞的方式,可以只需要一个线程,管理多个IO连接。一旦有连接可以读/写,才开启一个线程进行读/写、执行相应的操作。如下:

Java中的NIO

原理接说到这里,下面看一下jdk中NIO的实现和用法。jdk中的NIO的实现,主要几个部分是Channel(通道),Buffer(缓冲区),Selector(选择器)。

  • Channel提供从文件、网络读取数据的通道,但是读取或写入的数据都必须经由Buffer。通道是双向的,通过一个Channel既可以进行读,也可以进行写
  • Buffer,故名思意,缓冲区,实际上是一个容器,是一个连续数组。服务端接收数据必须通过Channel将数据读入到Buffer中,然后再从Buffer中取出数据来处理。
  • Selector类是NIO的核心类,Selector能够检测多个注册的通道上是否有事件发生,如果有事件发生,便获取事件然后针对每个事件进行相应的响应处理。

在一个基于NIO的IO多路复用的具体应用场景中,它们之间的关系可能是这样的:

其中,一个线程管理一个Selector,而一个Selector管理多个Channel,被管理的Channel需要在该Selector上注册自己感兴趣的事件,如Accept,Read,Write等。

每个Channel对应一个缓冲区Buffer,每次Channel中有数据可以读写的时候,就读写到缓冲区中。然后程序再对缓冲区进行操作。

这样一来,只是用一个单线程就可以管理多个通道,也就是管理多个连接。这样使得只有在连接真正有读写事件发生时,才会调用相应的方法或者线程来进行读写、操作,大大地减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程,并且避免了多个空闲连接的多线程之间的上下文切换导致的开销。

Selector

既然NIO是非阻塞,其实就是把阻塞的位置从系统的CPU层面提到了程序层面,那么当Channel中注册的感兴趣的事件就绪时,Selector需要通过某种策略得知Channel数据已经就绪,可以采用轮询、事件驱动等方式。这里就封装成了Selector的select方法,返回值是已经就绪的通道的数量。

当Selector得知有通道对其感兴趣的事件就绪时,就取出所有已经就绪的通道,进行读写或者其它操作。Selector的 selectedKeys()方法就封装了取出所有就绪的通道的事件,返回值是一个SelectionKey的集合。SelectionKey中封装了一个Channel与selector的对应关系、Channel感兴趣的事件、Channel哪种事件已经就绪的判断(isReadable、isWritable等)。

Selector的工作方式

看一下Selector的工作方式:

流程总结如下:

  • 通过 Selector.open() 打开一个 Selector.
  • 将 Channel 注册到 Selector 中, 并设置需要监听的事件(interest set)
  • 不断重复:
    • 调用 select() 方法,阻塞获取到就绪通道
    • 调用 selector.selectedKeys() 获取 selected keys
    • 迭代每个selected keys,对每个 selected key:
    1. 从 selected key 中获取 对应的 Channel 和附加信息(如果有的话)
    2. 判断是哪些 IO 事件已经就绪了, 然后处理它们. 如果是 OP_ACCEPT 事件, 则获取 SocketChannel, 并将它设置为 非阻塞的, 然后将这个 Channel 注册到 Selector 中.,如果是读/写事件,则进行读写操作。
    3. 根据需要更改 selected key 的监听事件.
    4. 将已经处理过的 key 从 selected keys 集合中删除.

需要注意的一点是,图中第4步中的select方法,有几个重载的方法:

select()   阻塞到至少有一个通道在你注册的事件上就绪了。

select(long timeout)    和select()一样,但最长阻塞事件为timeout毫秒。

selectNow()  非阻塞,立即返回结果,如果没有已就绪事件,直接返回0。

IO多路复用

由一个线程管理一个Selector,一个Selector可以管理多个通道Channel。当调用Selector的select方法时,会阻塞的等待操作系统返回已经就绪的IO通道。这里用到的技术是IO多路复用,从而实现了同步非阻塞,解决了一个请求一个线程一直在阻塞的问题。因为一个selector线程管理了多个连接、通道,select一旦拿到有准备就绪的通道,无论是在本线程内对其做读写操作,还是交给一个其他线程去做读写操作,这个时候Selector所在的线程其实一直是可用的,并没有因为其他通道还未就绪而一直空闲。

同步

因为我们的Selector线程是去主动问操作系统有没有IO已经就绪,若就绪则进行读写(用户空间↔内核空间数据copy),而不是操作系统把数据准备好之后(用户空间↔内核空间数据copy完成)再来通知我们的程序。所以说这里的IO是同步的。

非阻塞

因为Selector线程对于每一个通道的数据,并没有等待数据就绪,而是直接返回,所以这里的IO是非阻塞的。上面提到的阻塞等待,等待的是这个Selector所管理的所有通道。也因此一个Selector线程可以管理多个IO通道。

综上所述,Java的NIO是以Selector为核心的,基于同步非阻塞的IO多路复用。

OS的IO多路复用

那么,Selector是如何得知哪些通道是就已经就绪了呢?这里涉及到的系统调用是select,poll,epoll。既然我们的程序使用了IO多路复用实现了一个线程管理多个IO,那么操作系统告诉我们已就绪的IO通道时,底层是否也采用了IO多路复用呢?操作系统的IO多路复用(IO multiplexing)就是我们说的select,poll,epoll,有些地方也称这种IO方式为event driven IO。select/epoll的好处就在于单个process就可以同时处理多个网络连接的IO。它的基本原理就是select,poll,epoll这个function会不断的轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程。

所以总结来说,就是我们的用户线程使用IO多路复用,管理多个IO通道,一旦有通道就绪,就进行读写。而我们的操作系统同样采用了IO多路复用,一旦有socket数据准备就绪,就通知我们的的用户线程。

select()的实现

翻一番Selector实现类的源码,select()方法其实还是调用了select(long timeout)方法。

public int select() throws IOException {  
    return this.select(0L);  
}  
  
  
public int select(long timeout) throws IOException {  
    if (timeout < 0L) {  
        throw new IllegalArgumentException("Negative timeout");  
    } else {  
        return this.lockAndDoSelect(timeout == 0L ? -1L : timeout);  
    }  
}  
  
  
public int selectNow() throws IOException {  
    return this.lockAndDoSelect(0L);  
}  

看得出来,select阻塞获取操作系统就绪通道的关键的实现在于lockAndDoSelect方法中:

private int lockAndDoSelect(long var1) throws IOException {  
    synchronized(this) {  
        if (!this.isOpen()) {  
            throw new ClosedSelectorException();  
        } else {  
            Set var4 = this.publicKeys;  
            int var10000;  
            synchronized(this.publicKeys) {  
                Set var5 = this.publicSelectedKeys;  
                synchronized(this.publicSelectedKeys) {  
                    var10000 = this.doSelect(var1);  
                }  
            }  
  
            return var10000;  
        }  
    }  
}  

加了两个锁,然后会调用一个doSelect方法。doSelect方法由子类实现,有PollSelectorImpl、EPollSelectorImpl。他们实现doSelect时分别调用了本地方法poll0、epollWait,分别对应操作系统的poll、epoll策略。

在调用Selector的open方时,就已经根据操作系统、内核版本决定了采用哪种IO复用策略,简单看一下sun.nio.ch.DefaultSelectorProvider#create里Selector的创建:

public static SelectorProvider create() {  
    String var0 = (String)AccessController.doPrivileged(new GetPropertyAction("os.name"));  
    if (var0.equals("SunOS")) {  
        return createProvider("sun.nio.ch.DevPollSelectorProvider");  
    } else {  
        return (SelectorProvider)(var0.equals("Linux") ? createProvider("sun.nio.ch.EPollSelectorProvider") : new PollSelectorProvider());  
    }  
}  

如果是Linux系统的话,使用的是操作系统的epoll 的策略

对于操作系统来说:

epoll:如果有IO已经就绪,会给用户线程返回所有就绪的事件,可以对这个就绪的IO通道进行读写。

poll:得到有就绪的IO时,需要遍历去查询哪些IO是已就绪的,然后返回给用户线程去读写。

参考文章:

Java NIO系列教程

Java NIO:NIO概述

Java IO & NIO & NIO2

Java 网络 IO 模型

Java NIO系列教程(六) 多路复用器Selector

Java 网络IO编程总结(BIO、NIO、AIO均含完整实例代码)

Java NIO 反应堆模式简单模型

Java NIO(7): Epoll版的Selector

Linux IO模式及 select、poll、epoll详解

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏企鹅号快讯

每日一学之socket编程(四)

图片来自于百度图片 NIO 专门为服务器而设计的API。能极大的提高IO的性能,避免使用大量线程。虽然是为了服务器而设计,但是依然可以被使用在客户端。 Chan...

2020
来自专栏纯洁的微笑

springboot(十二):springboot如何测试打包部署

有很多网友会时不时的问我,spring boot项目如何测试,如何部署,在生产中有什么好的部署方案吗?这篇文章就来介绍一下spring boot 如何开发、调试...

4366
来自专栏Spark学习技巧

Kafka源码系列之通过源码分析Producer性能瓶颈

Kafka源码系列之通过源码分析Producer性能瓶颈 本文,kafka源码是以0.8.2.2,原因是浪尖一直没对kafka系统进行升级。主要是java的ka...

3066
来自专栏匠心独运的博客

消息中间件—RabbitMQ(集群监控篇1)

摘要:任何没有监控的系统上线,一旦在生产环境发生故障,那么排查和修复问题的及时性将无法得到保证

2943
来自专栏java学习

数据库连接池C3P0,DBCP教程详解示例

连接池 实际开发中“获得连接”或“释放资源”是非常消耗系统资源的两个过程,为了解决此类性能问题,通常情况我们采用连接池技术,来共享连接Connection。...

9256
来自专栏CSDN技术头条

一组 Redis 实际应用中的异常场景及其根因分析和解决方案

在上一场 Chat《基于 Redis 的分布式缓存实现方案及可靠性加固策略》中,我已经较为全面的介绍了 Redis 的原理和分布式缓存方案。如果只是从“会用”的...

3903
来自专栏JavaEdge

GET和POST到底啥区别???

最普遍的答案 我一直就觉得GET和POST没有什么除了语义之外的区别,自打我开始学习Web编程开始就是这么理解的。 可能很多人都已经猜到了,他要的答案是:

1142
来自专栏技术栈大杂烩

Linux: linux 匿名管道

相信很多在linux平台工作的童鞋, 都很熟悉管道符 '|', 通过它, 我们能够很灵活的将几种不同的命令协同起来完成一件任务.就好像下面的命令:

2272
来自专栏aoho求索

基于可靠消息方案的分布式事务(四):接入Lottor服务

在上一篇文章中,通过Lottor Sample介绍了快速体验分布式事务Lottor。本文将会介绍如何将微服务中的生产方和消费方服务接入Lottor。

2891
来自专栏分布式系统进阶

Kafka的日志管理模块--LogManagerKafka源码分析-汇总

a. 如果kafka进程是优雅干净地退出的,会创建一个名为.kafka_cleanshutdown的文件作为标识; b. 启动kafka时, 如果不存在该文件...

1861

扫码关注云+社区

领取腾讯云代金券