Java IO流深入理解

目录
  • 阻塞(Block)和非阻塞(Non-Block)
  • 同步(Synchronization)和异步(Asynchronous)
  • BIO与NIO对比
    • 面向流与面向缓冲
    • 阻塞与非阻塞
    • 选择器的问世
  • Java NIO三件套
    • 缓冲区Buffer
    • Buffer的基本的原理
    • 缓冲区分配
  • 选择器Selector
  • 通道Channel
    • 使用NIO读取数据
    • 使用NIO写入数据
  • IO多路复用
  • 总结

阻塞(Block)和非阻塞(Non-Block)

阻塞和非阻塞是进程在访问数据的时候,数据是否准备就绪的一种处理方式,当数据没有准备的时候。

**阻塞:**往往需要等待缓冲区中的数据准备好过后才处理其他的事情,否者一直等待在那里

**非阻塞:**当我们进程访问我们的数据缓冲区的时候,如果数据没有准备好则直接返回,不会等待。如果数据已经准备好,也直接返回。

同步(Synchronization)和异步(Asynchronous)

同步和异步都是基于应用程序和操作系统处理IO事件所采用的方式。比如同步:是应用程序要直接参与IO读写的操作。异步:所有的IO读写交给操作系统去处理,应用程序只需要等待通知。

同步方式在处理IO事件的时候,必须阻塞在某个方法上面等待我们的IO事件完成(阻塞IO事件或者通过轮询IO事件的方式),对于异步来说,所有的IO读写都交给了操作系统。这个时候,我们可以去做其他的事情,并不需要去完成真正的IO操作,当操作完成iO后,会给我们的应用程序一个通知。

**同步:**阻塞到IO事件,阻塞到read或者write。这个时候我们就完全不能做自己的事情。让读写方法加入到线程里面,然后阻塞线程来实现,对线程的性能开销比较大。

BIO与NIO对比

IO模型 BIO NIO
通信 面向流(乡村公路) 面向缓冲(高速公路,多路复用技术)
处理 阻塞IO(多线程) 非阻塞IO(反应堆Reactor)
触发 选择器 轮询机制

面向流与面向缓冲

java NIO和BIO之间第一个最大的区别是,BIO是面向流的,NIO是面向缓冲的。Java BIO面向流意味着每次从流中读一个或多个字节,直至读取所有的字节,它们没有被缓存在任何地方。此外,它不能前后移动流中的数据。如果需要前后移动从流中读取的数据,需要先将它缓存到一个缓冲区。Java NIO的缓冲导向方法略有不同。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区前后移动。这就增加了处理过程中的灵活性。但是,还需要检查是否缓冲区包含了所有您需要处理的数据。而且,需确保当更多的数据读入缓冲区时,不要覆盖缓冲区里尚未处理的数据。

阻塞与非阻塞

Java BIO的各种流是阻塞的。这意味着,当一个线程调用read()和write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。java NIO的非阻塞模式,使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么多不会获取。而不是保持线程阻塞,所以直至数据变得可以读取之前,该线程可以继续做其他的事情。

非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。线程通常将非阻塞IO的空闲时间用于在其他通道上执行IO操作,所以一个单独的线程现在可以管理多个输入和输出通道。

选择器的问世

java NIO的选择器(Selector)允许一个单独的线程来监视多个输入通道,你可以注册多个通道使用一个选择器,然后使用一个单独的线程来“选择”通道,这些通道里已经有可以处理的输入,或者选择已准备写入的通道。这种选择机制,使得一个单独的线程很容易来管理多个通道。

Java NIO三件套

在NIO中有几个核心对象需要掌握:缓冲器(Buffer)选择器(Selector)通道(Channel)

缓冲区Buffer

缓冲区实际上是一个容器对象,更直接的说,其实就是一个数组,在NIO库中,所有数据都是用缓冲区出来的。在读取数据时,它是直接读到缓冲区的;在写入数据时,它也是写入到缓冲区的;任何时候访问NIO中的数据,都是将它放到缓冲区中。而在面向流I/O系统中,所有数据都是直接写入或者直接将数据读取到Stream对象中。

在NIO中,所有的缓冲区类型都继承于抽象类Buffer,最常用的就是ByteBuffer,对于java中的基本类型,基本都有一个具体Buffer类型与之相对于,他们之间的extend关系如下图所示。

