IO综述·

阻塞模式

读写数据会发生阻塞现象。当用户线程发起IO请求之后,内核会查看数据检查就绪。如果没有就绪就会等待数据就绪。而用户线程会处于阻塞状态,用户线程交出CPU。当数据就绪之后,内核会将数据拷贝到用户线程,并返回结果给用户线程,用户线程才接触block状态,data=socket.read();如果数据没有就绪,就会一直阻塞在read方法。

非阻塞IO模型

当用户线程发起一个 read 操作后,并不需要等待,而是马上就得到了一个结果。如果结果是一个 error 时,它就知道数据还没有准备好,于是它可以再次发送 read 操作。一旦内核中的数据准备 好了,并且又再次收到了用户线程的请求,那么它马上就将数据拷贝到了用户线程,然后返回。 所以事实上,在非阻塞 IO 模型中,用户线程需要不断地询问内核数据是否就绪,也就说非阻塞 IO 不会交出 CPU,而会一直占用 CPU。典型的非阻塞 IO 模型一般如下:

while(true){
data = socket.read(); if(data!= error){ 处理数据
break;
}
}

但是对于非阻塞 IO 就有一个非常严重的问题,在 while 循环中需要不断地去询问内核数据是否就绪,这样会导致 CPU 占用率非常高,因此一般情况下很少使用 while 循环这种方式来读取数据。

同步与异步

从Linux操作系统层面上了解,Linux内核会将所有的外部设备当作一个文件来操作,Linux内核使用file descriptor对本地进行读/写,同样,Linux内核便使用socket file descriptor处理与socket相关的网络读写.I/O涉及两个系统对象,一个调用它的用户进程,另外一个是系统内核

  • 用户进程调用Read方法向内核发起读请求并等待就绪
  • 内核将要读取的数据复制到文件描述符说指向的内核缓冲区.
  • 内核将数据从内核缓存区复制到用户进程空间.
同步I/O

在系统内核准备好处理数据后,还需要等待内核将数据复制到用户进程,才能处理.

异步I/O

用户进程无须关心实际I/O的操作过程,只需在I/O完成后由内核接收通知,I/O操作全部由内核进程执行.

多路复用IO模型

多路复用 IO 模型是目前使用得比较多的模型。Java NIO 实际上就是多路复用 IO。在多路复用 IO 模型中,会有一个线程不断去轮询多个 socket 的状态,只有当 socket 真正有读写事件时,才真 正调用实际的 IO 读写操作。因为在多路复用 IO 模型中,只需要使用一个线程就可以管理多个 socket,系统不需要建立新的进程或者线程,也不必维护这些线程和进程,并且只有在真正有 socket 读写事件进行时,才会使用 IO 资源,所以它大大减少了资源占用。在 Java NIO 中,是通 过 selector.select()去查询每个通道是否有到达事件,如果没有事件,则一直阻塞在那里,因此这 种方式会导致用户线程的阻塞。多路复用 IO 模式,通过一个线程就可以管理多个 socket,只有当 socket 真正有读写事件发生才会占用资源来进行实际的读写操作。因此,多路复用 IO 比较适合连 接数比较多的情况。

另外多路复用IO为何比非阻塞IO模型的效率高是因为在非阻塞IO中,不断询问Socket状态是通过用户线程去进行的,而多路复用IO中是使用内核里面进行轮询socket状态。

不过要注意的是,多路复用 IO 模型是通过轮询的方式来检测是否有事件到达,并且对到达的事件 逐一进行响应。因此对于多路复用 IO 模型来说,一旦事件响应体很大,那么就会导致后续的事件 迟迟得不到处理,并且会影响新的事件轮询。

信号驱动IO模型

在信号驱动IO模型中,当用户线程发起一个IO请求操作时候,会给对应的socket注册一个信号函数,然后用户线程会继执行,当内核数据就绪时候会发送一个信号给用户线程,用户收到信号后,遍在信号函数中调用IO读写操作来进行实际的IO请求操作。

异步IO模型

