天天看点

JAVA NIOJAVA IO/NIO学习笔记

JAVA IO/NIO学习笔记

文章目录

  • JAVA IO/NIO学习笔记
    • 笔记链接
    • 1. JAVA IO/NIO
      • 1.1 简单介绍
      • 1.2阻塞和非阻塞
      • 1.3 Linux 网络 I/O模型(前置知识)
        • 阻塞IO模型
        • 非阻塞IO模型
        • 多路复用IO模型
        • 信息驱动IO模型
        • 异步IO模型
        • 五种IO对比
      • 1.4 NIO
        • 认识NIO
        • 认识channel
        • 认识Buffer
        • 认识Selector
        • FileChannel使用
          • 获取Filechannel的方法
          • FileChannel读取数据
          • 从FileChannel写数据
        • Charset使用
          • 获取解码器对象
          • 获取编码器对象
        • SocketChannel和ServerSocketChannel使用
          • 创建SocketChannel
          • 创建ServerSocketChannel
          • 阻塞和非阻塞的切换
          • SocketChannel非阻塞式
        • Selector使用
          • 创建Selector
          • 注册Channel到Selector
          • 轮询channel

笔记链接

JVM学习笔记(一)

JVM学习笔记(二)

JVM学习笔记(三)

JVM学习笔记(四)

(待更新…)

JAVA NIO

(待更新…)

1. JAVA IO/NIO

1.1 简单介绍

NIO即New IO,是在JDK1.4中才引入的。NIO主要用到的是块,所以NIO的效率要比IO高很多。NIO 和传统 IO 之间第一个最大的区别是,IO 是面向流的,NIO 是面向缓冲区的。NIO主要是使用Selector多路复用器来实现。Selector在Linux等主流操作系统上是通过epoll实现的。

1.2阻塞和非阻塞

阻塞型

Java IO的各种流是阻塞的。这意味着,当一个线程调用read() 或 write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。典型的阻塞 IO 的例子为:data = socket.read();如果数据没有就绪,就会一直阻塞在 read 方法。

非阻塞型

Java NIO的非阻塞模式,使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取,而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。

1.3 Linux 网络 I/O模型(前置知识)

阻塞IO模型

传统的IO模型,读写过程都会发生阻塞现象。当用户线程发出 IO 请求之后,内核会去查看数据是否就绪,如果没有就绪就会等待数据就绪,而用户线程就会处于阻塞状态(进入阻塞队列),用户线程交出 CPU。当数据就绪之后,内核会将数据拷贝到用户线程,并返回结果给用户线程,用户线程才解除 block 状态。

一些相关图片

JAVA NIOJAVA IO/NIO学习笔记

非阻塞IO模型

一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取,而不是保持线程阻塞,数据还没有准备好时,线程会继续发生读取请求,一旦内核中的数据准备好了,并且又再次收到了用户线程的请求,那么它马上就将数据拷贝到了用户线程,然后返回。但是对于非阻塞 IO 就有一个非常严重的问题,在 while 循环中需要不断地去询问内核数据是否就绪,这样会导致 CPU 占用率非常高。

相关图片

JAVA NIOJAVA IO/NIO学习笔记

多路复用IO模型

Java NIO 实际上就是多路复用 IO。在多路复用 IO 模型中,只需要使用一个线程就可以管理多个socket,系统不需要建立新的进程或者线程,也不必维护这些线程和进程,并且只有在真正有socket 读写事件进行时,才会使用 IO 资源,所以它大大减少了资源占用。当用户进程调用了select,那么整个进程会被block,而同时,kernel会“监视”所有select负责的socket,当任何一个socket中的数据准备好了,select就会返回。这个时候用户进程再调用read操作,将数据从kernel拷贝到用户进程。另外多路复用 IO 为何比非阻塞 IO 模型的效率高是因为在非阻塞 IO 中,不断地询问 socket 状态时通过用户线程去进行的,而在多路复用 IO 中,轮询每个 socket 状态是内核在进行的,这个效率要比用户线程要高的多。

相关图片:

JAVA NIOJAVA IO/NIO学习笔记

信息驱动IO模型

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

相关图片:

JAVA NIOJAVA IO/NIO学习笔记

异步IO模型