eg:

  public static void main(String[] args) {
        //new NIOServerDemo(8080).listen();

        // 分配新的 int 缓冲区,参数为缓冲区容量
        // 新缓冲区的当前位置将为零,其界限(限制位置)将为其容量。它将具有一个底层实现数组,其数组偏移量将为零。
        IntBuffer buffer = IntBuffer.allocate(8);
        for (int i = 0; i < buffer.capacity(); ++i) {
            int j = 2 * (i + 1);
            // 将给定整数写入此缓冲区的当前位置,当前位置递增
            buffer.put(j);
        }
        // 重设此缓冲区,将限制设置为当前位置,然后将当前位置设置为 0
        buffer.flip();
        // 查看在当前位置和限制位置之间是否有元素
        while (buffer.hasRemaining()) {
            // 读取此缓冲区当前位置的整数,然后当前位置递增
            int j = buffer.get();
            System.out.print(j + " ");
        }
    }
2 4 6 8 10 12 14 16
Process finished with exit code 0

Buffer的基本的原理

在谈到缓冲区时,我们说缓冲区对象本质上是一个数组,但它其实是一个特殊的数组,缓冲区对象内置了一些机制,能够跟踪和记录缓冲区的状态变化情况。如果我们使用get()方法从缓冲区获取数据或者使用put()方法把数据写入缓冲区,都会引起缓冲区状态的变化。

在缓冲区中,最重要的属性有下面三个,它们一起合作完成对缓冲区内部的状态的变化跟踪。

position: 指定下一个将要被写入或者读取的元素索引,它的值由get()/put()方法自动更新,在新创建一个Buffer对象时,position被初始化为0.

limit:指定还有多少数据需要取出(在从缓冲区写入通道时),或者还有多少空间可以放入数据(在从通道读入缓冲区时)

**capacity:**制定了可以存储在缓冲区中的最大数据容量,实际上,它制定了底层数组的大小,或者至少是指定了准许我们使用的底层数组的容量。

以上三个属性值之间有一些相对大小的关系: 0<=positon<=limit<=capacity。如果我们创建一个新的容量大小为10的ByteBuffer对象,在初始化的时候,positon设置为0,limit和capacity被设置为10,在以后使用ByteBuffer对象过程中,capacity的值不会再发生变化,而其他两个将会随着使用而变化。

eg:

package com.evan.netty.nio.demo;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.Buffer;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

/**
 * @author evanYang
 * @version 1.0
 * @date 2021/7/26 11:29
 */
public class BufferDemo {
    public static void main(String[] args) throws IOException {

        FileInputStream fin = new FileInputStream("D://evan.txt");
        FileChannel fc = fin.getChannel();
        //先分配一个10大小的缓冲区
        ByteBuffer buffer = ByteBuffer.allocate(10);
        outPut("初始化",buffer);

        fc.read(buffer);
        outPut("调用read()方法",buffer);

        buffer.flip();
        outPut("调用flip()",buffer);

        //判断有没有可读数据
        while (buffer.remaining()>0){
            byte b = buffer.get();
        }

        outPut("调用get()",buffer);

        //可以理解为解锁
        buffer.clear();
        outPut("调用clear()",buffer);

        fin.close();
    }

    /**
     * 打印缓存实时状况
     * @param step
     * @param buffer
     */
    public static void outPut(String step, Buffer buffer){
        System.out.println(step+":");
        System.out.println("capacity: "+buffer.capacity()+",");
        System.out.println("position: "+buffer.position()+",");
        System.out.println("limit: "+buffer.limit());
        System.out.println();
    }
}

文件中的数据

输出结果:

运行结果我们已经可以知道,四个属性值分别如图所示:

我们可以从管道中读取一些数据到缓冲区,注意从通道读取数据,相当于往缓冲区写入数据。如果读取4个自己的数据,则此时position的值为4,即下一个将要被下入的字节索引是4,而limit仍然是10,如下图所示:

下一步把读取的数据写入到输出管道中,相当于从缓冲区中读取数据,在此之前,必须调用flip()方法,该方法将会完成两件事:

1,把limit设置为当前的positon值

2,把position设置为0

由于position被设置为0,所以可以保证在下一步输出时读取到的是缓冲区中的第一个字节,而limit被设置为当前的position,可以保证读取的数据正好是之前写入到缓冲区的数据,如下图所示。

现在调用get()方法从缓冲区读取数据写入到输出通道,这会导致position的增加而limit保持不变,单position不会超过limit的值,所以在读取我们之前写入到缓冲区中的4个自己之后,position和limit的值都为4.如下图所示。