异步IO模型才是最理想的IO模型,在异步模型中,当用户线程发起read操作之后,立刻就可以开始做其他的事情,而另一方面,从内核的角度,当他受到一异步读的时候,他会立刻返回,说明read请求已经成功发起,因此不会对用户线程产生任何block。然后,内核会等待数据准备完成,然后拷贝到用户线程。当这一切都完成之后,内核会给用户线程发送一个信号,告诉他read完毕。在异步 IO 模型中,IO 操作的两个阶段都不会阻塞用户线程,这两个阶段都是由内核自动完 成,然后发送一个信号告知用户线程操作已完成。当用户线程接收到信号表示数据 已经就绪,然后需要用户线程调用 IO 函数进行实际的读写操作;而在异步 IO 模型中,收到信号 表示 IO 操作已经完成,不需要再在用户线程中调用 IO 函数进行实际的读写操作。

注意,异步 IO 是需要操作系统的底层支持,在 Java 7 中,提供了 Asynchronous IO。

Java IO包

Java Nio(Non-blocking I/O,在Java领域,也称为New I/O)

是一种同步非阻塞的I/O模型,也是I/O多路复用的基础,已经被越来越多地应用到大型应用服务器,成为解决高并发与大量连接、I/O处理问题的有效方式。

  • Channel 通道
  • Buffer 缓冲区
  • Selector

传统IO基于字符流和字节流,而NIO是面向缓冲区的。

Nio网络模型

NIO缓冲区

Java IO 面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何 地方。此外,它不能前后移动流中的数据。如果需要前后移动从流中读取的数据,需要先将它缓 存到一个缓冲区。NIO 的缓冲导向方法不同。数据读取到一个它稍后处理的缓冲区,需要时可在 缓冲区中前后移动。这就增加了处理过程中的灵活性。 IO 的各种流是阻塞的。这意味着,当一个线程调用 read() 或 write()时,该线程被阻塞,直到有 一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。 NIO 的非阻塞模式, 使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可 用时,就什么都不会获取。而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以 继续做其他的事情。 非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它 完全写入,这个线程同时可以去做别的事情。 线程通常将非阻塞 IO 的空闲时间用于在其它通道上。

channel

首先说一下 Channel,国内大多翻译成“通道”。Channel 和 IO 中的 Stream(流)是差不多一个 等级的。只不过 Stream 是单向的,譬如:InputStream, OutputStream,而 Channel 是双向。

  • FileChannel
  • DatagramChannel
  • SocketChannel
  • ServerSocketChannel

>这里看名字就可以猜出个所以然来:分别可以对应文件 IO、UDP 和 TCP(Server 和 Client)。

Buffer

Buffer,故名思意,缓冲区,实际上是一个容器,是一个连续数组。Channel 提供从文件、 网络读取数据的渠道,但是读取或写入的数据都必须经由 Buffer。

Selector

Selector 类是 NIO 的核心类,Selector 能够检测多个注册的通道上是否有事件发生,如果有事 件发生,便获取事件然后针对每个事件进行相应的响应处理。这样一来,只是用一个单线程就可 以管理多个通道,也就是管理多个连接。这样使得只有在连接真正有读写事件发生时,才会调用 函数来进行读写,就大大地减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护 多个线程,并且避免了多线程之间的上下文切换导致的开销。

用一个死循环选择就绪的事件,会执行系统调用(Linux 2.6之前是select、poll,2.6之后是epoll,Windows是IOCP),还会阻塞的等待新事件的到来。新事件到来的时候,会在selector上注册标记位,标示可读、可写或者有连接到来。

注意,select是阻塞的,无论是通过操作系统的通知(epoll)还是不停的轮询(select,poll),这个函数是阻塞的。所以你可以放心大胆地在一个while(true)里面调用这个函数而不用担心CPU空转。

一个Selector可以同时注册、监听和轮询成千上百个Channel,一个用于处理I/O的线程可以同时并发处理多个客户端的连接,具体数量取决于进程可以的最大文件句柄数.由于处理I/O线程数大幅度减少.所以缺少竞争CPU.

最常见的Selector监听时间有以下几种

  • 客户端连接服务端事件,对应的SelectorKey为OP_CONNNECT
  • 服务端接收客户端连接事件,对应的SelectorKey为OP_ACCEPT
  • 读事件,对应的SelectorKey为OP_READ
  • 写事件,对应的SelectorKey为OP_WRITE

Proactor与Reactor模型

Reactor模型

 涉及到事件分发器的两种模式称为:Reactor和Proactor。 Reactor模式是基于同步I/O的,而Proactor模式是和异步I/O相关的。在Reactor模式中,事件分发器等待某个事件或者可应用或个操作的状态发生(比如文件描述符可读写,或者是socket可读写),事件分发器就把这个事件传给事先注册的事件处理函数或者回调函数,由后者来做实际的读写操作。

