回顾

上一章我们介绍了操作系统层面的 IO 模型。

  • 阻塞 IO 模型。
  • 非阻塞 IO 模型。
  • IO 复用模型。
  • 信号驱动 IO 模型(用的不多,知道个概念就行)。
  • 异步 IO 模型。

并且介绍了 IO 多路复用的底层实现中,select,poll 和 epoll 的区别。

几个概念

我们在这里在强调一下几个概念。

一个 IO 操作的具体步骤:

对于操作系统来说,进程是没有直接操作硬件的权限的,所以必须请求内核来帮忙完成。

  • 等待数据准备好,对于一个套接字上得操作,这一步骤关系到数据从网络到达,并将其复制到内核某个缓冲区。
  • 将数据从内核缓冲区复制到进程缓冲区。

同步和异步的区别在于第二个步骤是否阻塞,如果从内核缓冲区复制到用户缓冲区的过程阻塞,那么就是同步 IO,否则就是异步 IO。所以上面提到的前四种 IO 模型都是同步 IO,最后一种是异步 IO。

阻塞和非阻塞的区别在于第一步,发起 IO 请求是否会被阻塞,如果阻塞直到完成那么就是传统的阻塞 IO,否则就是非阻塞 IO。所以上面提到的第一种 IO 模型是阻塞 IO,其余的都是非阻塞 IO。

Java IO API

介绍完操作系统层面的 IO 模型,我们来看看,Java 提供的 IO 相关的 API。

Java 中提供三种 IO 操作的 API,阻塞 IO(BIO,同步阻塞),非阻塞 IO(NIO,同步非阻塞)和异步 IO (AIO,异步非阻塞)。

Java 中提供的 IO 有关的 API,在文件处理的时候,其实是依赖操作系统层面的 IO 操作实现的。比如在 Linux 2.6 以后,Java 中的 NIO 和 AIO 都是通过 epoll(前面讲过的,IO 多路复用) 来实现的。而在 windows 上,AIO 是通过 IOCP 来实现的。

可以把 Java 中的 BIO,NIO 和 AIO 理解为是 Java 语言对操作系统的各种 IO 模型的封装。程序员在使用这些 API 的时候,不需要关心操作系统层面的知识,只需要使用 Java API 就可以了。

Java BIO NIO 与 AIO

  1. BIO 就是传统的 java.io 包,它是基于流模型实现的,交互方式是同步阻塞,也就是在读取或者写入输入输出流的时候,在读写动作完成之前,线程会一直阻塞在那里。它的效率比较低,容易成为性能瓶颈。

  2. NIO 是 Java 1.4 引入的 java.nio 包,提供了 Channel,Buffer,Selector 等工具类,底层依赖与 IO 多路复用模型,基于 epoll 实现(根据操作系统来看)。同步非阻塞模式。

  3. AIO 是 Java 1.7 引入的包,是 NIO 的升级版本,提供了异步非阻塞的 IO 操作方式,所以人们叫它 AIO,异步 IO 是基于事件回调机制实现的,也就是应用操作之后会直接返回,不会阻塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续操作。底层也是依赖于 IO 多路复用模型,基于 epoll 实现,异步非阻塞模式。