在从缓冲区读取数据完毕后,limit的值仍然保持在我们调用flip()方法时的值,调用clean()方法能够把所有的状态设置为初始值。

缓冲区分配

在前面的几个例子中,我们已经看过了,在创建一个缓冲对象时,会调用静态方法allocate()来指定缓冲区的容量,其实调用allocate()相当于创建一个指定大小的数组,并把它包装为缓冲区对象。或者我们也可以直接将一个现有的数组,包装为缓冲区对对象

选择器Selector

传统的Server/Client 模式会基于TPR(Thread per Request),服务器会为每个客户端请求建立一个线程,由该线程单独负责处理一个客户请求。这种模式带来的一个问题就是线程数量的剧增,大量的线程会增大服务器的开销。大多数的实现为了避免这个问题,都采用了线程池模型,并设置线程池模型的最大数量,这又带来了新的问题,如果线程池中有200个线程,而有200个用户都在进行大文件下载,会导致第201个用户的请求无法及时处理,即便第201个用户只想请求一个几kb大小的页面。传统的Server/Client模式如下图所示。

NIO 中非阻塞I/O采用了基于Reactor模式的工作模式,I/O调用不会被阻塞,相反是注册感兴趣的特定I/O事件,如可读数据到达,新的套接字连接等等,在发生特定事件时,系统在通知我们。NIO中实现非阻塞I/O的核心对象就是Selector,Selector就是注册各种I/O事件地方,而且当那些事件发生时,就是这个对象告诉我们所发生的事件。如下图所示。

从图中可以看出,当有读或写等任何注册的时间发生时,可以从Selector中获得相应的SelectionKey,同时从SelectionKey中可以找到发生的事件和该事件所发生的具体SelectableChannel,以获得客户端发送过来的数据。

使用NIO中非阻塞I/O编写服务器处理程序,大体上可以分为下面三个步骤:

1,向Selector对象注册感兴趣的事件。

2,从Selector中获取感兴趣的事件。

3,根据不同的事件进行相应的处理。

通道Channel

通道是一个对象,通过它可以读取和写入数据,当然了所有数据都通过Buffer对象来处理。我们永远不会将字节直接写入通道中,相反是将数据写入包含一个或者多个字节的缓存区。同样不会直接从通道中读取字节,而是将数据从通道读入缓存区,再从缓冲区获取这个字节,而是将数据从通道读入缓冲区,再从缓冲区获取这个字节。

在NIO中,提供了多种通道对象,而所有的通道对象都实现了Channel接口。它们之间的继承关系如下图所示:

使用NIO读取数据

在前面我们说过,任何时候读取数据,都不是直接从通道读取,而是从通道读取到缓冲区。所以使用NIO读取数据可以分为下面三个步骤:

1,从FileInputStream获取Channel

2,创建Buffer

3,将数据从Channel读取到Buffer中

package com.evan.netty.nio.demo;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.Buffer;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

/**
 * @author evanYang
 * @version 1.0
 * @date 2021/7/26 16:15
 */
public class FileInputDemo {
    public static void main(String[] args) throws IOException {
        FileInputStream fin=new FileInputStream("D://evan.txt");
        FileChannel channel = fin.getChannel();

        ByteBuffer allocate = ByteBuffer.allocate(1024);
        //读取数据到缓冲区
        channel.read(allocate);

        allocate.flip();
        while (allocate.remaining()>0){
            byte b = allocate.get();
            System.out.println(b);
        }
        fin.close();
    }
}

使用NIO写入数据

使用NIO写入数据与读取数据的过程类似,同样数据不是直接写入通道,而是写入缓冲区,可以分为下面三个步骤:

1,从FileputStream获取channel。

2,创建Buffer

3,将数据从Channel写入到Buffer中,

package com.evan.netty.nio.demo;

import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

/**
 * @author evanYang
 * @version 1.0
 * @date 2021/7/26 16:33
 */
public class FileOutPutDemo {
    static private final byte message[] ={83, 111, 109, 101, 32, 98, 121, 116, 101, 115, 46 };
    public static void main(String[] args) throws IOException {
        FileOutputStream fout=new FileOutputStream("D://evan.txt");

        FileChannel channel = fout.getChannel();

        ByteBuffer buffer = ByteBuffer.allocate(1024);
        for (int i = 0; i < message.length; i++) {
            buffer.put(message[i]);
        }
        buffer.flip();
        channel.write(buffer);
        fout.close();
    }
}

IO多路复用

我们试想一下这样的现实场景。

100桌客人到店点菜

