聊聊Java-IO模型那些事

本文将重点讲述IO模型,基础相关部分涉及较少,请谨慎食用!

什么是IO?Java中处理IO的方式有什么

IO就是输入与输出,输入数据到内存的过程为输入,从内存输出到外部存储为输出。Java中处理IO的方式如下:

  • 1、字节流:InputStream/OutputStream,java.io.InputStream是所有字节输入流的父级抽象类,而我们常见的像FileInputStream,可以指定文件路径,进行文件读取。这里不涉及使用方法。
  • 2、字符流:Reader/Writer,其实有个知识,不管文件读写还是网络发送数据,信息的最小存储单元都是字节,但是为什么IO操作还是还分字节流和字符流?字符流是Java虚拟机将字节转换过来的,这个过程是比较耗时的,同时如果我们不知道编码的类型,那么很可能会造成读取数据乱码的问题。
  • 3、字节缓冲流:它是字节流的一种增强,典型使用为BufferedInputStream/BufferOutputStream。
  • 4、字符缓冲流:它是字符流的一种增强,典型使用为BufferReader/BufferWriter。
  • 5、打印流:System.out
  • 6、随机访问流:RandomAccessFile,可以支持跳转到文件的任意位置进行读写。同时可以制定模式,r只读,rw读写,rws同时更新文件的内容或元数据(用来描述文件的属性,比如大小,创建时间等)修改到外部存储设备。rwd,同时更新文件的内容到外部存储设备。

关于IO的一些前置知识

在操作系统中,为了保证操作系统的稳定性和安全性,一个进程的地址空间分为了两种,用户空间内核空间 。我们平时运行的程序都是在用户空间运行的,在内核空间中才能完成系统级别的资源操作,比如文件,进程通信,内存管理等等,并且,用户空间不能直接访问内核通奸,必须由系统调用操作才行。 在Unix系统下,IO的模型一共有五种,同步阻塞IO,异步非阻塞IO,IO多路复用,信号驱动IO和异步IO。下面我们说一说Java中常见的三种IO模型。

Java中常见的3中IO模型

BIO(Blocking IO)

同步阻塞IO,当应用程序发起read调用后,会一直阻塞,等内核把数据拷贝到用户空间。

如下图,引自《深入拆解Tomcat & Jetty》

这种方式有很明显的弊端,当面对大量连接的的时候,效率太低下了。

NIO(Non-blocking/New IO)

在Java1.4的时候引入,提供了Channel ,Selector ,Buffer等。它并不是同步非阻塞IO,我们先看一下同步非组合IO是什么样的。

如下图引自《深入拆解Tomcat & Jetty》

同步非阻塞IO进行了一些改进,就是应用程序会一直发起Read调用,也就是会一直问内核,数据准备好没有,不管好没好都会直接返回,但是应用程序可以一直Read,但是数据从内核拷贝到用户空间的这段时间线程依然是阻塞的。同样问题也非常明显,就是,应用程序不断的轮询去read是非常消耗CPU资源的。所以Java的NIO可以看作是IO的多路复用模型。

如下图,引自《深入拆解Tomcat & Jetty》

在IO多路复用模型中,线程有限发起select调用,询问数据是否准备就绪,等内核准备好了,再发起read调用。一会我们详细说一下NIO,再说下一个模型

AIO(Asynchronous IO)

在Java7中引入了NIO的改进版,它是异步IO模型,基于事件和回调机制实现的,操作之后会理解返回,当后台处理完会通知响应线程进行操作

如下图,引自《深入拆解Tomcat & Jetty》

但是应用还不广泛。

详细说一说NIO

上面我们说了NIO的一些基础概念,从机制上来说,它是一个非阻塞,面向缓冲,基于通道的IO,可以通过少量线程处理多个连接,很大成都的提高了IO的的效率和并发程度。它最重要的是三个组件

  • 1、Channel:通道,是一个双向的可读可写的数据传输通道,NIO通过Channel来实现数据的输入输出,它可以代表多种连接,比如FileChannel(文件访问通道),SocketChannel,ServerSocketChannel(TCP通信通道),DatagramChannel(UDP)通信通道。
  • 2、Buffer:缓冲区,NIO对数据的读写都是通过缓冲区操作的,读操作的时候将Channel数据填充到Buffer中,写的时候将Buffer中的数据写入到Channel中。
  • 3、Selector:选择器,允许一个线程处理多个Channel,所有的Channel都可以注册到Selector上,由Selector来分配线程处理事件

所以通过描述我们就可以大概知道IO多路复用就像是一个树,Selector就像是一个根节点一样。我们详细说一下这三个组件。

Buffer缓冲区

在阻塞io中,数据的读写都是面向字节流和字符流的。而在BIO中所有的数据都是面向缓冲区的,甭管你是读还是写,对应的都是对缓冲区的数据进行读写。看下图的一个示例

Buffer是一个抽象类,对于它的子类,比如ByteBuffer,CharBuffer,IntBuffer等存储的数据都是数组格式,对应的就是byte[],char[],int[]。然后我们看一下Buffer定义的四个成员变量。

  • capacity:容量,Buffer可以存储的最大数据量,创建时设置,不可改变。
  • limit:界限,在写模式下,limit代表最多能写入的数据,一般等于capacity,当然也可以自己设置,读模式下,limit等于Buffer实际写入的数据大小
  • position:位置,下一个可以被读写的数据的索引位置。通过flop归零,从头可以读写
  • mark: 标记,Buffer允许将位置直接定位到该标记,默认设置为-1,就是为了区别0和正整数,代表着一个未设置的状态。