从代码看 BIO NIO 于 AIO 的区别

  • 传统的 Socket 实现

    //服务端
    ServerSocket serverSocket = ......
    serverSocket.bind(8899); while(true){
    Socket sokcet = serverSocket.accept(); //阻塞方法
    new Thread(socket);
    run(){
    socket.getInputStream();
    ....
    ....
    }
    } //客户端
    Socket socket = new Socket("localhost",8899);
    socket.connect(); 8899 是用于客户端向服务端发起连接的端口号,并不是传递数据的端口号,服务端会根据每个连接也就是 Socket 选择一个端口与客户端进行通信。

    在 Java 中,线程的实现是比较重量级的,所以线程的启动和销毁是很消耗服务器资源的,即使使用线程池来实现,使用上述传统的 Socket 方式,当连接数急剧上升也会带来性能瓶颈,原因是线程的上下文切换开销会在高并发的时候体现的很明显,并且以上方式是同步阻塞,性能问题在高并发的时候会体现的尤为明显。

  • NIO 多路复用

    Java new IO 底层是基于 IO 多路复用模型实现的。NIO 是利用了单线程轮训事件的机制,通过高效地地位就绪的 Channel,来决定做什么,仅仅 select 阶段是阻塞的,可以避免大量的客户端连接时,频繁切换线程带来的问题,应用的扩展能力有了非常大的提高。

    // NIO 多路复用
    ThreadPoolExecutor threadPool = new ThreadPoolExecutor(4, 4,
    60L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
    threadPool.execute(new Runnable() {
    @Override
    public void run() {
    try (Selector selector = Selector.open();
    ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();) {
    serverSocketChannel.bind(new InetSocketAddress(InetAddress.getLocalHost(), port));
    serverSocketChannel.configureBlocking(false);
    serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
    while (true) {
    selector.select(); // 阻塞等待就绪的Channel
    Set<SelectionKey> selectionKeys = selector.selectedKeys();
    Iterator<SelectionKey> iterator = selectionKeys.iterator();
    while (iterator.hasNext()) {
    SelectionKey key = iterator.next();
    try (SocketChannel channel = ((ServerSocketChannel) key.channel()).accept()) {
    channel.write(Charset.defaultCharset().encode("你好,世界"));
    }
    iterator.remove();
    }
    }
    } catch (IOException e) {
    e.printStackTrace();
    }
    }
    }); // Socket 客户端(接收信息并打印)
    try (Socket cSocket = new Socket(InetAddress.getLocalHost(), port)) {
    BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(cSocket.getInputStream()));
    bufferedReader.lines().forEach(s -> System.out.println("NIO 客户端:" + s));
    } catch (IOException e) {
    e.printStackTrace();
    }
    1. 通过 Selector.open() 创建一个 selector,作为类似调度员的角色。
    2. 创建一个 ServerSocketChannel,并且像 selector 注册,通过指定 SelectionKey.OP_ACCEPT,告诉调度员,它关注的是新的连接请求。
    3. Selector 阻塞在 select 操作,当有 channel 发生接入请求,就会被唤醒。

  • AIO 版的 Socket 实现

    // AIO线程复用版
    Thread sThread = new Thread(new Runnable() {
    @Override
    public void run() {
    AsynchronousChannelGroup group = null;
    try {
    group = AsynchronousChannelGroup.withThreadPool(Executors.newFixedThreadPool(4));
    AsynchronousServerSocketChannel server = AsynchronousServerSocketChannel.open(group).bind(new InetSocketAddress(InetAddress.getLocalHost(), port));
    server.accept(null, new CompletionHandler<AsynchronousSocketChannel, AsynchronousServerSocketChannel>() {
    @Override
    public void completed(AsynchronousSocketChannel result, AsynchronousServerSocketChannel attachment) {
    server.accept(null, this); // 接收下一个请求
    try {
    Future<Integer> f = result.write(Charset.defaultCharset().encode("你好,世界"));
    f.get();
    System.out.println("服务端发送时间:" + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
    result.close();
    } catch (InterruptedException | ExecutionException | IOException e) {
    e.printStackTrace();
    }
    } @Override
    public void failed(Throwable exc, AsynchronousServerSocketChannel attachment) {
    }
    });
    group.awaitTermination(Long.MAX_VALUE, TimeUnit.SECONDS);
    } catch (IOException | InterruptedException e) {
    e.printStackTrace();
    }
    }
    });
    sThread.start(); // Socket 客户端
    AsynchronousSocketChannel client = AsynchronousSocketChannel.open();
    Future<Void> future = client.connect(new InetSocketAddress(InetAddress.getLocalHost(), port));
    future.get();
    ByteBuffer buffer = ByteBuffer.allocate(100);
    client.read(buffer, null, new CompletionHandler<Integer, Void>() {
    @Override
    public void completed(Integer result, Void attachment) {
    System.out.println("客户端打印:" + new String(buffer.array()));
    } @Override
    public void failed(Throwable exc, Void attachment) {
    exc.printStackTrace();
    try {
    client.close();
    } catch (IOException e) {
    e.printStackTrace();
    }
    }
    });
    Thread.sleep(10 * 1000);

    AIO 就是在 NIO 的基础上提供了回调函数。

NIO 中的重要概念

零拷贝

我们读取磁盘文件读取到内存中,以流的形式发送或者传输,这种形式我们使用的太多,太多了。我们可以 new InputStream 指向一个文件,读取完毕后在写到目标中,这样整个流程就结束了。

一个从磁盘文件读取并且通过socket写出的过程,对应的系统调用如下:

File.read(file, buf, len);
Socket.send(socket, buf, len);

  1. 程序使用read()系统调用。系统由用户态转换为内核态(第一次上线文切换),磁盘中的数据有DMA(Direct Memory Access)的方式读取到内核缓冲区(kernel buffer)。DMA过程中CPU不需要参与数据的读写,而是DMA处理器直接将硬盘数据通过总线传输到内存中。
  2. 由于应用程序无法读取内核地址空间的数据,如果应用程序要操作这些数据,必须把这些内容从读取缓冲区拷贝到用户缓冲区。系统由内核态转换为用户态(第二次上下文切换),当程序要读取的数据已经完成写入内核缓冲区以后,程序会将数据由内核缓存区,写入用户缓存区,这个过程需要CPU参与数据的读写。
  3. 程序使用write()系统调用。系统由用户态切换到内核态(第三次上下文切换),数据从用户态缓冲区写入到网络缓冲区(Socket Buffer),这个过程需要CPU参与数据的读写。
  4. 系统由内核态切换到用户态(第四次上下文切换),网络缓冲区的数据通过DMA的方式传输到网卡的驱动(存储缓冲区)中(protocol engine)

传统的I/O方式会经过4次用户态和内核态的切换(上下文切换),两次CPU中内存中进行数据读写的过程。这种拷贝过程相对来说比较消耗资源。

在整个过程中,过程1和4是由DMA负责,并不会消耗CPU,只有过程2和3的拷贝需要CPU参与。

我们思考一个问题,如果在应用程序中,不需要操作内容,过程2和3就是多余的,如果可以直接把内核态读取缓存冲区数据直接拷贝到套接字相关的缓存区,是不是可以达到优化的目的?

在Java中,正好FileChannel的transferTo() 方法可以实现这个过程,该方法将数据从文件通道传输到给定的可写字节通道, 上面的file.read()socket.send()调用动作可以替换为 transferTo()调用。

public void transferTo(long position, long count, WritableByteChannel target);

在 UNIX 和各种 Linux 系统中,此调用被传递到 sendfile() 系统调用中,最终实现将数据从一个文件描述符传输到了另一个文件描述符。

NIO 的零拷贝依赖于操作系统的支持,我们来看看操作系统意义上的零拷贝的流程(没有内核空间和用户空间数据拷贝)。相比于传统 IO,减少了两次上下文切换和数据拷贝,从操作系统角度称为零拷贝。如果熟悉 JVM 的同学应该知道,NIO 会使用一块 JVM 之外的内存区域,直接在该区域进行操作。

这种方式的I/O原理就是将用户缓冲区(user buffer)的内存地址和内核缓冲区(kernel buffer)的内存地址做一个映射,也就是说系统在用户态可以直接读取并操作内核空间的数据。

  1. sendfile()系统调用也会引起用户态到内核态的切换,与内存映射方式不同的是,用户空间此时是无法看到或修改数据内容,也就是说这是一次完全意义上的数据传输过程。

  2. 从磁盘读取到内存是DMA的方式,从内核读缓冲区读取到网络发送缓冲区,依旧需要CPU参与拷贝,而从网络发送缓冲区到网卡中的缓冲区依旧是DMA方式。

从上面我们可以看出,零拷贝的是指在操作过程中,CPU 不需要为数据在内存之间拷贝消耗资源,传统的 IO 操作需用从用户态转为内核态,内核拿到数据后还需要由内核态转为用户态将数据拷贝到用户空间,而零拷贝不需要将文件拷贝到用户空间,而直接在内核空间中传输到网络的方式。

内核空间操作文件的过程对用户来说是不透明的,用户只能请求和接受结果,如果用户想要参与这个过称怎么办?这时候就需要一个内存映射文件(将磁盘上的文件映射到内存之中,修改内存就可以修改磁盘上的文件),直接操作内核空间。

MappedByteBuffer,文件在内存中的映射,Java 程序不用和磁盘打交道,应用程序只需要对内存进行操作,这块内存是一个堆外内存。操作系统负责将我们对内存映射文件的修改更新到磁盘。

Java 中的实现

  File file = new File("test.zip");
RandomAccessFile raf = new RandomAccessFile(file, "rw");
FileChannel fileChannel = raf.getChannel();
SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("", 1234));
// 直接使用了transferTo()进行通道间的数据传输
fileChannel.transferTo(0, fileChannel.size(), socketChannel);