方法A:

服务员都把仅有的一份菜单递给其中一桌客人,然后站在这个客人身旁等待客人完成点菜过程。。。。。

方法B:

老板马上新雇佣99名服务员,同时印制99本新的菜单。没人服务一桌客人。

方法C:

改进点菜的方式,当客人到店后,自己申请一本菜单。想好自己要点的菜,然后呼叫服务员。服务员站在自己身边记录客人点的菜的内容。

  • 到店情况 :并发量

    • 到店情况不理想时,一个服务员一本菜单,就足够了
  • 客人:服务端请求
  • 点餐内容:客服端发送的实际数据
  • 老板:操作系统
  • 人力成本:系统资源
  • 菜单:文件状态描述符(FD)。操作系统对于一个进程能够同时持有的文件状态描述符的个数是有限制的,在linux系统中,$Ulimit -n 查看这个限制值,当然也是可以(并且应该)进行内核参数调整的
  • 服务员:操作系统内核用于IO操作的线程(内核线程)
  • 厨师:应用程序线程(当然厨房就是应用程序进程)
    • 方法A:同步IO
    • 方法B:同步IO
    • 方法C:多路复用IO

总结

本篇文章就到这里了,希望能给你带来帮助,也希望您能够多多关注我们的更多内容!

(0)

