Java的BIO、NIO、AIO详解

1、BIO、NIO、AIO的区别

BIO(blocked IO,同步阻塞IO):我们常说Java的IO流一般就是指BIO,BIO提供了很多类满足我们对系统文件输入与输出。BIO是面向流操作的,只能顺序地从流中读取数据。此外,BIO的最大特点就是其阻塞性,如调用InputStream.read()时,它会一直等到数据到来时才执行后续操作,否则会一直等待,这也是制约BIO的最大原因,也就引出NIO。

NIO(non-blocking IO,同步非阻塞性IO):也有称之为New IO,新IO流,jdk1.4时出现的。如果使用传统IO流,当要处理多个连接时,就需要采用多线程的方式,由于每个线程都拥有自己的内存栈,而且阻塞会导致大量线程之间进行切换,会使效率十分低下。NIO是面向缓存的,可以跳跃性读取和反复读取。此外,NIO的读写操作具有非阻塞性,不会影响后续代码的执行。

AIO(Asynchronous IO,异步非阻塞性IO):jdk7升级了java.nio库,升级后的NIO被称为NIO 2.0,也就是AIO。AIO提供了异步文件I/O操作,它不需要通过多路复用器对注册的通道进行轮询操作即可实现异步读写,从而简化了NIO的编程模型。

2、File类

  File类是java.io包下操作文件和文件夹的类。它提供了许多方法,包括新建文件,删除文件,获取文件名等等操作。主要分为5类:

2.1、 访问文件名相关的方法

public static void fileNameTest() { 
    File file = new File("E:\\code\\JavaSE\\IO\\resourse\\a.txt");//获得文件
    System.out.println(file.getName());//返回File对象的文件名或者路径名
    System.out.println(file.getPath());//返回File对象的路径
    System.out.println(file.getAbsoluteFile());//返回File对象的绝对路径名
    System.out.println(file.getParent());//返回File对象路径的父路径
    System.out.println(file.renameTo(new File("bbb.txt")));//重命名,如果成功返回True
}

2.2、 与文件相关的判断方法

public static void fileIsTest() {
    File file = new File("E:\\code\\JavaSE\\IO\\resourse\\aaa.txt");//获得文件
    System.out.println(file.exists());//判断File对象是否存在
    System.out.println(file.canRead());//判断File对象是否可读
    System.out.println(file.canWrite());//判断File对象是否可写
    System.out.println(file.isFile());//判断File对象是否是文件
    System.out.println(file.isDirectory());//判断File对象是否是目录
    System.out.println(file.isAbsolute());//判断File对象是否是绝对路径
}

2.3、 获取文件信息

public static void fileMesTest(){
    File file = new File("E:\\code\\JavaSE\\IO\\resourse\\aaa.txt");//获得文件
    System.out.println(file.lastModified());//获得文件的最后修改时间
    System.out.println(file.length());//获得文件的长度
}

2.4、 文件的创建和删除

public static void fileHandleTest() throws IOException {
    File file = new File("ccc.txt");
    boolean b = file.createNewFile();//创建文件或目录
    file.delete();//删除文件或目录
    file.deleteOnExit();//当Java虚拟机退出时删除File对象对于的目录或文件

}

2.5、 目录操作

public static void derectoryHandleTest(){
    File file = new File("E:\\code\\JavaSE\\IO");//获得目录
    boolean mkdir = file.mkdir();//创建目录,若是成功返回True
    String[] list = file.list();//获得子文件名和目录名
    for (String s : list) {
        System.out.println("s = " + s);
    }
    File[] listFiles = file.listFiles();//获得子文件和目录
    for (File listFile : listFiles) {
        System.out.println(listFile);
    }
}

3、BIO

3.1、IO流介绍

  Java的IO体系十分庞大,总共有40多个类。如果不对其进行分类的话,学习起来会非常混乱。这个体系可以分为4大类:字节输入流(InputStream)、字节输出流(OutputStream)、字符输入流(Reader)、字符输出流(Writer),其他的所有类都是基于此的子类。
在这里插入图片描述

3.2、字节与字符

