Java基础之IO模型

同步和异步

  • 同步和异步是针对应用程序和内核的交互而言

    • 同步

      1. 用户进程触发 IO 操作
      2. 等待或轮询的去查看 IO 操作是否就绪
    • 异步:

      1. 用户进程触发 IO 操作以后便开始做自己的事情
      2. IO 操作完成时会得到 IO 完成的通知
  • 例如:银行取款

    • 同步:亲自持银行卡到银行取钱
      • 使用同步 IO 时,Java 自己处理 IO 读写
    • 异步:委托三方拿银行卡到银行取钱,然后给你
      • 使用异步 IO 时,JavaIO 读写委托给 OS 处理
        • 需要将数据缓冲区地址和大小传给 OS (银行卡和密码),OS 需要支持异步 IO 操作 API

阻塞和非阻塞

  • 阻塞和非阻塞:针对于进程在访问数据时,根据 IO 操作就绪状态采取的不同方式

    • 是一种读取或者写入操作方法的实现方式

    • 阻塞方式:读取或者写入函数将一直等待

    • 非阻塞方式:读取或者写入方法会立即返回一个状态值

  • 例如:银行取款

    • 阻塞: ATM 排队取款,只能等待
      • 使用阻塞 IO 时,Java 调用会一直阻塞到读写完成才返回
    • 非阻塞:柜台取款,取号然后等广播会通知办理,没到号就不能去,可以不断询问,如果说还没到就不能去
      • 使用非阻塞IO时,如果不能读写 Java 调用会马上返回
        • IO 事件分发器通知可读写时再继续进行读写,不断循环直到读写完成

BIO 编程

BIO

  • Blocking IO: 同步阻塞的编程方式

    • BIO 编程方式通常是在 JDK1.4 版本之前常用的编程方式
  • 单独的 TCP 网络开发虽然已经实现了网络程序开发

    • 服务器只能为一个客户端线程提供服务

    • 若有多人连接访问时无法提供服务

  • 将每一个连接到服务器的客户端都通过线程对象处理

    • 即 服务器上启动多个线程,每个线程单独为一个客户端服务
    • 同步并阻塞:服务器实现模式为一个连接一个线程
      • 客户端有连接请求时服务器端就需要启动一个线程进行处理
  • BIO 方式适用于连接数目比较小且固定的架构

    • 对服务器资源要求比较高,并发局限于应用中
    • JDK1.4以前的唯一选择,但程序直观简单易理解
  • 缺点

    1. 监听客户端连接时(serverSocket.accept())处于阻塞状态,不能处理其他事务
    2. 需要为每个客户端建立一个线程,虽然用线程池来优化,但并发较大时,线程开销依旧很大
    3. 当连接的客户端没有发送数据时,服务器端会阻塞在read操作上,等待客户端输入,造成线程资源浪费
  • 在这里插入图片描述