在Reactor模式中,事件分离在Socket读写操作准备就绪之后,会将就绪事件传递给相应的处理器由其完成实际的读写工作.在NIO事件分离器有Selector担任,它负责查询I/O是否就绪,并在I/O就绪后调用预先注册的相关处理器进行处理

在Reactor中实现读
  • 注册读就绪事件和相应的事件处理器。
  • 事件分发器等待事件。
  • 事件到来,激活分发器,分发器调用事件对应的处理器。
  • 事件处理器完成实际的读操作,处理读到的数据,注册新的事件,然后返还控制权。
Proactor模型

在Proactor模式中,事件处理者(或者代由事件分发器发起)直接发起一个异步读写操作(相当于请求),而实际的工作是由操作系统来完成的。发起时,需要提供的参数包括用于存放读到数据的缓存区、读的数据大小或用于存放外发数据的缓存区,以及这个请求完后的回调函数等信息。事件分发器得知了这个请求,它默默等待这个请求的完成,然后转发完成事件给相应的事件处理者或者回调。举例来说,在Windows上事件处理者投递了一个异步IO操作(称为overlapped技术),事件分发器等IO Complete事件完成。这种异步模式的典型实现是基于操作系统底层异步API的,所以我们可称之为“系统级别”的或者“真正意义上”的异步,因为具体的读写是由操作系统代劳的。

在Proactor中实现读:
  • 处理器发起异步读操作(注意:操作系统必须支持异步IO)。在这种情况下,处理器无视IO就绪事件,它关注的是完成事件。
  • 事件分发器等待操作完成事件。
  • 在分发器等待过程中,操作系统利用并行的内核线程执行实际的读操作,并将结果数据存入用户自定义缓冲区,最后通知事件分发器读操作完成。
  • 事件分发器呼唤处理器。
  • 事件处理器处理用户自定义缓冲区中的数据,然后启动一个新的异步操作,并将控制权返回事件分发器。

两个模式的相同点,都是对某个I/O事件的事件通知(即告诉某个模块,这个I/O操作可以进行或已经完成)。在结构上,两者也有相同点:事件分发器负责提交IO操作(异步)、查询设备是否可操作(同步),然后当条件满足时,就回调handler;不同点在于,异步情况下(Proactor),当回调handler时,表示I/O操作已经完成;同步情况下(Reactor),回调handler时,表示I/O设备可以进行某个操作(can read 或 can write)。

Java中的IO

Java中的IO分为BIO、NIO和AIO,Java目前并不支持异步I/O,BIO对应的阻塞同步IO,NIO和AIO对应非阻塞同步I/O.

NIO

ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.configureBlocking(false);
serverChannel.socket().bind(new InetSocketAddress(port));
Selector selector = Selector.open();
serverChannel.register(selector,SelectionKey.OP_ACCEPT);

只需向通道注册,SelectionKey.OP_ACCEPT事件即可,当OP_ACCEPT事件未达到的时候,selector.select()将一直阻塞.

下面使用NIO处理同步非阻塞I/O请求的服务端代码

while(!stopped()){
    selector.select();
    Iterator<SelectionKey> selectorKeys = selector.selectedKeys().iterators();
    while(selectionKeys.hasNext()){
        SelectionKey key = selectionKeys.next();
        selectionsKeys.remove();
        if(key.isAcceptable()){
            SeverSocketChannel server  = (ServerSocketChannel) key.channel();
            SocketSocketChannel channel = server.accept();
            channel.configureBlocking(false);
            channel.register(selector,SelectionKey.OP_READ);
        }else if(key.isReadable()){
            //通过buffer处理读操作
        }
    }
}
客户端代码

SocketChannel channel = SocketChannel.open();
channel.configureBlocking(false);
Selector selector = Selector.open();
channel.connect(new InetSocketAddress(serverIP,serverPort));
channel.register(selector,SelectionKey.OP_CONNECT))

同步非阻塞的I/O

while(!stopped()){
    selector.select();
    Iterator<SelectionKey> selectorKeys = selector.selectedKeys().iterators();
    while(selectionKeys.hasNext()){
        SelectionKey key = selectionKeys.next();
        selectionsKeys.remove();
        if(key.isConnectable()){
            SocketChannel server  = (SocketChannel) key.channel();
           if(channel.isConnectionPending()){
               channel.finishConnect();
           }
            channel.configureBlocking(false);
            channel.register(selector,SelectionKey.OP_READ);
        }else if(key.isReadable()){
            //通过buffer处理读操作
        }
    }
}