异步IO是最最理想的 IO 模型,异步IO执行过程:

  1. 用户线程发起 read 操作。
  2. 内核接受到一个 asynchronous read 之后,立刻返回,说明 read 请求已经成功发起了。(只是告知线程,我收到了你的读取请求啦)
  3. 用户线程不会产生任何 block,用户线程继续干自己的事情。
  4. 内核会等待数据准备完成,然后将数据拷贝到用户线程,当这一切都完成之后,内核会给用户线程发送一个信号,告诉它 read 操作完成了。
  5. 用户线程被告知操作完成后去读取数据。

在用户线程看来:只需要先发起一个请求,当接收内核返回的成功信号时表示 IO 操作已经完成,可以直接去使用数据了。

跟信号驱动IO对比:信号驱动IO只是由内核通知我们合适可以开始下一个IO操作,而异步IO模型是由内核通知我们操作什么时候完成,IO全由内核全部处理。异步IO,内核会主动将数据复制到用户进程/线程。也就是说,在异步 IO 模型中,收到信号表示 IO 操作已经完成,不需要再在用户线程中调用 IO 函数进行实际的读写操作。

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

相关图片:

JAVA NIOJAVA IO/NIO学习笔记

五种IO对比

IO过程可以划分为:等待数据到达,数据读取过程。两大时间段。

从下面的图可以发现:

阻塞IO模型: 阻塞时间最长。

非阻塞IO模型: CPU消耗大,不断check浪费CPU资源。

多路复用IO模型: 占用资源小(使用一个线程管理多个socket)。

信息驱动IO模型: 通过注册信号函数,内核准备好数据后,发送信息告知执行下一步IO。

异步IO模型: IO全程交由内核处理,用户程序只需发送IO请求,然后等待内核告知IO完成即可。

JAVA NIOJAVA IO/NIO学习笔记

1.4 NIO

认识NIO

JDK1.4开始引入了NIO类库,NIO主要是使用Selector多路复用器来实现,NIO 主要有三大核心部分:

  • Channel(通道)
  • Buffer(缓冲区)
  • Selector

传统 IO 基于字节流和字符流进行操作,而 NIO 基于 Channel 和 Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Java IO 面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。NIO 的缓冲导向方法不同。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动。这就增加了处理过程中的灵活性。

模型图

JAVA NIOJAVA IO/NIO学习笔记

认识channel

Channel 和 IO 中的 Stream(流)是差不多的。只不过 Stream 是单向的,而Channel 是双向的,既可以用来进行读操作,又可以用来进行写操作。NIO 中的 Channel 的主要实现有:

  1. FileChannel
  2. DatagramChannel
  3. SocketChannel
  4. ServerSocketChannel

后三个用于网络通信,SocketChannel和ServerSocketChannel用于TCP连接,DatagramChannel用于UDP连接。

认识Buffer

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

常用的 Buffer 的子类有:ByteBuffer、IntBuffer、 CharBuffer、 LongBuffer、 DoubleBuffer、FloatBuffer、ShortBuffer。

所有缓冲区都有4个属性:capacity、limit、position、mark。

  • Capacity 容量,即可以容纳的最大数据量;在缓冲区创建时被设定并且不能改变
  • Limit 表示缓冲区的当前终点,不能对缓冲区超过极限的位置进行读写操作。且极限是可以修改的
  • Position 位置,下一个要被读或写的元素的索引,每次读写缓冲区数据时都会改变改值,为下次读写作准备
  • Mark 标记,调用mark()来设置mark=position,再调用reset()可以让position恢复到标记的位置

关于ByteBuffer类实例化

ByteBuffer类提供了4个静态工厂方法来获得ByteBuffer的实例。

  • allocate(int capacity) 从堆空间中分配一个容量大小为capacity的byte数组作为缓冲区的byte数据存储器
  • allocateDirect(int capacity) 是不使用JVM堆栈而是通过操作系统来创建内存块用作缓冲区,它与当前操作系统能够更好的耦合,因此能进一步提高I/O操作速度。但是分配直接缓冲区的系统开销很大,因此只有在缓冲区较大并长期存在,或者需要经常重用时,才使用这种缓冲区
  • wrap(byte[] array) 这个缓冲区的数据会存放在byte数组中,bytes数组或buff缓冲区任何一方中数据的改动都会影响另一方。其实ByteBuffer底层本来就有一个bytes数组负责来保存buffer缓冲区中的数据,通过allocate方法系统会帮你构造一个byte数组
  • wrap(byte[] array, int offset, int length) 在上一个方法的基础上可以指定偏移量和长度,这个offset也就是包装后byteBuffer的position,而length呢就是limit-position的大小,从而我们可以得到limit的位置为length+position(offset)