实现过程

  1. 服务端启动一个 ServerSocket 监听网络请求

  2. 客户端启动 Socket 发起网络请求

    • 默认情况下 ServerSocket 会建立一个线程处理此请求
  3. 如果服务端没有线程可用,客户端会阻塞等待或遭到拒绝

  4. 有线程响应,客户端线程会等待请求结束后,再继续执行

    • 建立好的连接,在通讯过程中是同步的,在并发处理效率上比较低
    /* ~~~ 服务器端代理线程 ~~~ */
    class EchoThread implements Runnable{
        private Socket client;
        public EchoThread(Socket client){
            this.clien = client;												 	// 被代理对象
        }
        @Override
        public void run() {
            try{
                Scanner scanner= new Scanner(client.getInputStream());				// 输入流,读取客户端信息
                PrintStream out= new PrintStream(client.getOutputStream());			// 输出流,返回客户端信息
                boolean flag = true;												// 控制程序结束
                while(flag){
                    if(scanner.hasNext()){
                        String str = scanner.next().trim();							// 获取客户端信息并去除前后空格
                        //程序结束
                        if(str.equalsIgnoreCase("byebye")){							// 匹配到输入 byebye 时控制程序结束
                            out.println("bye~~~~~~~~~~~~~~~");
                            flag = false;
                        }else{
                            out.println("ECHO:"+str);								// 返回客户端接收到的数据
                        }
                    }
                }
                scanner.close();													// 关闭流
                out.close();
                client.close();														// 关闭客户端连接
            }catch(Exception e){
                e.printStackTrace();
            }
        }
        public class TestDemo{
            public static void main(String[] args) throws Exception{
                ServerSocket server = new ServerSocket(9999);						// 创建服务器对象
                System.out.println("等待客户端连接=====");
                while(true){														// 持续监听并开启单独线程
                    Socket client = server.accept();								// 等待客户端连接
                    new Thread(new EchoThread(client)).start();						// 有客户端连接时开启一个线程
                }
                server.close();
            }
        }
    

NIO 编程

介绍

  • Unblocking IONew IO):同步非阻塞的编程方式

    • NIO本身是基于事件驱动思想来完成
    • 主要想解决 BIO 的大并发问题,出现于JDK 1.4之后
    • 相对于BIO出现了几个核心的组件
      • Selector:选择器
      • Channle:通道
      • Buffer:缓冲区
  • NIO 最重要的:当一个连接创建后,不需要对应一个线程

    • 连接会被注册到多路复用器上面
      • 所以所有的连接只需要一个线程就可以搞定
    • 当线程中的多路复用器进行轮询的时候,发现连接上有请求的话,才开启一个线程进行处理
      • 一个请求一个线程模式
      • 当连接没有数据时,没有工作线程来处理
  • NIO 的处理方式:当一个请求来,开启线程进行处理,可能会等待后端应用的资源(JDBC连接等)

    • 其实线程已经被阻塞,并发量大时还会有 BIO 一样的问题
  • 适用连接数目多且连接比较短(轻操作)的架构

    • 比如聊天服务器
    • 并发局限于应用中
    • 编程比较复杂,JDK1.4开始支持
  • 在这里插入图片描述

  • Reactor:一种事件处理模式

    • 用于处理通过一个或多个输入同时交付给服务处理程序的服务请求
    • 服务处理程序对传入的请求进行多路分解,并将它们同步分发到关联的请求处理程序
    • NIO 实现多路复用的一种模式
    • socket 有流可读或可写入 socket 时,操作系统会相应的通知引用程序进行处理
      • 应用再将流读取到缓冲区或写入操作
  • Netty :基于 Java NIO 类库的异步通信框架

    • 架构特点:异步非阻塞、基于事件驱动、高性能、高可靠性和高可定制性
    • 可以很好地替代掉繁琐的、难以使用的Java NIO类库,并提供更多可用的功能
    • 实际应用
      1. 高性能 HTTP 服务器
      2. 高性能 RPC 框架;例如 Dubbo
      3. Redission(Redis For Java)
      4. IM即时通信应用
      5. 大量需要网络通信的框架底层实现

结构

Buffer
本质
  • NIO 是面向缓冲区, 或面向块编程的

    • NIO 的 IO 传输中,数据会先读入到缓冲区
    • 当需要时再从缓冲区写出
    • 减少了直接读写磁盘的次数,提高了IO传输的效率
  • 本质上是可以读写数据的内存块

    • 即在内存空间中预留了一定的存储空间,用来缓冲输入和输出的数据
    • 预留的存储空间就叫缓冲区。
  • NIO 程序中,通道 channel 负责数据的传输

    • 但是输入和输出的数据都必须经过缓冲区buffer
  • 在这里插入图片描述

使用
  • 缓冲区的相关类都在 java.nio 包下,最顶层是 Buffer 抽象类

  • 重要属性

    • mark:标记

    • position:位置,下一个要被读或写的元素的索引,每次读写缓冲区都会改变该值,为下次读写做准备

    • limit:缓冲区的终点,不能对缓冲区中超过极限的位置进行读写操作,且极限是可修改的

    • capacity:容量,即缓冲区的最多可容纳的数据量,该值在创建缓冲区时被设立,且不可修改

  • 常用方法

    • 方法作用
      int capacity()返回缓冲区容量
      int position()返回缓冲区位置
      int position(int newPosition)设置缓冲区位置
      int limit()返回缓冲区限制
      Buffer limit(int newLimit)设置缓冲区限制
      Buffer mark()在缓冲区位置设置标记
      Buffer reset()将缓冲区位置设置为先前标记位置
      Buffer clear()清除缓冲区,各属性恢复到默认值;底层数组数据未清空
      Buffer flip()反转缓冲区
      Buffer rewind()倒带缓冲区
      int remaining()返回当前位置到限制之间的元素数
      boolean remaining判断当前位置到限制之间是否存在元素
      abstract boolean isReadOnly()缓冲区是否是只读
      abstract boolean hasArray()缓冲区是否有可访问的底层实现数组
      abstract Object array()返回缓冲区的底层实现数组
      abstract int arrayOffset返回缓冲区底层实现数组的第一个缓冲区元素偏移量
      abstract booelan isDirect缓冲区是否是直接缓冲区
