BIO,BIO,AIO总结

理论

同步与异步

  • 同步:发起一个请求调用后,被调用这处理完请求之前,调用不返回
  • 异步:异步就是发起一个调用之后,立即得到被调用者的会员表示已接收到的请求,但是被调用者并没有返回结果,此时我们可以处理其他请求,被调用者通常依靠时间,会吊灯机制来通知调用者返回结果

同步和异步的最大区别在于异步的话调用者不需要等待处理的结果,被调用者会通过回调机制来通知调用者返回的结果

阻塞和非阻塞

  • 阻塞:急速发起一个请求,调用者一直等待请求结果返回,也就是当前线程会被挂起,无从事其他任务,只有当条件就绪才能继续
  • 非阻塞:发起一个请求,调用者不用一直等着返回结果,可以先去干其他的事情

(同步异步是针对被调用端的,阻塞和非阻塞是针对调用端的)

那么同步阻塞,同步非阻塞和异步非阻塞又代表着什么意思呢

举个生活中简单的例子,你妈妈让你烧水,小时候你比较笨啊,在哪里傻等着水开(同步阻塞)。等你稍微再长大一点,你知道每次烧水的空隙可以去干点其他事,然后只需要时不时来看看水开了没有(同步非阻塞)。后来,你们家用上了水开了会发出声音的壶,这样你就只需要听到响声后就知道水开了,在这期间你可以随便干自己的事情,你需要去倒水了(异步非阻塞)。

BIO(Blocking I/O)

同步阻塞I/O模式,数据的读取写入必须阻塞在一个线程内等待其完成

传统BIO

BIO通信(一请求一应答)模型图如下

采用BIO通信模型的服务端,通常由一个独立的Acceptor线程负责监听客户端的连接,我们一般通过在while(true)循环中会调用accept()方法接受客户端的链接方式监听请求,请求一旦收到一个 连接请求,就可以建立通信套接字在这个通信套接字上进行读操作,此时不能再接收其他客户端连接请求,只能等待同当前连接的客户端的执行操作完成,不过可以通过多线程来支持多个客户端的连接

如果要让BIO通信模型能够同时处理多个客户端请求,就必须使用多线程(主要原因是 socket.accept()socket.read()socket.write() 涉及的三个主要函数都是同步阻塞的),也就是说它在接收到客户端链接请求之后为每个客户端创建一个新的线程进行链路处理,处理完之后,通过输出流应答给客户端.线程销毁.这就是典型的一请求一应答通信模型.我们可以设想一下如果这个链接不作任何事情的话就会造成不必要的线程开销,不过可以通过线程池机制改善,线程池还可以让线程的创建和回收成本相对较低.使用FixeThreadPool可以有效的控制了线程的最大数量,保证了系统有限资源的控制,实现了N(客户请求数量)M(处理客户端请求的线程数量)的伪异步I/O模型(N可远远大于M),

当并发量增加后这种模型会出现什么问题?

在java虚拟机中,线程是宝贵的资源,线程的创建和销毁的成本很高,除此之外,线程切换成本也很高.尤其在linux这样的操作系统中,线程本质上就是一个进程,创建和销毁线程都是最垃圾的系统函.如果并发访问量增加会导致线程数急剧膨胀可能会导致线程堆栈溢出,创建新线程失败等问题,最终导致进程宕机或者僵死,不能提供对外服务

伪异步IO

为了解决同步阻塞I/O面临的一个链路需要一个线程处理的问题,后来有人对它的线程模型进行了优化__后端通过一个线程池来处理镀铬客户端的接入请求,形成客户端个数M:线程池最大线程数N的比例关系,其中M可远远大于N.通过线程池可灵活地调整线程池资源,设置线程最大值,防止由于海量并发接入导致线程池耗尽

采用线程池和队列任务可以实现一种叫做伪哦异步的I/O通信框架,它的模型如上图所示.当有新的客户端接入时,将客户端的Sokect封装成一个Task(实现java.lang.Runnable接口)投递后到后端的线程池中进行处理,JDK的线程池维护一个消息队列和N个活跃队列,对消息队列中的任务进行处理.由于线程池可以设置消息队列的大小和最大线程数,因此,它的资源占用是可控的,无论多少个客户端并发访问,都不会导致资源的耗尽和宕机.