字节流 :以字节为单位进行读写数据的流。
字符流 :以字符为单位进行读写数据的流。

字节:所有文件数据(文本、图片、音视频等等)在存储时,都是以二进制的形式保存的。所以,字节流可以传输任意文件数据。在操作流的时候,我们要时刻明确,无论使用什么样的流对象,底 层传输的始终为二进制数据。
字符:Java规定了字符的内码要用UTF-16编码,所以1个字符=2个字节。但我们通常采用UTF-8的编码格式,UTF-8编码是变长编码,通常汉字占三个字节,扩展B区以后的汉字占四个字节。由于UTF-8特点的编码方式,读取文件时可以判断一个字符占多少字节,从而保证数据的读取完整。
在这里插入图片描述

3.3、字节流

  这两个类是文件读取和写入的字节流类,FileInputStream负责将数据写入内存,FileOutputStream负责将数据从内存中写出。

import java.io.FileInputStream;
import java.io.FileOutputStream;
/**
 * @author RuiMing Lin
 * @date 2020-03-11 19:28
 */
public class Demo2 {
    public static void main(String[] args) throws Exception{
        FileInputStream fis = new FileInputStream("fis.txt");
        FileOutputStream fos = new FileOutputStream("fos.txt",true);
        int len;
        byte[] bytes = new byte[4];         //一次性写入4个字节,提高效率
        while ((len = fis.read(bytes))!=-1){    
            fos.write(bytes,0,len);     //要把len写进去,不然可能出现写入重复数据
        }
        fis.close();
        fos.close();
    }
}

使用try…catch…

import java.io.FileInputStream;
import java.io.FileOutputStream;

/**
 * @author RuiMing Lin
 * @date 2020-03-11 19:28
 */