常用子类
  • 最大区别在于底层实现数组的数据类型

    • ByteBuffer:存储字节数据到缓冲区

    • CharBuffer:存储字符数据到缓冲区

    • IntBuffer:存储整型数据到缓冲区

    • ShortBuffer:存储短整型数据到缓冲区

    • LongBuffer:存储长整型数据到缓冲区

    • FloatBuffer:存储浮点型数据到缓冲区

    • DoubleBuffer:存储双精度浮点型数据到缓冲区

ByteBuffer
  • 所有子类中,最常用的是 ByteBuffer

  • 常用方法

    • 方法作用
      ByteBuffer allocateDirect(int capacity)创建直接缓冲区,指定容量
      ByteBuffer allocate(int capacity)创建缓冲区,指定容量
      ByteBuffer wrap(byte[] array)创建缓冲区,指定底层实现数组,缓冲区容量、限制为数组大小,位置 0
      ByteBuffer wrap(byte[] array, int offSet, int length)创建缓冲区,指定底层实现数组,缓冲区容量为数组大小,缓冲区限制为 offeSet + length,位置 offSet
      abstract byte get()获取当前位置 position 数据,获取后 position 自动 + 1
      abstract byte get(int index)获取指定位置 index 的数据
      abstract ByteBuffer put(byte b)当前位置 position 放入数据 b,放入后 position + 1
      abstract ByteByffer put(int index, byte b)指定位置 index 放入数据 b
Channel
介绍
  • NIO 程序中服务器端和客户端之间的数据读写不是通过流,而是通过通道

  • 类似于流,都是用来读写数据的,但也有区别的

    • 通道是双向的,即可以读也可以写

      • 流是单向的,只能读或写
    • 通道可以实现异步读写数据

    • 通道可以从缓冲区读数据,也可以把数据写入缓冲区

  • 相关类在 java.nio.channel 包下

    • Channel是一个接口,常用的实现类

      • FileChannel:用于文件的数据读写

        • 真正的实现类为 FileChannelImpl
      • DatagramChannel:用于 UDP 的数据读写

        • 真正的实现类为 DatagramChannelImpl
      • ServerSocketChannel:用于监听 TCP 连接

        • 每当有客户端连接时都会创建 SocketChannel,功能类似 ServerSocket
        • 真正的实现类为 ServerSocketChannelImpl
      • SocketChannel: 用于 TCP 的数据读写

        • 功能类似 节点流 + Socket
        • 真正的实现类为 SocketChannelImpl
FileChannel
  • 主要用于对本地文件进行IO操作,如文件复制等

  • 常用方法

    • 方法作用
      int read(ByteBuffer bst)将通道中数据读入缓冲区 bst
      int write(ByteBuffer src)将缓冲区 src 的数据写入通道
      long transferTo(long position, long count, WritableByteChannel target)将通道数据复制到目标通道 target,复制起始位置 position,复制数据量 count
      long transferFrom(ReadableByteChannel src, long position, long count)将目标通道 src 的数据复制到此通道,复制起始位置 position,复制数据量 count
  • 属性 channel:默认是空的

    • 通过流中的 getChanel() 方法根据当前文件流的属性生成对应的 FileChannel

      • public FileChannel getChannel() {
                synchronized (this) {
                    if (channel == null) {
                        channel = FileChannelImpl.open(fd, path, false, true, append, this);
                    }
                    return channel;
                }
            }
        }
        
