NIO
截止到目前Java共有三种IO模型:IO,NIO,AIO;
同步与异步:一个任务中的子步骤必须严格按照结果调用顺序执行就称为同步执行,而各个子步骤不需要依靠上一步子步骤的结果而启动就叫做异步,形象的讲同步就像是做算术题,环环相扣,异步就像是狗熊掰玉米,做一步算一步,子步骤间没有可强制的关联。
阻塞与非阻塞:阻塞就是因为当前任务资源被锁线程让出计算机资源,到时醒来或者被notify唤醒然后进入等待竞争队列,在此之前线程处于阻塞状态,相当于假死;非阻塞就是如果当前任务的一条执行语句因资源被阻塞就跳过这行代码继续下行代码的任务执行,此时线程状态仍然是运行状态。
IO就是我们熟知的阻塞同步IO,也就是BIO,传统的IO,遇到资源上锁就等待直到被资源占用线程唤醒开始竞争锁。。。;
NIO就是同步非阻塞IO,又叫NIO,新型IO,使用费用Channel通道的方式解决TCP连接耗时大,耗费资源大的难题,一个TCP连接可以复用多个Channel通道。
AIO就是异步非阻塞IO,原名是NIO2,看名字我大概可以猜测就是NIO加上回调的方式执行任务,不要求直接返回结果,至于结果怎么样返回这里不再讨论。这里主要讲NIO。
Java NIO,是一种同步非阻塞IO模型,多路复用IO的基础,成为解决高并发与大量连接,IO处理问题的有效方式。
下面会按照从传统IO和线程池模型面临的问题讲起,然后对比几种常见的IO模型,一步步分析NIO怎么利用事件模型处理IO,解决线程池瓶颈处理海量连接,包括利用面向事件的方式编写服务端/客户端程序。最后延申到一些高级主题,如Reactor和Proactor模型的对比。
先看下传统的同步阻塞IO也就是BIO的经典编程模型:
1 { 2 ExecutorService executor = Excutors.newFixedThreadPollExecutor(100);//线程池 3 4 ServerSocket serverSocket = new ServerSocket(); 5 serverSocket.bind(8088); 6 while(!Thread.currentThread.isInturrupted()){//主线程死循环等待新连接到来 7 Socket socket = serverSocket.accept(); 8 executor.submit(new ConnectIOnHandler(socket));//为新的连接创建新的线程 9 } 10 11 class ConnectIOnHandler extends Thread{ 12 private Socket socket; 13 public ConnectIOnHandler(Socket socket){ 14 this.socket = socket; 15 } 16 public void run(){ 17 while(!Thread.currentThread.isInturrupted()&&!socket.isClosed()){死循环处理读写事件 18 String someThing = socket.read()....//读取数据 19 if(someThing!=null){ 20 ......//处理数据 21 socket.write()....//写数据 22 } 24 } 25 } 26 }
上面的例子是经典的案例,因为socket的accept,read,write三个主要的函数都是同步阻塞的,所以单线程必定会是个异常程序。所以这里采用线程的方法去执行。
上述例子可以解决单线程可能阻塞的问题,但是多线程的另一个问题是线程资源问题,严重依赖于线程。
1. 线程的创建和销毁成本很高,在Linux这样的操作系统中,线程本质上就是一个进程。创建和销毁都是重量级的系统函数。
2. 线程本身占用较大内存,像Java的线程栈,一般至少分配512K~1M的空间,如果系统中的线程数过千,恐怕整个JVM的内存都会被吃掉一半。
3. 线程的切换成本是很高的。操作系统发生线程切换的时候,需要保留线程的上下文,然后执行系统调用。如果线程数过高,可能执行线程切换的时间甚至会大于线程执行的时间,这时候带来的表现往往是系统load偏高、CPU sy使用率特别高(超过20%以上),导致系统几乎陷入不可用的状态。
4. 容易造成锯齿状的系统负载。因为系统负载是用活动线程数或CPU核心数,一旦线程数量高但外部网络环境不是很稳定,就很容易造成大量请求的结果同时返回,激活大量阻塞线程从而使系统负载压力过大。
所以小型的项目,BIO可以完美胜任,但是面对甚至超过百万级别的数量请求时,BIO会导致系统崩溃的,所以此时一种高效的IO处理方式必须得到实现。
接下来就是NIO的介绍:
1. 所有的系统I/O都分为两个阶段:等待就绪和操作。举例来说,读函数,分为等待系统可读和真正的读;同理,写函数分为等待网卡可以写和真正的写。
等待就绪的阻塞是不使用CPU的,是在空等,因为阻塞状态的线程是不会释放当前所占的系统资源的。而真正的读写操作过程是十分快的,属于memory copy,带宽通常在1gb/s,基本相当于不耗时。
2. NIO可以设置为非阻塞,socketChannel.configureBlocking(false); 此时任何IO操作,如accept,read,write当没有IO事件发生时,不会阻塞,而是返回读取到数量0;而BIO当TCP RecvBuffer里没有数据,函数会一直阻塞直到读到数据。{最新的AIO(Async I/O)异步IO,不但等待是非阻塞的,就连数据从网卡到内存也是异步的。}
3. NIO的另一个重要的特点是selector是阻塞的,但是socket的读写注册和接受函数,在等待就绪阶段都是非阻塞的,真正的IO操作都是同阻塞的,虽然消耗系统性能但是因为不需要花费等待就绪的时间,memeory copy进行的很快,所以性能很高。
4. NIO是使用单线程或者只使用少量的多线程,多个连接共用一个线程,当处于等待(没有事件)的时候线程资源可以释放出来处理别的请求,通过事件驱动模型当有accept/read/write等事件发生后通知(唤醒)主线程分配资源来处理相关事件。java.nio.channels.Selector就是在该模型中事件的观察者,可以将多个SocketChannel的事件注册到一个Selector上,当没有事件发生时Selector处于阻塞状态,当SocketChannel有accept/read/write等事件发生时唤醒Selector。 这个Selector是使用了单线程模型,主要用来描述事件驱动模型。
5. NIO主要的事件有:读就绪、写就绪、有新连接到来
6. NIO的selector当没有IO事件时出于阻塞状态,slector相当于一个多路电路,它不需要循环的遍历每个channel,而只是等待IO事件到来,并把事件属于读还是写等交给selector,由selector交给线程去处理,这比传统的阻塞IO节省了很多的系统资源。这个地方有歧义,我在很多地方都是看的轮询,但是我在一篇文章中看到了上述理论,我本人更赞成这种端点,先记下来,后续修正。
Proactor与Reactor
一般情况下,I/O 复用机制需要事件分发器(event dispatcher)。 事件分发器的作用,即将那些读写事件源分发给各读写事件的处理者,就像送快递的在楼下喊: 谁谁谁的快递到了, 快来拿吧!开发人员在开始的时候需要在分发器那里注册感兴趣的事件,并提供相应的处理者(event handler),或者是回调函数;事件分发器在适当的时候,会将请求的事件分发给这些handler或者回调函数。
涉及到事件分发器的两种模式称为:Reactor和Proactor。 Reactor模式是基于同步I/O的,而Proactor模式是和异步I/O相关的。在Reactor模式中,事件分发器等待某个事件或者可应用或个操作的状态发生(比如文件描述符可读写,或者是socket可读写),事件分发器就把这个事件传给事先注册的事件处理函数或者回调函数,由后者来做实际的读写操作。
而在Proactor模式中,事件处理者(或者代由事件分发器发起)直接发起一个异步读写操作(相当于请求),而实际的工作是由操作系统来完成的。发起时,需要提供的参数包括用于存放读到数据的缓存区、读的数据大小或用于存放外发数据的缓存区,以及这个请求完后的回调函数等信息。事件分发器得知了这个请求,它默默等待这个请求的完成,然后转发完成事件给相应的事件处理者或者回调。举例来说,在Windows上事件处理者投递了一个异步IO操作(称为overlapped技术),事件分发器等IO Complete事件完成。这种异步模式的典型实现是基于操作系统底层异步API的,所以我们可称之为“系统级别”的或者“真正意义上”的异步,因为具体的读写是由操作系统代劳的。
Proactor与Reactor参考文章:https://tech.meituan.com/2016/11/04/nio.html
介绍NIO更详细的文章:https://www.cnblogs.com/sxkgeek/p/9488703.html