AIO

Java 7推出了NIO.2 也是AIO.AIO采用Proactor模式实现I/O多路复用技术的另一种模式,它主要是用于异步I/O处理.Proactor模式与Reactor模式类似,他们都是使用事件分离器分离读/写与任务分发.相对Reactor模式更牛逼,它不用关心如何处理读/写事件,而是由操作系统将读/写执行完通知回调函数,回调方法只关系自己需要处理.Reactor模式的回调方法是在读/写操作执行之前被调用,由应用开发这者负责读写事件,而Proactor模式的回调则是在读/写操作完毕后被调用.

AIO在window和Linux有不同的实现,windows的中iocp支持真正的异步I/O,Linux的I/O模型还是使用poll或epoll,并将API封装成为异步I/O的样子.本质还是同步非阻塞的I/O

AIO有两种使用方式:

  • 简单的将来式

将来式使用java.util.concurrent.Future对结果进行访问.在提交一个I/O请求之后返回一个Future对象,然后通过检查Future的状态得到操作完成或者失败.调用Future的get是阻塞当前进程或获取消息,所以不推荐用

  • 复杂的回调式

回调是AIO的推荐方式,NIO2提供java.niochannels.CompletionHandler作为回调接口,该接口定义了completed和failed的方法,用于应用开发这自己覆盖并实现业务逻辑.I/O操作结束之后,系统会自动调用CompletionHandler的completed或failed方法来结束回调.

AIO处理同步非阻塞I/O请求的服务端核心代码

AsynchronousChannelGroup channelGroup = AsynchronousChannelGroup.withThreadPool(Executors.newFixedThreadPool(10));
final AsynchronousServerSocketChannel severChannel = AsynchronousServerSocketChannel.open(channelGroup).bind(
new InetSocketAddress(port));
severChannel.accept(null,new CompletionHandler<AsynchronousSocketChannel,Void>(){
    @Override
    public void completed(AsynchronousSocketChannel channel,Void attachement){
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        Future<Integter> future = channel.read(buffer);
        //执行业务逻辑
        serverChannel.accept(null,this);
    }
    @Override
    public void failed(Throwable exc,Void attachment){
        exec.printStackTrace();
    }
}

没有Selector的轮询需要处理,AIO采用AsynchronousChannelGroup的线程池处理事务,这些事务主要包括等待I/O事件,处理数据以及分发至各种的回调函数.通过匿名内部类的方式注册事件回调方法,覆盖completed方法用于处理I/O的后续业务逻辑,方法最后再调用accept方法接受下一次请求,覆盖failed方法用于处理I/O中产生的错误.

AIO客户端代码更加简单

AsynchronousSocketChannel channelClient = AsynchronousSocketChannel.open();
channelClient.connect(new InetSocketAddress(serverIp,serverPort)).get();
ByteBuffer buffer = ByteBuffer.allocate(1024);
channelClient.read(buffer,null, new CompletionHandler<Integer,Void>(){
    @Override
    public void completed(Integer result,Void attachment){
        //执行业务逻辑
    }
    @Override
    public void failed(Throwable exc,Void attachment){
        exc.printStackTrace();
    }
}

AIO虽然在编程接口上比起NIO简单,但是由于其使用的IO模型与NIO是一样的,由于AIO出现的事件较晚,但是没有带来多少性能提升.

总结

  • 阻塞和非阻塞、同步与异步都是I/O的不同维度
  • 同步I/O和异步I/O针对的是内核,而阻塞I/O和非阻塞I/O针对的是调用它的函数.
  • 同步I/O在实际中比较常用的,select、poll、epoll是Linux系统使用最经常使用的I/O多路复用机制.I/O多路复用机制.I/O多路复制可以监视多个描述符,一旦某个描述符读/写操作就绪,便可以通知程序进行相应的读写/写操作.他们都需要在读/写事件就绪后再进行读/写操作,内核向用户进程复制数据的过程仍然是阻塞的.但异步无须自己负责读/写操作,它负责把数据从内核复制到用户空间.
  • 总体来说判断同步I/O还是异步I/O.主要关注内核数据从内核复制到空间.
  • NIO采用Reactor模式来时实现I/O操作.

  • 18
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序猿阿三

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值