NIO 的零拷贝由 transferTo() 方法实现。transferTo() 方法将数据从 FileChannel 对象传送到可写的字节通道(如Socket Channel等)。在内部实现中,由 native 方法 transferTo0() 来实现,它依赖底层操作系统的支持。在UNIX 和 Linux 系统中,调用这个方法将会引起 sendfile() 系统调用。

我们上面也说过,内核空间操作文件的过程对用户来说是不透明的,用户只能请求和接受结果,如果用户想要参与这个过称怎么办?

  File file = new File("test.zip");
RandomAccessFile raf = new RandomAccessFile(file, "rw");
FileChannel fileChannel = raf.getChannel();
MappedByteBuffer buffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, fileChannel.size());

首先,它的作用位置处于传统IO(BIO)与零拷贝之间,为何这么说?

  • IO,可以把磁盘的文件经过内核空间,读到 JVM 空间(用户空间),然后进行各种操作,最后再写到磁盘或是发送到网络,效率较慢但支持数据文件操作。
  • 零拷贝则是直接在内核空间完成文件读取并转到磁盘(或发送到网络)。由于它没有读取文件数据到JVM这一环,因此程序无法操作该文件数据,尽管效率很高!

MappedByteBuffer 使用的是 JVM 之外的一块直接内存。

