深入剖析BIO到NIO演变史

导读

  • BIO、NIO、AIO的区别?
  • 他们各自的特点是什么?
  • NIO如何实现多路复用?
  • 同/异步、阻/非阻塞的区别是什么?

 BIO、NIO、AIO的区别

  1. BIO 就是传统的 java.io 包,它是基于流模型实现的,交互的方式是同步、阻塞方式,也就是说在读入输入流或者输出流时,在读写动作完成之前,线程会一直阻塞在那里,它们之间的调用时可靠的线性顺序。它的有点就是代码比较简单、直观;缺点就是 IO 的效率和扩展性很低,容易成为应用性能瓶颈。
  2. NIO 是 Java 1.4 引入的 java.nio 包,提供了 Channel、Selector、Buffer 等新的抽象,可以构建多路复用的、同步非阻塞 IO 程序,同时提供了更接近操作系统底层高性能的数据操作方式。
  3. AIO 是 Java 1.7 之后引入的包,是 NIO 的升级版本,提供了异步非堵塞的 IO 操作方式,所以人们叫它 AIO(Asynchronous IO),异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。

通过张表简单理解一下三者的区别:

名称

jdk支持版本

特征

IO(BIO)

1.4之前

同步阻塞输入输出

NIO

1.4开始

同步非阻塞

AIO

1.7开始

异步非阻塞

 

同步、异步、阻塞、非阻塞 

上面说了很多关于同步、异步、阻塞和非阻塞的概念,接下来就具体聊一下它们4个的含义,以及组合之后形成的性能分析。

a.同步与异步

同步就是一个任务的完成需要依赖另外一个任务时,只有等待被依赖的任务完成后,依赖的任务才能算完成,这是一种可靠的任务序列。要么成功都成功,失败都失败,两个任务的状态可以保持一致。而异步是不需要等待被依赖的任务完成,只是通知被依赖的任务要完成什么工作,依赖的任务也立即执行,只要自己完成了整个任务就算完成了。至于被依赖的任务最终是否真正完成,依赖它的任务无法确定,所以它是不可靠的任务序列。我们可以用打电话和发短信来很好的比喻同步与异步操作。

b.阻塞与非阻塞

阻塞与非阻塞主要是从 CPU 的消耗上来说的,阻塞就是 CPU 停下来等待一个慢的操作完成 CPU 才接着完成其它的事。非阻塞就是在这个慢的操作在执行时 CPU 去干其它别的事,等这个慢的操作完成时,CPU 再接着完成后续的操作。虽然表面上看非阻塞的方式可以明显的提高 CPU 的利用率,但是也带了另外一种后果就是系统的线程切换增加。增加的 CPU 使用时间能不能补偿系统的切换成本需要好好评估。

 一、IO介绍

IO的全称其实是:Input/Output的缩写。也就是我们常说的BIO。

1.1 全面认识IO

传统的 IO 大致可以分为4种类型:

  • InputStream、OutputStream 基于字节操作的 IO
  • Writer、Reader 基于字符操作的 IO
  • File 基于磁盘操作的 IO
  • Socket 基于网络操作的 IO

java.net 下提供的 Scoket 很多时候人们也把它归为 同步阻塞 IO ,因为网络通讯同样是 IO 行为。
java.io 下的类和接口很多,但大体都是 InputStream、OutputStream、Writer、Reader 的子集,所有掌握这4个类和File的使用,是用好 IO 的关键。

1.2 IO过程描述

 可以参考下阻塞式I/O模式图:

图解: 

BIO的同步过程:

  1.  Java # read 基于 Thread main 、 T1 、 T2 Java 虚拟机栈栈帧表示一个方法的执行 Thread :为什么会被执行? CPU 资源: Thread Lifecyle State 线程的阻塞与不阻塞旧层面.表示的当前线程除了此旧不能进行其他操作,阻塞
  2. 内核空间接收到 read 请求,进一步跟硬盘要数据
  3. 硬盘准备数据,把数据复制到内核空间的缓冲区
  4. 再把内核空间缓冲区的数据复制到用户空间 

1.3 Tread.State类

Tread类下面有一个State枚举,这个枚举包含了线程的生命周期:

