这些可以理解为是Java语言对操作系统的各种IO模型的封装,程序员在使用这些API的时候,不需要关系操作系统层面的知识,也不需要根据不同操作系统编写不同的代码,只需要使用Java的API就可以了。
1.BIO就是传统的java.io包(意思就是你在使用java.io包进行输入输出操作的时候,就是使用的BIO通信机制),BIO是传统的同步阻塞式的I/O。也就是说在读入输入流或者输出流时,在读写操作完成之前,线程会一直阻塞,会一直占用CPU资源,直到读写操作完成之后,才继续完成下面的任务。
采用BIO通信模型的服务端,通常有一个独立的Acceptor线程负责监听客户端的连接,它接收到客户端的连接请求之后,会为客户端每一个连接请求创建一个新的线程进行处理,处理完之后,通过输出流返回给客户端,线程销毁,这就是典型的一请求一应答模型。
可以看出,这是在多线程情况下执行的。当在单线程环境条件下,在while循环中服务端会调用accept方法等待接收客户端的连接请求,一旦收到这个连接请求,就可以建立socket,并在socket上进行读写操作,此时不能再接收其他客户端的连接请求,只能等待同当前服务端连接的客户端的操作完成或者连接断开。
该模型最大的缺单就是缺乏弹性伸缩能力,当客户端并发访问量增加后,服务端的线程个数和客户端并发访问数呈1:1的正比关系,由于线程是java虚拟机非常宝贵的系统资源,当线程数膨胀之后,系统的性能会急剧下降,随着并发量的继续增大,系统会发生线程堆栈溢出、创建新线程失败等问题,并最终导致进程宕机或者僵死,不能对外提供服务。
BIO实现的代码:
public class server {
private static Socket socket=null;
public static void main(String[] args) {
try {
//绑定端口
ServerSocket serverSocket=new ServerSocket();
serverSocket.bind(new InetSocketAddress(8080));
while (true){
//等待连接 阻塞
System.out.println("等待连接");
socket = serverSocket.accept();
System.out.println("连接成功");
//连接成功后新开一个线程去处理这个连接
new Thread(new Runnable() {
@Override
public void run() {
byte[] bytes=new byte[1024];
try {
System.out.println("等待读取数据");
//等待读取数据 阻塞
int length=socket.getInputStream().read(bytes);
System.out.println(new String(bytes,0,length));
System.out.println("数据读取成功");
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
}
} catch (IOException e) {
e.printStackTrace();
}
}
2.NIO是Java 1.4引入的java.nio包,提供了Channel、Selector、Buffer等新的抽象,可以构建多路复用的、同步非阻塞式IO程序,同时提供了更接近操作系统底层高性能的数据操作方式。NIO通过使用单线程轮询多个连接的的方式来实现高效的处理方式,可以支持较大数量的并发连接,但编程模型较为复杂。
NIO是为了解决BIO的缺陷提出的通信模型,以socket.read()为例:
传统的BIO里面的socket.read(),如果TCP RecvBuffer里没有数据,函数会一直阻塞,直到收到数据。
对于NIO,如果TCP RecvBuffer有数据,就把数据从网卡读到内存,并且返回给用户;反之则直接返回0,永远不会阻塞。
BIO关心的是“我要读”的问题,NIO关心的是“我可以读了”的问题。
NIO一个重要的特点是:socket主要的读、写、注册和接收函数,在等待就绪阶段都是非阻塞的,真正的I/O操作是同步阻塞的(消耗CPU但性能非常高)。
Channel,翻译过来就是“通道”,就是数据传输的管道,类似于“流”,但是与“流”又有着区别。
Channel与“流”的区别:
Buffer是一个对象,里面是要写入或者读出的数据,在java.nio库中,所有的数据都是用缓冲区处理的。
在读取数据时,它是直接读到缓冲区中的;在写入数据时,也是直接写到缓冲区中,任何时候访问Channel中的数据,都是通过缓冲区进行操作的。
缓冲区实质上是一个数组,通常是一个字节数组ByteBuffer,当然也有其他类型的:
Selector被称为选择器,Selector会不断地轮询注册在其上的Channel,如果某个Channel上发生读或写事件,这个Channel就被判定处于就绪状态,会被Selector轮询出来,然后通过SelectionKey可以获取到就绪Channel的集合,进行后续的I/O操作。
一个多路复用器Selector可以同时轮询多个Channel,JDK使用了epoll()代替了传统的select实现,所以并没有最大连接句柄的限制,这意味着只需要一个线程负责Selector的轮询,就可以接入成千上万的客户端。
NIO是利用了单线程轮询事件的机制,通过高效地定位就绪的Channel,来决定做什么,仅仅select阶段是阻塞的,可以有效避免大量客户端连接时,频繁切换线程带来的问题,应用的拓展能力有了很大的提高。
public class server {
public static void main(String[] args) {
try {
//创建一个socket通道,并且设置为非阻塞的方式
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);//设置为非阻塞的方式
serverSocketChannel.socket().bind(new InetSocketAddress(9000));
//创建一个selector选择器,把channel注册到selector选择器上
Selector selector = Selector.open();
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
while(true)
{
System.out.println("等待事件发生");
selector.select();
System.out.println("有事件发生了");
Iterator iterator = selector.selectedKeys().iterator();
while(iterator.hasNext())
{
SelectionKey key = iterator.next();
iterator.remove();
handle(key);
}
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private static void handle(SelectionKey key) throws IOException{
if(key.isAcceptable())
{
System.out.println("连接事件发生");
ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();
//创建客户端一侧的channel,并注册到selector上
SocketChannel socketChannel = serverSocketChannel.accept();
socketChannel.configureBlocking(false);
socketChannel.register(key.selector(),SelectionKey.OP_READ);
} else if (key.isReadable()) {
System.out.println("数据可读事件发生");
SocketChannel socketChannel = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int len = socketChannel.read(buffer);
if(len!=-1)
{
System.out.println("读取到客户端发送的数据:"+new String(buffer.array(),0,len));
}
//给客户端发送信息
ByteBuffer wrap = ByteBuffer.wrap("hello world".getBytes());
socketChannel.write(wrap);
key.interestOps(SelectionKey.OP_READ|SelectionKey.OP_WRITE);
socketChannel.close();
}
}
下面是多路复用的流程图:
3.AIO是Java 1.7之后引入的包,是NIO的升级版本。异步非阻塞I/O,通过回调的方式实现高效的I/O操作,也就是应用操作之后会直接返回,不会堵塞(此时用户进程只需要对数据处理,而不需要进行实际的IO读写操作,因为真正的IO操作已经由操作系统内核完成了),当后台去处理完成之后,操作系统会通知相应的线程进行后续的操作。这样可以大大降低系统资源的消耗,适用于高并发、高吞吐量的场景,但在实际使用中可能会受到操作系统和硬件的限制。
4.可以用一个例子来描述这三个通信模型的区别:设想你要烧水这样一个场景
BIO:在烧水期间,你一直守在旁边,不敢任何事,等到水烧开了才去完成其他事
NIO:在烧水期间,你不必一直守在旁边,而是时不时来看水是否烧开,其他时间可以去完成其他事
AIO:在烧水期间,你无需来看水是否烧开了,可以去完成其他任务,水烧开的时候会发出提示音提示你水开了,这时候你再来处理。
同步和异步(线程之间的调用):
同步操作时,调用者需要等待被调用者返回结果,才会进行下一步操作
而异步则相反,调用者不需要等待被调用者返回调用,即可进行下一步操作,被调用者通常依靠事件、回调等机制来通知调用者返回调用结果
阻塞和非阻塞(线程内调用):
阻塞和非阻塞是对同一个线程来说的,在某个时刻,线程要么处于阻塞,要么处于非阻塞
阻塞和非阻塞关注的是程序在等待调用结果(消息、返回值)时的状态:
阻塞调用是指在调用结果返回之前,当前线程会被挂起,调用线程只有在得到结果之后才会返回
非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程
IO分为两个操作:第一步是发起IO请求,第二步真正执行IO读写操作
同步和异步IO的概念:
同步是用户线程发起IO请求之后一直等待或者轮询内核IO操作完成之后才能继续执行后续代码。
异步是用户线程发起IO请求后仍然继续执行后续代码,当内核IO操作完成后会通知用户线程,或者调用用户线程注册的回调函数
阻塞和非阻塞IO的概念:
阻塞是指IO读写操作需要彻底完成后才能返回用户空间
非阻塞是指IO读写操作被调用后立即返回一个状态值,无需等待IO操作彻底完成
四种组合方式:
参考资料:
Java NIO三大角色Channel、Buffer、Selector相关解析 - 掘金 (juejin.cn)https://juejin.cn/post/7037028848487104519#heading-28Java NIO 中的 Channel 详解 - 掘金 (juejin.cn)https://juejin.cn/post/7058948260529963039JAVA中BIO、NIO、AIO的分析理解-阿里云开发者社区 (aliyun.com)https://developer.aliyun.com/article/726698#slide-26Java核心(五)深入理解BIO、NIO、AIO - 腾讯云开发者社区-腾讯云 (tencent.com)https://cloud.tencent.com/developer/article/1376675Java NIO浅析 - 知乎 (zhihu.com)https://zhuanlan.zhihu.com/p/23488863彻底理解同步 异步 阻塞 非阻塞 - LittleDonkey - 博客园 (cnblogs.com)https://www.cnblogs.com/loveer/p/11479249.html