认识Selector

Selector 类是 NIO 的核心类,Selector 能够检测多个注册的通道上是否有事件发生,如果有事件发生,便获取事件然后针对每个事件进行相应的响应处理。这样一来,只是用一个单线程就可以管理多个通道,也就是管理多个连接。

FileChannel使用

获取Filechannel的方法

通过FileInputStream/FileOutputStream获取

//通过FileInputStream/FileOutputStream获取
File file =new File("./src/testchannl.txt");
FileInputStream fin = new FileInputStream(file.getAbsoluteFile());
FileChannel fcl = fin.getChannel();
           

通过FileChannel的open方法

//通过FileChannel的open方法,open默认为读模式打开
Path path = Paths.get("./src/testchannl.txt");
FileChannel channel = FileChannel.open(path);
//写模式打开
channel = FileChannel.open(path, StandardOpenOption.WRITE);
           
FileChannel读取数据

非直接缓冲区

//申请buffer
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
//clear buffer中的数组,其实是通过改变position的数值进行伪删除的,并未真正的删除数据
byteBuffer.clear();
while (fileChannel.read(byteBuffer)>0){
    //切换,更改limit=position,position=0,主要目的是要锁定有效的数据块
    byteBuffer.flip();
    byte[] tem = new byte[byteBuffer.limit()];
    //获取数据
    byteBuffer.get(tem);
    System.out.print(new String(tem));
    byteBuffer.clear();
}
           

通过内存映射文件

//---------------------通过内存映射文件-----------------
//读入模式打开通道
inchannel = FileChannel.open(path, StandardOpenOption.READ);
//获取映射文件
MappedByteBuffer mapBuffer = inchannel.map(FileChannel.MapMode.READ_ONLY,0,inchannel.size());

byte[] dst = new byte[mappedByteBuffer.limit()];
//从映射文件调用get方法写到dst中
mappedByteBuffer.get(dst);
System.out.println(new String(dst));

           
从FileChannel写数据
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
String data = new String("hello world!");
byteBuffer.put(data.getBytes());//写到buffer
byteBuffer.flip();  //切换
channel.write(byteBuffer);//写入通道
byteBuffer.clear();//重置position和limit,等待下次的读写
           

Charset使用

Charset为字符集对象

//获取所有可以字符集
Charset.availableCharsets();
//获取默认字符集
Charset.defaultCharset();
           
获取解码器对象
Charset charset = Charset.forName(charsetname);
charset.newDecoder();
           
获取编码器对象
Charset charset = Charset.forName(charsetname);
charset.newEncoder();
           

SocketChannel和ServerSocketChannel使用

创建SocketChannel
//方式一
SocketChannel sc = SocketChannel.open(new InetSocketAddress("127.0.0.1",12345));

//方式二
SocketChannel sc = SocketChannel.open();
sc.connect(new InetSocketAddress("127.0.0.1",12345));
           
创建ServerSocketChannel
//创建并绑定到指定端口
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.bind(new InetSocketAddress(12345));
           
阻塞和非阻塞的切换
sc.configureBlocking(false);
ssc.configureBlocking(false);
           
SocketChannel非阻塞式
sc.configureBlocking(false);
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);  //申请空间
Scanner scanner = new Scanner(System.in);  //标准输入流(键盘)
String str = null;
while ((str = scanner.nextLine())!=null){
    byteBuffer.put(str.getBytes());   //将键盘输入的数据放到buffer中
    byteBuffer.flip();    //切换状态,锁定要写
    sc.write(byteBuffer);   //将buffer中的数据写到sc中
    byteBuffer.clear();  //重置
}
           

Selector使用

创建Selector
注册Channel到Selector
//ServerSocketChannel对象注册到指定的selector对象
//SelectionKey.OP_ACCEPT为监控事件
ssc.register(selector, SelectionKey.OP_ACCEPT);
           
轮询channel
selector.select()