1.NEW(新建状态)
至今尚未启动的线程的状态。 
2.RUNNABLE(可运行状态) 
可运行线程的线程状态。处于可运行状态的某一线程正在 Java 虚拟机中运行,但它可能正在等待操作系统中的其他资源,比如处理器。 
3.BLOCKED(阻塞状态) 
受阻塞并且正在等待监视器锁的某一线程的线程状态。处于受阻塞状态的某一线程正在等待监视器锁,以便进入一个同步的块/方法,或者在调用 Object.wait 之后再次进入同步的块/方法。 
4.WAITING(等待状态) 
某一等待线程的线程状态。某一线程因为调用下列方法之一而处于等待状态: 

  • 不带超时值的 Object.wait
  • 不带超时值的 Thread.join
  • Thread.sleep
  • 带有超时值的 Object.wait
  • 带有超时值的 Thread.join
  • LockSupport.parkNanos
  • LockSupport.parkUntil

LockSupport.park 
处于等待状态的线程正等待另一个线程,以执行特定操作。 例如,已经在某一对象上调用了 Object.wait() 的线程正等待另一个线程,以便在该对象上调用 Object.notify() 或 Object.notifyAll()。已经调用了 Thread.join() 的线程正在等待指定线程终止。 
5.TIMED_WAITING(定时等待状态)

具有指定等待时间的某一等待线程的线程状态。某一线程因为调用以下带有指定正等待时间的方法之一而处于定时等待状态: 

  • Thread.sleep
  • 带有超时值的 Object.wait
  • 带有超时值的 Thread.join
  • LockSupport.parkNanos
  • LockSupport.parkUntil

6.TERMINATED(死亡状态) 
已终止线程的线程状态。线程已经结束执行。 

2.1 BIO测试demo

2.1.1  BioServer.class

package io;

import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;

/**
 * 等待客户端请求过来
 * @author Qian
 * @Version 1.0
 * @Since JDK1.6
 * @Company Bangsun
 * @Date 2021/2/4 0:28
 */
public class BioServer {
    public static void main(String[] args) {
        try( ServerSocket serverSocket = new ServerSocket(8888);) {
            System.out.println("BioServer已经启动,监听接口:"+serverSocket.getLocalSocketAddress());
            //为了处理很多客户端请求
            while (true){
                Socket clientSocket = serverSocket.accept();
                System.out.println("请求来自:"+clientSocket.getRemoteSocketAddress());
                //针对每个Socket进行数据交互的操作,看看是否真的阻塞?当前线程只能等待i/o完成 main-Thread
                try (Scanner input = new Scanner(clientSocket.getInputStream())){
                    //不断地和Socket进行数据交互
                    while (true){
                        String request = input.nextLine();
                        if ("quit".equals(request)){
                            break;
                        }
                        System.out.println(String.format("From %s : %s",clientSocket.getRemoteSocketAddress(),request));
                        String response = "From BioServer Hello " + request + ".\n";
                        clientSocket.getOutputStream().write(response.getBytes());
                    }
                }
            }
        }catch (Exception e){

        }
    }
}

2.1.2 测试结果

 1.启动main方法

2.telnet连接8888端口

3.输入字段,服务是有的反应的

4.再开启一个cmd,看看是否阻塞;再打字是没反应的,证明是阻塞住了

2.2 线程池实现多客户端交互demo

上述证明io是阻塞式的,我们是否可以通过线程池优化呢?

2.2.1 BioServerThreadPool.class

public class BioServerThreadPool {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(3);
        RequestHandler requestHandler = new RequestHandler();
        try(ServerSocket serverSocket = new ServerSocket(9999);) {
            System.out.println("BioServerThreadPool已经启动,监听接口:" + serverSocket.getLocalSocketAddress());
            while (true){
                Socket clientSocket = serverSocket.accept();
                //针对每个客户端分配一个线程
                executor.submit(new ClientHandler(clientSocket,requestHandler));
                System.out.println("连接来自于:"+clientSocket.getRemoteSocketAddress());
            }
        }catch (Exception e){

        }
    }
}

2.2.2 ClientHandler.class

public class ClientHandler implements Runnable{
    private final Socket clientSocket;
    private final RequestHandler requestHandler;