public class Demo2 {
    public static void main(String[] args) throws Exception{
        try (FileInputStream fis = new FileInputStream("fis.txt");
             FileOutputStream fos = new FileOutputStream("fos.txt",true);){
            int len;
            byte[] bytes = new byte[4];         //一次性写入4个字节,提高效率
            while ((len = fis.read(bytes))!=-1){
                fos.write(bytes,0,len);     //要把len写进去,不然可能出现写入重复数据
            }
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

3.4、字符流

import java.io.FileReader;
import java.io.FileWriter;

/**
 * @author RuiMing Lin
 * @date 2020-03-11 19:28
 */
public class Demo2 {
    public static void main(String[] args) throws Exception{
        try (FileReader fr = new FileReader("fr.txt");
             FileWriter fw = new FileWriter("fw.txt",true);){
            int len = 0;
            char[] chars = new char[4];
            while ((len = fr.read(chars))!=-1){
                fw.write(chars, 0, len);
            }

        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

3.5、缓冲流

  不使用数组时,每次都是一个字节一个字节读取,效率很慢;使用数组的话,每次按数组长度读取字节,效率提高了;使用缓冲流,查看源码可知,每次读写8192个字节,效率显著提升。

import com.sun.org.apache.bcel.internal.generic.NEW;

import java.io.*;

/**
 * @author RuiMing Lin
 * @date 2020-03-11 19:28
 */
public class Demo2 {
    public static void main(String[] args) throws Exception{
        test1();
        test2();
        test3();
    }

    public static void test1() {
        // 记录开始时间
        long start = System.currentTimeMillis(); // 创建流对象
        try (FileInputStream fis = new FileInputStream("cloudmusic.exe");
             FileOutputStream fos = new FileOutputStream("copy.exe");){
            int b;
            while ((b = fis.read()) != -1){
                fos.write(b);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        long end = System.currentTimeMillis();
        System.out.println("不使用数组复制时间:"+(end - start)+" 毫秒");
    }
    public static void test2() {
        // 记录开始时间
        long start = System.currentTimeMillis(); // 创建流对象
        try (FileInputStream fis = new FileInputStream("cloudmusic.exe");
             FileOutputStream fos = new FileOutputStream("copy.exe");){
            int b;
            byte[] bytes = new byte[32];
            while ((b = fis.read(bytes)) != -1){
                fos.write(bytes,0,b);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        long end = System.currentTimeMillis();
        System.out.println("使用数组复制时间:"+(end - start)+" 毫秒");
    }
    public static void test3(){
        // 记录开始时间
        long start = System.currentTimeMillis(); // 创建流对象
        try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream("cloudmusic.exe"));
             BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("copy.exe"));) {
            int b;
            while ((b = bis.read()) != -1) {
                bos.write(b);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        long end = System.currentTimeMillis();
        System.out.println("使用缓冲流复制时间:"+(end - start)+" 毫秒");
    }
}

结果输出为:

不使用数组复制时间:6500 毫秒
使用数组复制时间:180 毫秒
使用缓冲流复制时间:40 毫秒

3.6、序列化流

  Java提供了一种对象序列化的机制。用一个字节序列可以表示一个对象,该字节序列包含该对象的属性、方法 等信息。我们可以通过将对象写进文件实现对象的持久化。 反之,该字节序列还可以从文件中读取回来,重构对象,称为反序列化
  一个对象要想实现序列化,必须满足两个条件: ①该类必须实现 java.io.Serializable 接口,不实现此接口的类将不会使任何状态序列化或反序化,会抛出 NotSerializableException 异常。②该类的所有属性必须是可序列化的。如果有一个属性不需要可序列化的,则该属性必须注明是瞬态的,使用 transient 关键字修饰。

定义一个Person类:

import java.io.Serializable;
/**
 * @author RuiMing Lin
 * @date 2020-03-11 21:05
 */
public class Person implements Serializable {
    private String name;      //
    public transient int age;	//transient瞬态修饰成员,不会被序列化
    public Person(String name) {
        this.name = name;
    }
    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                '}';
    }
}

ObjectInputStream和ObjectOutputStream 实现对象的序列化和反序列化

Person person = new Person("小明");
person.age = 20;        // age已经定义不参与序列化,故不能加入构造方法中
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("fis.txt"));
oos.writeObject(person);    //讲person对象写入文件
oos.close();        //关流

ObjectInputStream ois = new ObjectInputStream(new FileInputStream("fis.txt"));
Person person1 = (Person) ois.readObject();
System.out.println("person1 = " + person1);
ois.close();

4、NIO

4.1、NIO介绍

  NIO 全称non-blocking IO,是指JDK 提供的关于IO流的新 API。从 JDK1.4 开始,Java 提供了 一系列改进的输入/输出的新特性,被统称为 NIO(即 New IO)。新增了许多用于处理输入输出的类,这些类都被放在 java.nio 包及子包下,并且对原 java.io 包中的很多类进行改写,新增了满足 NIO 的功能。

4.2、阻塞与非阻塞

阻塞: 传统的IO流是阻塞的,当一个线程调用读写方法时,该线程就会被阻塞,即不能进行其它操作,直到读写结束。如果使用传统IO进行网络通信,由于线程会阻塞,而且只能处理服务端与一个客户端的通信,会造成需要大量的线程,这样会给服务器很大的压力。
非阻塞: NIO是非阻塞的,当线程从某通道进行读写数据时,若没有数据可以用。该线程便会转向其他任务,这样极大地节省了时间和提高效率。同时,一个线程可以处理多个IO连接通道,也能够减缓服务端的压力。

4.3、核心组件

4.3.1、Channel

  Channel(通道)是NIO读取数据的通道,是对BIO中输入流和输出流的强化。它提供了一个map()方法,可以将一块数据映射到内存中,所以说NIO是面向块的。此外,Channel的传输是双向的,可以同时进行读和写操作。

4.3.2、Buffer

  Buffer(缓冲区)实际上是一个特殊的数组,因为它内置了三个属性,所以可以跟踪和记录缓冲区的状态变化,进而实现更加复杂的操作。其中,最重要的就是容量(capacity)、界限(limit)、位置(position)
容量:缓冲区能够容纳的数据元素的最大数量,这一个容量在缓冲区被初始化时确定;
界限:缓冲区的第一个不能被读或写的元素;
位置:下一个要被读或写的元素的索引,位置会自动由相应的 get( )和 put( )函数更新;
标记:可直接将position定位到mark。
在这里插入图片描述

4.3.3、Selector

  Selector(选择器)能够检测多个注册的通道上是否有事件发生。如果有事件发生,便获取事件然后针对每个事件进行相应的处理。这样就可以只用一个单线程去管理多个通道,也就是管理多个连接。这样使得只有在连接真正有读写事件发生时,才会调用函数来进行读写,就大大地减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程,并 且避免了多线程之间的上下文切换导致的开销。
  简单来说,就是一个线程拥有一个Selector,而一个Selector管理多个读写IO,这样即使一个IO没有进行读写操作,也不会造成线程阻塞,导致性能降低。

4.4、操作

4.4.1、写操作

package nio;
import java.io.FileOutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
/**
 * @author RuiMing Lin
 * @date 2020-03-14 14:52
 */
public class Demo1 {
    public static void main(String[] args) throws Exception{
        // 1.创建输出流
        FileOutputStream fos = new FileOutputStream("fos.txt");
        // 2.获取通道
        FileChannel fileChannel = fos.getChannel();
        // 3.提供一个缓冲区
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        // 4.往缓冲区存入数据
        String string = "hello,nio!";
        buffer.put(string.getBytes());
        // 5.翻转
        buffer.flip();
        // 6.把缓冲区写入通道
        fileChannel.write(buffer);
        // 7.关闭
        fos.close();
    }
}

4.4.2、读操作

package nio;

import java.io.File;
import java.io.FileInputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

/**
 * @author RuiMing Lin
 * @date 2020-03-14 15:29
 */
public class Demo2 {
    public static void main(String[] args) throws Exception{
        //1.获取文件,创建输入流
        File file = new File("fis.txt");
        FileInputStream fileInputStream = new FileInputStream(file);
        // 2.获取通道
        FileChannel fileChannel = fileInputStream.getChannel();
        // 3.提供一个缓冲区
        ByteBuffer buffer = ByteBuffer.allocate((int) file.length());
        // 4.读取通道的数据并保存在缓冲区中
        fileChannel.read(buffer);
        // 5.获取缓冲区数据
        System.out.println(new String(buffer.array()));
        // 6.关流
        fileInputStream.close();
    }
}

4.4.3、复制操作

package nio;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

/**
 * @author RuiMing Lin
 * @date 2020-03-14 15:29
 */
public class Demo2 {
    public static void main(String[] args) throws Exception{
        // 1.获取文件
        FileInputStream fis=new FileInputStream("fis.txt"); 
        FileOutputStream fos=new FileOutputStream("fos.txt"); 
        // 2.获取通道
        FileChannel sourceCh = fis.getChannel(); 
        FileChannel destCh = fos.getChannel(); 
        // 3.复制
        destCh.transferFrom(sourceCh, 0, sourceCh.size()); 
        // 4.关流
        sourceCh.close();
        destCh.close();
    }
}

4.5、使用NIO实现网络传输

4.5.1、网络通信

  NIO最大用处就是用于网络IO,因为网络IO会产生高并发高访问的情况。上面演示代码中进行文件IO时用到的 FileChannel并不支持非阻塞操作。NIO 中的网络通道是非阻塞 IO 的实现,基于事件驱动,非常适用于服务器需要维持大量连接,但是数据交换量不大的情况,例如一些即时通信的服务等等。

在Java中编写 Socket 服务器,通常有以下几种模式:

  1. 一个客户端连接用一个线程:
    优点:程序编写简单;
    缺点:当连接非常多时,分配的线程也会非常多,服务器可能会因为资源耗尽而崩溃。
  2. 每一个客户端连接交给一个拥有固定数量线程的连接池:
    优点:程序编写相对简单,可以处理大量的连接;
    缺点:线程的开销非常大,连接如果非常多,排队现象会比较严重。
  3. 使用 Java 的NIO
    优点:用非阻塞的 IO 方式处理。这种模式可以用一个线程,通过selector处理大量的客户端连接。
    在这里插入图片描述

4.5.2、实现网络非阻塞通信的小案例

定义一个客户端类:

package nio;

import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;

/**
 * @author RuiMing Lin
 * @date 2020-03-14 19:49
 */
public class NIOClient {
    public static void main(String[] args) throws Exception{
        // 1.得到网络通道
        SocketChannel channel = SocketChannel.open();
        // 2.设置非阻塞
        channel.configureBlocking(false);
        // 3.提供服务器的IP+端口号
        InetSocketAddress address = new InetSocketAddress("127.0.0.1", 9999);
        // 4.连接服务器
        if (!channel.connect(address)) {        //如果连接不上
            while (!channel.finishConnect()) {  //继续连,此时并不阻塞线程
                System.out.println("client:连接服务器端的同时,我还可以做别的事!");
            }
        }
        // 5.得到一个缓冲区并存入数据
        String string = "hello,服务端";
        ByteBuffer buffer = ByteBuffer.wrap(string.getBytes());
        // 6.发送数据
        channel.write(buffer);
        System.in.read();
    }
}

定义一个服务端类:

package nio;

import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;

/**
 * @author RuiMing Lin
 * @date 2020-03-14 20:01
 */
public class NIOServer {
    public static void main(String[] args) throws Exception{
        // 1.得到ServerSocketChannel对象
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        // 2.得到Selector对象
        Selector selector = Selector.open();
        // 3.绑定端口号
        serverSocketChannel.bind(new InetSocketAddress(9999));
        // 4.设置非阻塞
        serverSocketChannel.configureBlocking(false);
        //  5.注册selector
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        while (true){
            //6.1 监控客户端
            if(selector.select(2000)==0){//nio 非阻塞式的优势
                System.out.println("Server:没有客户端搭理我,我就干点别的事");
                continue;
            }
            //6.2 得到 SelectionKey,判断通道里的事件
            Iterator<SelectionKey> keyIterator=selector.selectedKeys().iterator();
            while(keyIterator.hasNext()) {
                SelectionKey key = keyIterator.next();
                if (key.isAcceptable()) {
                    //客户端连接请求事件
                    System.out.println("OP_ACCEPT");
                    SocketChannel socketChannel = serverSocketChannel.accept();
                    socketChannel.configureBlocking(false);
                    socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
                }
                if (key.isReadable()) {
                    //读取客户端数据事件
                    SocketChannel channel = (SocketChannel) key.channel();
                    ByteBuffer buffer = (ByteBuffer) key.attachment();
                    channel.read(buffer);
                    System.out.println("客户端发来数据:" + new String(buffer.array()));
                }
                // 6.3 手动从集合中移除当前 key,防止重复处理
                keyIterator.remove();
            }
        }
    }
}

先启动服务端类:此时没有客户端连接服务端,但是并没有造成线程阻塞!
在这里插入图片描述
再启动客户端类:
在这里插入图片描述
当selector检测客户端有动作时,服务端接受数据。检测到客户端没有动作时,服务端开启的线程仍然可以进行其他操作!

5、AIO

  AIO(Asynchronous IO)是jdk7才有的,在进行 IO 编程中,常用到两种模式:Reactor 和 Proactor。NIO 就是采用 Reactor模式,当有事件触发时,服务器端得到通知,进行相应的处理。AIO即NIO2.0,叫做异步非阻塞IO。AIO 引入异步通道的概念,采用了 Proactor 模式, 简化了程序编写,一个有效的请求才启动一个线程,它的特点是先由操作系统完成后才通知服务端程序启动线程去处理,一般适用于连接数较多且连接时间较长的应用。

  目前AIO还没有广泛应用,现在流行的网络框架netty也是基于nio实现的。翻阅了两本Java的书,都对AIO的API没有详细介绍,都只是介绍了它的特性。。等以后有机会学习到再回来补充这篇博客吧

  IO的方式通常分为三种:同步阻塞的BIO、同步非阻塞的NIO、异步非阻塞的AIO。BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并 发局限于应用中,但程序直观简单易理解。NIO 方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂。AIO方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂。

有错误的地方敬请指出!觉得写得可以的话麻烦给个赞!欢迎大家评论区或者私信交流!

发布了30 篇原创文章 · 获赞 72 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/Orange_minger/article/details/104758663
今日推荐