伪异步I/O通信框架采用了线程池实现,因此避免了每个请求都创建一个独立的线程造成的线程资源的耗尽问题.不过因为它的底层仍然是同步阻塞BIO模型,因此无法从根本上解决问题

下面代码中演示了BIO通信.服务端还会为每个客户端创建一个线程来处理.

客户端:

public class IOClient {
    public static void main(String[] args) {
        // TODO 创建多个线程,模拟多个客户端连接服务端
        new Thread(() -> {
            try {
                Socket socket = new Socket("127.0.0.1", 3333);
                while (true) {
                    try {
                        socket.getOutputStream().write((new Date() + ": hello world").getBytes());
                        Thread.sleep(2000);
                    } catch (Exception e) {
                    }
                }
            } catch (IOException e) {
​
            }
        }).start();
 }

服务端

public class IOServer {
​
   public static void main(String[] args) throws IOException {
       // TODO 服务端处理客户端连接请求
       ServerSocket serverSocket = new ServerSocket(3333);
       // 接收到客户端连接请求之后为每个客户端创建一个新的线程进行链路处理
       new Thread(() -> {
           while (true) {
               try {
                   // 阻塞方法获取新的连接
                   Socket socket = serverSocket.accept();
                   // 每一个新的连接都创建一个线程,负责读取数据
                   new Thread(() -> {
                       try {
                           int len;
                           byte[] data = new byte[1024];
                           InputStream inputStream = socket.getInputStream();
                           // 按字节流方式读取数据
                           while ((len = inputStream.read(data)) != -1) {
                               System.out.println(new String(data, 0, len));
                           }
                       } catch (IOException e) {
                       }
                   }).start();
               } catch (IOException e) {
               }
           }
       }).start();
   }
}

NIO

简介

NIO是一种同步的非阻塞的I/O模型,在java1.4中引入了NIO框架,对应java.nio包,提供了selector,Buffer等抽象

NIO中的N可以理解为Non-blocking,不单纯的是New.它支持面向缓冲的,基于通道的I/O操作方法.NIO提供了与传统BIO模型中的Socket和ServerSocket相对应的SocketChannel和ServerSocketChannel俩种不同的套接字来实现,俩中通道都支持阻塞和非阻塞俩种模式.阻塞模式就像传统的支持一样,比较艰难,但是性能和可靠性都不好,非阻塞模式正好相反.对于低负载.低并发的应用程序,可以使用同步阻塞I/O来提升开发效率和更好的维护性;对于高负载,高并发的(网络)应用,应使用NIO的非阻塞模式来开发.

区别

IO流是阻塞的,NIO是不阻塞的

java NIO使我们可以进行非阻塞IO操作.比如说,单线程中通道读取到buffer,同时可以继续做别的事情,当数据读取到buffer中后线程再继续处理数据.写数据也是一样的.另外,非阻塞写也是如此.一个线程请求写入一些数据到某通道,但不需要等待它万千写入,这个线程同时可以去做别的事情.

java IO的各种流是阻塞的,这意味着,当一个线程调用read或write时,该线程被阻塞,直到有一些数据被读取,.或者数据完全写入.再次会期间不能再干任何事情了

IO面向流,而NIO面向缓冲区

Buffer是一个对象,它包含一些要写入或者要读出的数据.在NIO类库中,加入Buffer对象,体现了新库与原I/O的一个重要区别.在面向流的I/O中.可以直接讲数据写入或者直接读取到Stream对象中.虽然Stream中也有Buffer开头的扩展类,但是只是流的包装类,还是从流读到会出去,而NIO确实直接读到Buffer中进行操作

在NIO库中,所有数据都是缓冲区处理的.在读取数据时,它是直接读到缓冲区中的;在写入数据时,写入到缓冲区中.任何时候访问NIO都是通过缓冲区进行操作

最常用的缓冲区是ByteBuffer,一个ByteBuffer提供了一组功能用于操作byte数组.除了ByteBuffer还有其他的缓冲区,事实上,每一种java基本内存(除了Boolean类型)都有对应的一种缓冲区

Channel通道

NIO是通过Channel(通道)进行读写

通道是双向的,可读也可写,而流的读写是单向的.无论读写,通道只能和buffer交互.因为buffer,通道可以异步地读写

selectors选择器

NIO有选择器,io没有

选择器用于使用单个线程处理多个通道.因此它需要较少的线程来处理这些通道.线程之间的切换对于操作系统来说是昂贵的.因此为了提高系统效率选择器是有用的

NIO读取数据和写入方式

通常来说NIO中的所有IO都是从Channel(通道)开始的