这四种变量的关系:0 <= mark <= position <= limit <= capacity

四种变量读写模式下的示意图如下

Buffer中get方法读取缓冲区数据,put方法写入缓冲区数据。flip()将缓冲区从写模式切换到读模式,将limit设置为当前position的值,将position设置为0。clear()清空缓冲区,将读模式切换成写模式,将position设置为0,将limit设置为capation的值

Buffer对象不能通过new创建,只能通过静态方法实例化,如下

Channel通道

缓冲区是不能进行文件的续写的,所以就需要一个通道,完成这个事,上面我们也说了,Buffer写数据就是将数据写入到Channel中,Channel可以用于读、写或者同时读写,比流更好的映射了底层操作系统的API,最核心的两个方法,read()读取数据到Buffer中,write()将Buffer中数据写到Channel中。

Selector选择器

它允许一个线程处理多个Channel,Selector是基于事件驱动的多路复用模型,主要原理就是,各种Channel注册到Selector上面,然后Selector不断的轮询他们,当某个Channel上面有新的TPC接入,读写事件,这个Channel就处于继续状态,Selector轮询出来,然后加入到就绪集合中,通过SelectionKey可以获取就绪的Channel集合,然后对这些就绪的Channel进行IO操作。

Selector可以监听下面四种事件类型:

  • 1、SelectionKey.OP_ACCEPT:表示接受连接的事件
  • 2、SelectionKey.OP_CONNECT:表示完成连接的事件
  • 3、SelectionKey.OP_READ:表示准备好读取的事件
  • 4、SelectionKey.OP_WRITE:表示准备好进行写入的事件 下面展示一个网络读写的简单案例,来描述一下过程
java 复制代码
public static void main(String[] args) throws Exception {
    try {
        //创建一个ServerSocketChannel的实例
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.configureBlocking(false);
        serverSocketChannel.socket().bind(new InetSocketAddress(8080));

        //实例化Selector,需要调用open方法
        Selector selector = Selector.open();
        //注册serverSocketChannel到selector,指明监听事件
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        //可以获取所有的注册的SelectionKey:selector.keys(),但是还没有被选择
        //selector.selectedKeys()没有它
        while (true){
            //监控所有注册的Channel,当需要处理IO的时候返回,这时候才会进入被选择SelectionKey中
            int readyChannels = selector.select();
            if(readyChannels ==0){
                continue;
            }
            //这时候才有值
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = selectionKeys.iterator();
            //遍历
            while (iterator.hasNext()){
                SelectionKey selectionKey = iterator.next();
                //写事件的实际处理过程
                //处理连接事件
                if(selectionKey.isAcceptable()){
                    
                }else if(selectionKey.isReadable()){
                    //处理读事件
                    
                }else if(selectionKey.isWritable()){
                    //处理写事件
                    
                }
                iterator.remove();
            }
        }
    }catch (IOException e){
        e.printStackTrace();
    }
}

扩展知识--NIO零拷贝

零拷贝是指计算机执行IO操作时,CPU不需要将数据从一个存储区域复制到另一个存储区域,从而减少上下文切换以及CPU的拷贝时间。Java中对零拷贝的支持有2种形式

  • 1、MappedByteBuffer: 是一种基于内存映射的零拷贝的一种实现,地层是调用linux内核中的mmap。他可以将文件或者文件的一部分映射到内存中,形成一个虚拟内存文件,然后直接操作内存中的数据,而不需要徐彤调用读写文件。
  • 2、FileChannel中的transferTo/transferFrom方法,是NIO基于发送文件(sendfile)这种方式实现的 ,sendfile是Linux内核的一种系统调用,它可以直接将文件从餐盘发送到网络,而不需要用户控件的缓存区。 但是不管哪种方式,都需要两次DMA(直接内存访问),读一次,写入一次,而常规的方法,在CPU中的拷贝就需要梁文,内核->用户,用户->内核。
相关推荐
hong_zc14 分钟前
JDBC 编程
java·数据库·mysql
Flying_Fish_roe15 分钟前
MyBatis-Plus 常见问题与优化
java·tomcat·mybatis
X² 编程说18 分钟前
14.面试算法-字符串常见算法题(三)
java·数据结构·后端·算法·面试
imc.111 小时前
初识linux(2)
java·linux·数据库
武子康1 小时前
大数据-143 - ClickHouse 集群 SQL 超详细实践记录!
java·大数据·数据库·分布式·sql·clickhouse·flink
巧手打字通1 小时前
解锁Java线程池:实战技巧与陷阱规避
java·性能优化·线程池
请不要叫我菜鸡1 小时前
Go语言基础学习02-命令源码文件;库源码文件;类型推断;变量重声明
linux·后端·golang·类型推断·短变量·变量重声明·库源码文件
装不满的克莱因瓶1 小时前
【微服务】Eureka的自我保护机制
java·spring cloud·云原生·eureka·注册中心·服务注册
虫本初阳1 小时前
【Java】接口interface【主线学习笔记】
java·笔记·学习
繁依Fanyi1 小时前
828华为云征文|华为Flexus云服务器打造《我的世界》游戏服务器
java·服务器·开发语言·python·算法·华为·华为云