Java BIO NIO 与 AIO的更多相关文章

  1. JAVA bio nio aio

    [转自]http://qindongliang.iteye.com/blog/2018539 在高性能的IO体系设计中,有几个名词概念常常会使我们感到迷惑不解.具体如下: 序号 问题 1 什么是同步? ...

  2. java BIO/NIO/AIO 学习

    一.了解Unix网络编程5种I/O模型 1.1.阻塞式I/O模型 阻塞I/O(blocking I/O)模型,进程调用recvfrom,其系统调用直到数据报到达且被拷贝到应用进程的缓冲区中或者发生错误 ...

  3. Java的BIO,NIO和AIO的区别于演进

    作者:公众号:我是攻城师 前言 Java里面的IO模型种类较多,主要包括BIO,NIO和AIO,每个IO模型都有不一样的地方,那么这些IO模型是如何演变呢,底层的原理又是怎样的呢? 本文我们就来聊聊. ...

  4. 3. 彤哥说netty系列之Java BIO NIO AIO进化史

    你好,我是彤哥,本篇是netty系列的第三篇. 欢迎来我的公从号彤哥读源码系统地学习源码&架构的知识. 简介 上一章我们介绍了IO的五种模型,实际上Java只支持其中的三种,即BIO/NIO/ ...

  5. BIO,NIO与AIO的区别

    Java NIO : 同步非阻塞,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理.Java AIO(NIO.2 ...

  6. java的NIO和AIO

    1. 什么是NIO NIO是New I/O的简称,与旧式的基于流的I/O方法相对,从名字看,它表示新的一套Java I/O标 准.它是在Java 1.4中被纳入到JDK中的,并具有以下特性: NIO是 ...

  7. java BIO NIO IO

    参考 https://www.cnblogs.com/zedosu/p/6666984.html 摘要: 关于BIO和NIO的理解 最近大概看了ZooKeeper和Mina的源码发现都是用Java N ...

  8. 关于BIO NIO和AIO的理解

    转载自 :http://blog.csdn.net/anxpp/article/details/51512200 1.BIO编程 1.1.传统的BIO编程 网络编程的基本模型是C/S模型,即两个进程间 ...

  9. java BIO/NIO

    一.BIO Blocking IO(即阻塞IO); 1.      特点: a)   Socket服务端在监听过程中每次accept到一个客户端的Socket连接,就要处理这个请求,而此时其他连接过来 ...

随机推荐

  1. H5 移动端获取当前位置

    3种方法:1.H5自带的方法,获取经纬度2.通过地图提供的JS.获取位置3.通过微信的API(这个需要公众号 / 小程序) 1.通过H5自带的获取经纬度的方法 优点: 需要引用的资源较少,H5自带的方 ...

  2. Python--day41--条件

    1,条件 #锁 #acquire release#一个条件被创建之初 默认有一个False状态#False状态 会影响wait一直处于等待状态#notify(int数据类型) 造钥匙 代码示例:条件. ...

  3. 查看php-fpm的进程和端口号

    ps -ef | grep php-fpm   查看php-fpm所有的进程 ps -ef | grep php-fpn.conf 查看配置所在路径 netstat -lntp 查看监听端口  lis ...

  4. python基础十四之匿名函数

    匿名函数 处理简单问题的简化函数,关键字lambda. # 格式:函数名 = lambda 参数:返回值 anonymity = lambda s: s ** 0.5 print(anonymity( ...

  5. UVa 1627 - Team them up!——[0-1背包]

    Your task is to divide a number of persons into two teams, in such a way, that: everyone belongs to ...

  6. linux 内核定时器的实现

    为了使用它们, 尽管你不会需要知道内核定时器如何实现, 这个实现是有趣的, 并且值得 看一下它们的内部. 定时器的实现被设计来符合下列要求和假设: 定时器管理必须尽可能简化. 设计应当随着激活的定时器 ...

  7. 【codeforces 749A】Bachgold Problem

    time limit per test1 second memory limit per test256 megabytes inputstandard input outputstandard ou ...

  8. C# 对 byte 数组进行模式搜索

    本文告诉大家几个方法从 byte 数组找到对应的相同序列的数组 最简单的方法是进行数值判断,但是代码最少是使用Linq ,效率比较高是使用 Boyer-Moore 算法,下面就告诉大家几个算法的代码 ...

  9. git 常用操作命令(Common operation)

    win10清除已登录账号密码方法 打开控制面板(Control Panel): 选择用户账户(User Accounts): 选择管理你的凭据(Credential Manager): 管理windo ...

  10. Tufurama CodeForces - 961E (cdq分治)

    题面 One day Polycarp decided to rewatch his absolute favourite episode of well-known TV series " ...