相关推荐

  • Java-IO流实验

    目录 前言 一.资源管理器 [1]. 题目 [2]. 实例 [3]. 代码 二.文件复制与剪切 [1]. 题目 [2]. 复制 [3]. 剪切 [4]. 代码 三.文件数据读写 [1]. 题目 [2]. 实例 [3]. 代码 总结 前言 项目结构如下,在使用代码的时候注意修改成你自己的包名和类名 一.资源管理器 [1]. 题目 设计一个用于显示指定目录下所有文件与文件夹的资源管理器类,要求包括: 从命令行输入一个路径,如果不是目录,则输出提示信息 如果是目录且存在,则显示该目录下,所有的文件与文

  • java基础入门之IO流

    目录 io学习框架: 文件: Io流的原理: 节点流和处理流: BufferWriter: 处理字节的处理流: 标准输入和输出: 转换流: 打印流: Properties类: 总结 io学习框架: 文件: 保存数据的地方. 1)常见文件对象的相关构造器和方法: 当进行File file = new File(filePath);只是在内存上有一个文件对象: 只有file.createNewFile();才会在磁盘创建文件 获取文件的相关信息: utf8中,一个汉字是三个字节,所以当用字节流的re

  • Java IO流之节点流与字符流的相关知识总结

    一.File file是文件和目录路径名的抽象表示 1.1 File的用法 用法: File file = new File("路径名"); //如 File file = new File("L:\\FileTestDemo\\AAA\\aaa.txt"); 注意:在windows中,路径名不能使用单个的\,单个的\为转义字符,可以使用\\,//或/ 1.2 File的常用方法 1.boolean createNewFile() 当且仅当具有此名称的文件尚不存在时

  • Java字节流和字符流总结IO流!

    目录 从接收输入值说起 字节流读取 字符流读取 Scanner 读取 什么是 IO 流 字节流和字符流 字节流 字节输入流 字节输出流 缓冲流的原理 字符流 字符输入流 字符输出流 RandomAccessFile 总结 从接收输入值说起 在日常的开发应用中,有时候需要直接接收外部设备如键盘等的输入值,而对于这种数据的接收方式,我们一般有三种方法:字节流读取,字符流读取,Scanner 工具类读取. 字节流读取 直接看一个例子: public class Demo01SystemIn { pub

  • Java IO流深入理解

    目录 阻塞(Block)和非阻塞(Non-Block) 同步(Synchronization)和异步(Asynchronous) BIO与NIO对比 面向流与面向缓冲 阻塞与非阻塞 选择器的问世 Java NIO三件套 缓冲区Buffer Buffer的基本的原理 缓冲区分配 选择器Selector 通道Channel 使用NIO读取数据 使用NIO写入数据 IO多路复用 总结 阻塞(Block)和非阻塞(Non-Block) 阻塞和非阻塞是进程在访问数据的时候,数据是否准备就绪的一种处理方式,

  • 【Java IO流】字节流和字符流的实例讲解

    字节流和字符流 对于文件必然有读和写的操作,读和写就对应了输入和输出流,流又分成字节和字符流. 1.从对文件的操作来讲,有读和写的操作--也就是输入和输出. 2.从流的流向来讲,有输入和输出之分. 3.从流的内容来讲,有字节和字符之分. 这篇文章先后讲解IO流中的字节流和字符流的输入和输出操作. 一.字节流 1)输入和输出流 首先,字节流要进行读和写,也就是输入和输出,所以它有两个抽象的父类InputStream.OutputStream. InputStream抽象了应用程序读取数据的方式,即

  • java IO流文件的读写具体实例

    引言: 关于java IO流的操作是非常常见的,基本上每个项目都会用到,每次遇到都是去网上找一找就行了,屡试不爽.上次突然一个同事问了我java文件的读取,我一下子就懵了第一反应就是去网上找,虽然也能找到,但自己总感觉不是很踏实,所以今天就抽空看了看java IO流的一些操作,感觉还是很有收获的,顺便总结些资料,方便以后进一步的学习... IO流的分类:1.根据流的数据对象来分:高端流:所有的内存中的流都是高端流,比如:InputStreamReader  低端流:所有的外界设备中的流都是低端流

  • Java IO流 文件的编码实例代码

    •文件的编码 package cn.test; import java.io.UnsupportedEncodingException; public class Demo15 { public static void main(String[] args) throws UnsupportedEncodingException { String str = "你好ABC123"; byte[] b1 = str.getBytes();//转换成字节系列用的是项目默认的编码 for (

  • Java IO流对象的序列化和反序列化实例详解

    Java-IO流 对象的序列化和反序列化 序列化的基本操作 1.对象序列化,就是将Object转换成byte序列,反之叫对象的反序列化. 2.序列化流(ObjectOutputStream),writeObject 方法用于将对象写入输出流中: 反序列化流(ObjectInputStream),readObject 方法用于从输入流中读取对象. 3.序列化接口(Serializeable) 对象必须实现序列化接口,才能进行序列化,否则会出现异常.这个接口没有任何方法,只是一个标准. packag

  • java IO流 之 输入流 InputString()的使用

    本文主要给大家介绍java的InputStream 流的使用. (1)FileInputstream: 子类,读取数据的通道 使用步骤: 1.获取目标文件:new File() 2.建立通道:new FileInputString() 3.读取数据:read() 4.释放资源:close() //一些默认要导入的包 import java.io.File; import java.io.FileInputStream; import java.io.IOException; public sta

  • Java IO流 File类的常用API实例

    •File类 1.只用于表示文件(目录)的信息(名称.大小等),不能用于文件内容的访问. package cn.test; import java.io.File; import java.io.IOException; public class Demo16 { public static void main(String[] args) { File file = new File("F:\\javaio"); //文件(目录)是否存在 if(!file.exists()) { /

  • Java IO流体系继承结构图_动力节点Java学院整理

    Java IO体系结构看似庞大复杂,其实有规律可循,要弄清楚其结构,需要明白两点: 1. 其对称性质:InputStream 与 OutputStream, Reader 与 Writer,他们分别是一套字节输入-输出,字符输入-输出体系 2. 原始处理器(适配器)与链接流处理器(装饰器) 其结构图如下: Reader-Writer体系 1. 基类 InputStream与OutputStream是所有字节型输入输出流的基抽象类,同时也是适配器(原始流处理器)需要适配的对象,也是装饰器(链接流处

  • java IO流读取图片供前台显示代码分享

    最近项目中需要用到IO流来读取图片以提供前台页面展示,由于以前一直是用url路径的方式进行图片展示,一听说要项目要用IO流读取图片感觉好复杂一样,但任务下达下来了,做为程序员只有选择去执行喽,于是找了点资料看了会api, 嘿感觉挺简单的,由于是第一次采用IO流的方式进行读取图片供页面显示,所以把以下代码记录一下 后台代码: /** * IO流读取图片 by:long * @return */ @RequestMapping(value = "/IoReadImage/{imgName}"

  • java IO流将一个文件拆分为多个子文件代码示例

    文件分割与合并是一个常见需求,比如:上传大文件时,可以先分割成小块,传到服务器后,再进行合并.很多高大上的分布式文件系统(比如:google的GFS.taobao的TFS)里,也是按block为单位,对文件进行分割或合并. 看下基本思路: 如果有一个大文件,指定分割大小后(比如:按1M切割) step 1: 先根据原始文件大小.分割大小,算出最终分割的小文件数N step 2: 在磁盘上创建这N个小文件 step 3: 开多个线程(线程数=分割文件数),每个线程里,利用RandomAccessF

随机推荐