Selector
Selector
  • NIO 程序中,用选择器 Selector 实现 一个选择器处理多个通道,即一个线程处理多个连接

    • 把通道注册到 Selector 上,就可通过 Selector 来监测通道
    • 如果通道有事件发生,便获取事件通道然后针对每个事件进行相应的处理
    • 只有在通道(连接)有真正的读/写事件发生时,才会进行读写操作
      • 大大减少了系统开销,不必为每个连接创建线程,不用维护过多线程
  • 相关类在 java.nio.channels 包和其子包下

    • 顶层类是 Selector 抽象类

    • 常用方法

      • 方法作用
        Selector open()开启选择器
        boolean isOpen()查看选择器是否已经开启
        int select()检查所有注册的通道是否有事件发生,返回发生事件的通道数量;此方法会阻塞,直到注册的通道有事件发生
        int select(long timeout)检查所有注册的通道是否有事件发生,检查时长 timeout ,返回发生事件的通道数量
        int selectNow()检查所有注册的通道是否有事件发生,所有注册通道检查一遍就返回(不阻塞),返回发生事件的通道数量
        Set<SelectionKey> keys()返回所有注册通道对应的 SelectionKey 集合
        Set<SelectionKey> delectedKeys()返回所有发生事件的通道对应的 SelectionKey 集合
通道注册
  • ServerSocketChannelSocketChannel 类都有注册方法:register(Selector sel, int ops)

    • sel:要注册到的选择器
    • ops:该通道监听的操作事件的类型
    • 通过该方法将 ServerSocketChannelSocketChannel 注册到目标选择器中
    • 方法会返回 SelectionKey 储存在注册的 SelectorpublicKeys 集合属性
      • 真正实现类:SelectionKeyImpl
      • SelectionKey 储存通道的事件类型和该注册的通道对象
      • 通过 SelectionKey.channel() 方法获取 SelectionKey 对应的通道

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-neUkupEz-1660831390542)(images/1563713-20201105210144662-934002093.png)]

选择器检查
  • 通过选择器的检查方法,如select() 得知发生事件的通道数量

    • 数量大于为 0 时,至少有一个通道发生了事件
  • 使用 selectedKeys() 方法获取所有发生事件的通道对应的 SelectionKey

    • 通过 SelectionKey 中的方法来判断对应通道中需处理的事件类型,根据事件做出相应的处理。
  • public final boolean isReadable() { 				// 判断是否是读操作
        return (readyOps() & OP_READ) != 0;
    }
    
    public final boolean isWritable() { 				// 判断是否是写操作
        return (readyOps() & OP_WRITE) != 0;
    }
    
    public final boolean isConnectable() { 				// 判断是否是连接操作
        return (readyOps() & OP_CONNECT) != 0;
    }
    
    public final boolean isAcceptable() { 				// 判断是否是接收操作
        return (readyOps() & OP_ACCEPT) != 0;
    }
    

在这里插入图片描述

AIO编程

  • Asynchronous IO: 异步非阻塞的编程方式

    • JDK1.7 中被称为 NIO.2
  • NIO 的进一步增强

    • 新增的类

      • AsynchronousChannel:支持异步通道
        • 包括服务端 AsynchronousServerSocketChannel 和普通 AsynchronousSocketChannel 等实现
      • CompletionHandler:用户处理器
        • 定义一个用户处理就绪事件的接口,由用户自己实现,异步io的数据就绪后回调该处理器消费或处理数据
      • AsynchronousChannelGroup:用于资源共享的异步通道集合
        • 处理 IO 事件和分配给 CompletionHandler
    • 主要在 java.nio.channels 包下增加四个异步通道

      • AsynchronousServerSocketChannel

        • 提供了 open()静态工厂
        • bind() 方法:绑定服务端IP地址、端口号
        • accept():用于接收用户连接请求
      • AsynchronousSocketChannel

        • 提供 open() 静态工厂方法
        • 还提供了 read()write() 方法
      • AsynchronousFileChannel

      • AsynchronousDatagramChannel

  • NIO 不同:进行读写操作时,只须直接调用 APIreadwrite 方法

    • 两种方法均为异步
      • 读操作:当有流可读取时,操作系统会将可读的流传入 read 方法的缓冲区,并通知应用程序
      • 写操作:当操作系统将 write 方法传递的流写入完毕时,操作系统主动通知应用程序
        • 即 read / write 方法都是异步的,完成后会主动调用回调函数
  • 发出事件(acceptreadwrite 等)后要指定事件处理类(回调函数)

    • AIO 中的事件处理类:CompletionHandler<V,A>
      • 定义如下两个方法,分别在异步操作成功和失败时被回调
        • void completed(V result, A attachment);
        • void failed(Throwable exc, A attachment);
  • 适用于连接数目多且连接比较长(重操作)的架构

    • 比如相册服务器
    • 充分调用OS参与并发操作
    • 编程比较复杂,JDK7开始支持
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值