    public ClientHandler(Socket clientSocket,RequestHandler requestHandler){
        this.clientSocket=clientSocket;
        this.requestHandler=requestHandler;
    }
    @Override
    public void run() {
        //针对每个Socket进行数据交互的操作,看看是否真的阻塞?当前线程只能等待i/o完成 main-Thread
        try (Scanner input = new Scanner(clientSocket.getInputStream())){
            //不断地和Socket进行数据交互
            while (true){
                String request = input.nextLine();
                if ("quit".equals(request)){
                    break;
                }
                System.out.println(String.format("From %s : %s",clientSocket.getRemoteSocketAddress(),request));
                String response =requestHandler.handle(request);
                clientSocket.getOutputStream().write(response.getBytes());
            }
        }catch (Exception e){
            throw new RuntimeException();
        }
    }
}

2.2.3 RequestHandler.class

public class RequestHandler {
    public String handle(String request){
        return "来自于BioServerThreadPool Hello"+request +".\n";
    }
}

2.2.3 测试结果

1.启动

2. telnet连接8888端口

3.输入字段,服务是有的反应的

4.前三个都不阻塞,直到第四个才阻塞。

三、NIO

上面的线程池虽然能解决阻塞问题,但线程池依然治标不治本。

那么当服务端单线程时,多个客户端连接服务端,服务端可以同时处理吗?
——NIO

java NIO图解:

过程:

  1. 用户空间跟内核空间要数据的时候,如果数据还没准备好,那么Thread不阻塞
  2. Thread会主动不断的询问内核空间,数据有没有准备好
  3. 如果内核空间准备好了,那么数据就会从内核空间复制到用户空间【阻塞】

NIO模型图:

 

java中NIO常用的类

  • Channel:ServerSocketChannel、SocketChannel
  • Buffer
  • selector

Netty:本质上就是对NIO的封装和优化

NettyHandler:

  •  ctx:写数据
  • msg:读数据

Netty的应用:dubbo、rocketMq、spring5web flux等 

jdk1.7之后的 NIO2:伪异步的IO

NIO产生原因及应用:是因为业务的发展,单一架构——>分布式架构
对高性能IO的要求,RPC、异步通信MQ、Redis、springCloud、ELK等

3.1 NIO实现demo

3.3.1 NioServer.class

package io.nio;

import io.RequestHandler;

import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Set;

/**
 * @author Qian
 * @Version 1.0
 * @Since JDK1.6
 * @Company Bangsun
 * @Date 2021/2/4 22:19
 */
public class NioServer {
    public static void main(String[] args) throws Exception {
        //1.创建一个服务端的Channel
        ServerSocketChannel socketChannel = ServerSocketChannel.open();
        socketChannel.configureBlocking(false);
        //serverChannel需要绑定一个端口
        socketChannel.bind(new InetSocketAddress(6666));
        System.out.println("Nio 启动,监听端口:"+socketChannel.getLocalAddress());
        //2.selector,专门用来轮询,判断io的状态
        Selector selector = Selector.open();
        //将一个channel注册到selector,channel初始状态
        socketChannel.register(selector, SelectionKey.OP_ACCEPT);

        //ByteBuffer进行数据的临时存储
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        RequestHandler requestHandler = new RequestHandler();
        //对Selector里面的handler进行轮询,判断谁需要进行后续io操作
        while (true){
            int select = selector.select();
            if (select==0){
                continue;
            }
            //selector中有channel
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = selectionKeys.iterator();
            while (iterator.hasNext()){
                //selectionKeys已经保存了channel的各种信息
                SelectionKey key = iterator.next();
                //加入channel状态是acceptable
                if (key.isAcceptable()){
                    ServerSocketChannel channel = (ServerSocketChannel) key.channel();
                    SocketChannel clientChannel = channel.accept();
                    System.out.println("连接来自:"+clientChannel.getRemoteAddress());
                    clientChannel.configureBlocking(false);
                    //将该channel改变状态,read、write
                    clientChannel.register(selector,SelectionKey.OP_READ);
                }
                if (key.isReadable()){
                    //数据交互了
                    SocketChannel channel = (SocketChannel) key.channel();
                    //将数据读取到buffer
                    channel.read(buffer);
                    String request = new String(buffer.array()).trim();
                    buffer.clear();
                    //写数据
                    System.out.println(String.format("From %s : %s",channel.getRemoteAddress(),request));
                    String response = requestHandler.handle(request);
                    channel.write(ByteBuffer.wrap(request.getBytes()));
                }
                iterator.remove();
            }
        }
    }
}

1.启动

2.telnet 6666端口

3.输入字段,服务器有反应。且开启多个进程,不阻塞

四、AIO

jdk nio2 :伪异步io图解:

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值