  • 从通道进行数据读取:创建一个缓冲区,然后请求通道读取数据.
  • 从通道进行写入:创建一个缓冲区,填充数据,并要求通道写入数据

核心组件

  • Channel(通道)
  • Buffer缓冲区
  • selector选择器
public class NIOServer {
    public static void main(String[] args) throws IOException {
        // 1. serverSelector负责轮询是否有新的连接,服务端监测到新的连接之后,不再创建一个新的线程,
        // 而是直接将新连接绑定到clientSelector上,这样就不用 IO 模型中 1w 个 while 循环在死等
        Selector serverSelector = Selector.open();
        // 2. clientSelector负责轮询连接是否有数据可读
        Selector clientSelector = Selector.open();
        new Thread(() -> {
            try {
                // 对应IO编程中服务端启动
                ServerSocketChannel listenerChannel = ServerSocketChannel.open();
                listenerChannel.socket().bind(new InetSocketAddress(3333));
                listenerChannel.configureBlocking(false);
                listenerChannel.register(serverSelector, SelectionKey.OP_ACCEPT);
                while (true) {
                    // 监测是否有新的连接,这里的1指的是阻塞的时间为 1ms
                    if (serverSelector.select(1) > 0) {
                        Set<SelectionKey> set = serverSelector.selectedKeys();
                        Iterator<SelectionKey> keyIterator = set.iterator();
                        while (keyIterator.hasNext()) {
                            SelectionKey key = keyIterator.next();
                            if (key.isAcceptable()) {
                                try {
                                    // (1)
                                    // 每来一个新连接,不需要创建一个线程,而是直接注册到clientSelector
                                    SocketChannel clientChannel = ((ServerSocketChannel) key.channel()).accept();
                                    clientChannel.configureBlocking(false);
                                    clientChannel.register(clientSelector, SelectionKey.OP_READ);
                                } finally {
                                    keyIterator.remove();
                                }
                            }
                        }
                    }
                }
​
            } catch (IOException ignored) {
​
            }
​
        }).start();
​
        new Thread(() -> {
            try {
                while (true) {
                    // (2) 批量轮询是否有哪些连接有数据可读,这里的1指的是阻塞的时间为 1ms
                    if (clientSelector.select(1) > 0) {
                        Set<SelectionKey> set = clientSelector.selectedKeys();
                        Iterator<SelectionKey> keyIterator = set.iterator();
                        while (keyIterator.hasNext()) {
                            SelectionKey key = keyIterator.next();
                            if (key.isReadable()) {
                                try {
                                    SocketChannel clientChannel = (SocketChannel) key.channel();
                                    ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                                    // (3) 面向 Buffer
                                    clientChannel.read(byteBuffer);
                                    byteBuffer.flip();
                                    System.out.println(
                                            Charset.defaultCharset().newDecoder().decode(byteBuffer).toString());
                                } finally {
                                    keyIterator.remove();
                                    key.interestOps(SelectionKey.OP_READ);
                                }
                            }
                        }
                    }
                }
            } catch (IOException ignored) {
            }
        }).start();
    }
​

为什么大家都不愿意用 JDK 原生 NIO 进行开发呢?从上面的代码中大家都可以看出来,是真的难用!除了编程复杂、编程模型难之外,它还有以下让人诟病的问题:

  • JDK底层的NIO由epoll实现,盖世仙饱受诟病的空轮询bug会导致cpu飙升100%
  • 项目庞大之后,自行实现的NIO容易出现各类bug,维护成本较高

Netty的出现很大程度上改善了JDK元神NIO存在的一些问题

AIO

AIO也就是NIO2.在java7中引入了NIO的改进版NIO2,它是异步非阻塞的IO模型.异步IO是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,后台处理完成,操作系统会通知相应的线程进行后续的操作

AIO是异步IO的缩写,虽然NIO在网络操作中,提供了非阻塞的方法,但是NIO的IO行为还是同步的.对于NIO来说,我们的业务线程是在IO操作本身是同步的除了AIO的其他IO模型都是同步的,

Last modification:August 30, 2022
如果觉得我的文章对你有用,请随意赞赏