采用 BIO 通讯模型 的服务端,一般由一个独立的 Acceptor 线程负责监听客户端的链接。咱们通常经过在while(true) 循环中服务端会调用 accept() 方法等待接收客户端的链接的方式监听请求,请求一旦接收到一个链接请求,就能够创建通讯套接字在这个通讯套接字上进行读写操做,此时不能再接收其余客户端链接请求,只能等待同当前链接的客户端的操做执行完成, 不过能够经过多线程来支持多个客户端的链接,如上图所示。
若是要让 BIO 通讯模型 可以同时处理多个客户端请求,就必须使用多线程(主要缘由是socket.accept()、socket.read()、socket.write() 涉及的三个主要函数都是同步阻塞的),也就是说它在接收到客户端链接请求以后为每一个客户端建立一个新的线程进行链路处理,处理完成以后,经过输出流返回应答给客户端,线程销毁。这就是典型的 一请求一应答通讯模型 。咱们能够设想一下若是这个链接不作任何事情的话就会形成没必要要的线程开销,不过能够经过 线程池机制 改善,线程池还可让线程的建立和回收成本相对较低。使用FixedThreadPool 能够有效的控制了线程的最大数量,保证了系统有限的资源的控制,实现了N(客户端请求数量):M(处理客户端请求的线程数量)的伪异步I/O模型(N 能够远远大于 M)。
咱们再设想一下当客户端并发访问量增长后这种模型会出现什么问题?
在 Java 虚拟机中,线程是宝贵的资源,线程的建立和销毁成本很高,除此以外,线程的切换成本也是很高的。尤为在 Linux 这样的操做系统中,线程本质上就是一个进程,建立和销毁线程都是重量级的系统函数。若是并发访问量增长会致使线程数急剧膨胀可能会致使线程堆栈溢出、建立新线程失败等问题,最终致使进程宕机或者僵死,不能对外提供服务。
1.2 伪异步 IO
为了解决同步阻塞I/O面临的一个链路须要一个线程处理的问题,后来有人对它的线程模型进行了优化一一一后端经过一个线程池来处理多个客户端的请求接入,造成客户端个数M:线程池最大线程数N的比例关系,其中M能够远远大于N.经过线程池能够灵活地调配线程资源,设置线程的最大值,防止因为海量并发接入致使线程耗尽。
伪异步IO模型图(图源网络,原出处不明):
采用线程池和任务队列能够实现一种叫作伪异步的 I/O 通讯框架,它的模型图如上图所示。当有新的客户端接入时,将客户端的 Socket 封装成一个Task(该任务实现java.lang.Runnable接口)投递到后端的线程池中进行处理,JDK 的线程池维护一个消息队列和 N 个活跃线程,对消息队列中的任务进行处理。因为线程池能够设置消息队列的大小和最大线程数,所以,它的资源占用是可控的,不管多少个客户端并发访问,都不会致使资源的耗尽和宕机。
伪异步I/O通讯框架采用了线程池实现,所以避免了为每一个请求都建立一个独立线程形成的线程资源耗尽问题。不过由于它的底层任然是同步阻塞的BIO模型,所以没法从根本上解决问题。
2. NIO (New I/O)
2.1 NIO 简介
NIO是一种同步非阻塞的I/O模型,在Java 1.4 中引入了NIO框架,对应 java.nio 包,提供了 Channel , Selector,Buffer等抽象。
NIO中的N能够理解为Non-blocking,不单纯是New。它支持面向缓冲的,基于通道的I/O操做方法。 NIO提供了与传统BIO模型中的 Socket 和 ServerSocket 相对应的 SocketChannel 和 ServerSocketChannel 两种不一样的套接字通道实现,两种通道都支持阻塞和非阻塞两种模式。阻塞模式使用就像传统中的支持同样,比较简单,可是性能和可靠性都很差;非阻塞模式正好与之相反。对于低负载、低并发的应用程序,可使用同步阻塞I/O来提高开发速率和更好的维护性;对于高负载、高并发的(网络)应用,应使用 NIO 的非阻塞模式来开发。
2.2 NIO的特性/NIO与IO区别
若是是在面试中回答这个问题,我以为首先确定要从 NIO 流是非阻塞 IO 而 IO 流是阻塞 IO 提及。而后,能够从 NIO 的3个核心组件/特性为 NIO 带来的一些改进来分析。若是,你把这些都回答上了我以为你对于 NIO 就有了更为深刻一点的认识,面试官问到你这个问题,你也能很轻松的回答上来了。
1)Non-blocking IO(非阻塞IO)
IO流是阻塞的,NIO流是不阻塞的(读写的同时能够作别的事情)。
Java NIO使咱们能够进行非阻塞IO操做。好比说,单线程中从通道读取数据到buffer,同时能够继续作别的事情,当数据读取到buffer中后,线程再继续处理数据。写数据也是同样的。另外,非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不须要等待它彻底写入,这个线程同时能够去作别的事情。
Java IO的各类流是阻塞的。这意味着,当一个线程调用 read() 或 write() 时,该线程被阻塞,直到有一些数据被读取,或数据彻底写入。该线程在此期间不能再干任何事情了
2)Buffer(缓冲区)
IO 面向流(Stream oriented),而 NIO 面向缓冲区(Buffer oriented)。
Buffer是一个对象,它包含一些要写入或者要读出的数据。在NIO类库中加入Buffer对象,体现了新库与原I/O的一个重要区别。在面向流的I/O中·能够将数据直接写入或者将数据直接读到 Stream 对象中。虽然 Stream 中也有 Buffer 开头的扩展类,但只是流的包装类,仍是从流读到缓冲区,而 NIO 倒是直接读到 Buffer 中进行操做。
在NIO厍中,全部数据都是用缓冲区处理的。在读取数据时,它是直接读到缓冲区中的; 在写入数据时,写入到缓冲区中。任什么时候候访问NIO中的数据,都是经过缓冲区进行操做。
最经常使用的缓冲区是 ByteBuffer,一个 ByteBuffer 提供了一组功能用于操做 byte 数组。除了ByteBuffer,还有其余的一些缓冲区,事实上,每一种Java基本类型(除了Boolean类型)都对应有一种缓冲区。
3)Channel (通道)
NIO 经过Channel(通道) 进行读写。
通道是双向的,可读也可写,而流的读写是单向的。不管读写,通道只能和Buffer交互。由于 Buffer,通道能够异步地读写。
4)Selectors(选择器)
NIO有选择器,而IO没有。
选择器用于使用单个线程处理多个通道。所以,它须要较少的线程来处理这些通道。线程之间的切换对于操做系统来讲是昂贵的。 所以,为了提升系统效率选择器是有用的。
2.3 NIO 读数据和写数据方式
一般来讲NIO中的全部IO都是从 Channel(通道) 开始的。
- 从通道进行数据读取 :建立一个缓冲区,而后请求通道读取数据。
- 从通道进行数据写入 :建立一个缓冲区,填充数据,并要求通道写入数据。
数据读取和写入操做图示:
3. AIO (Asynchronous I/O)
AIO 也就是 NIO 2。在 Java 7 中引入了 NIO 的改进版 NIO 2,它是异步非阻塞的IO模型。异步 IO 是基于事件和回调机制实现的,也就是应用操做以后会直接返回,不会堵塞在那里,当后台处理完成,操做系统会通知相应的线程进行后续的操做。
AIO 是异步IO的缩写,虽然 NIO 在网络操做中,提供了非阻塞的方法,可是 NIO 的 IO 行为仍是同步的。对于 NIO 来讲,咱们的业务线程是在 IO 操做准备好时,获得通知,接着就由这个线程自行进行 IO 操做,IO操做自己是同步的。