JAVA IO : BIO NIO AIO
同步异步、阻塞非阻塞概念
同步和异步是针对应用程序和内核的交互而言的,主要看是主动轮训内核还是等待内核通知。 阻塞和非阻塞是针对于进程在访问数据的时候,根据IO操作的就绪状态来采取的不同方式,说白了是一种读取或者写入操作函数的实现方式,阻塞方式下读取或者写入函数将一直等待,而非阻塞方式下,读取或者写入函数会立即返回一个状态值。
同步与异步
- 同步:用户进程触发IO操作并等待或者轮询的去查看IO操作是否就绪。餐厅窗口点了餐品,每过一会来问一下,我们的餐品是否准备好了。
- 异步:指用户进程触发IO操作以后便开始做自己的事情,而当IO操作已经完成的时候会得到IO完成的通知(异步的特点就是通知)。餐厅窗口点餐,当说明我们需要的餐品之后,然后自己可以去干别的事。当餐厅准备好了之后会通知我们。(使用异步IO时,Java将IO读写委托给OS处理,需要将数据缓冲区地址和大小传给OS) 。
阻塞与非阻塞
-
阻塞:当试图对该文件描述符进行读写时, 如果当时没有东西可读,或者暂时不可写, 程序就进入等待 状态, 直到有东西可读或者可写为止。餐厅窗口点餐,当说明我们需要的餐品之后,窗口只会在加工完成并且送出我们需要的餐品的时候才会响应我们,在这期间,不可以做其他的事情,我们必须保持等待。
-
非阻塞:当试图对该文件描述符进行读写时, 如果没有东西可读, 或者不可写, 读写函数马上返回, 而不会等待。餐厅窗口点餐,当说明我们需要的餐品之后,窗口立刻响应,给我们一张小票,领票完后我们自己可以玩玩手机,或者与别人聊聊天,我们可以轮询(同步)我们的餐品是否OK或者等待通知(异步)。
IO VS NIO VS AIO
IO | NIO | AIO(NIO2) |
---|---|---|
面向流 | 面向缓冲 | 面向缓冲 |
阻塞IO | 非阻塞IO | 非阻塞IO |
同步 | 同步 | 异步 |
无 | 选择器 | 观察者 |
面向流与面向缓冲
Java NIO和IO之间第一个最大的区别是,IO是面向流的,NIO是面向缓冲区的。 Java IO面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。此外,它不能前后移动流中的数据。如果需要前后移动从流中读取的数据,需要先将它缓存到一个缓冲区。 Java NIO的缓冲导向方法略有不同。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动。这就增加了处理过程中的灵活性。但是,还需要检查是否该缓冲区中包含所有您需要处理的数据。而且,需确保当更多的数据读入缓冲区时,不要覆盖缓冲区里尚未处理的数据。
阻塞与非阻塞IO
Java IO的各种流是阻塞的。这意味着,当一个线程调用read()
或 write()
时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。 Java NIO
的非阻塞模式,使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取。而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。 非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。 线程通常将非阻塞IO的空闲时间用于在其它通道上执行IO操作,所以一个单独的线程现在可以管理多个输入和输出通道(channel)。
在IO设计中,我们从InputStream或 Reader逐字节读取数据。假设你正在处理一基于行的文本数据流,例如:
Name: Anna
Age: 25
Email: anna@mailserver.com
Phone: 1234567890
该文本行的流可以这样处理:
InputStream input = null;
try {
input = new FileInputStream("/data/file/temp/test.txt");
} catch (FileNotFoundException e) {
e.printStackTrace();
}
BufferedReader reader = new BufferedReader(new InputStreamReader(input));
try {
//堵塞到读取本行数据完成
String nameLine = reader.readLine();
//堵塞到读取本行数据完成
String ageLine = reader.readLine();
//堵塞到读取本行数据完成
String emailLine = reader.readLine();
//堵塞到读取本行数据完成
String phoneLine = reader.readLine();
} catch (IOException e) {
e.printStackTrace();
}
一旦reader.readLine()
方法返回,你就知道肯定文本行就已读完, readline()
阻塞直到整行读完,这就是原因。你也知道此行包含名称;同样,第二个readline()
调用返回的时候,你知道这行包含年龄等。 正如你可以看到,该处理程序仅在有新数据读入时运行,并知道每步的数据是什么。一旦正在运行的线程已处理过读入的某些数据,该线程不会再回退数据(大多如此)。
而一个NIO的实现会有所不同,下面是一个简单的例子:
RandomAccessFile aFile = null;
try {
aFile = new RandomAccessFile("/data/file/temp/big.txt", "rw");
} catch (FileNotFoundException e) {
e.printStackTrace();
}
FileChannel inChannel = aFile.getChannel();
//建造一块20480字节的大缓冲区
ByteBuffer buf = ByteBuffer.allocate(20480);
//写入文件信息到缓冲区
int bytesRead = 0;
try {
//不会堵塞,立刻返回
bytesRead = inChannel.read(buf);
//查看已经读取到缓存的字节数
System.out.println(bytesRead);
while (bytesRead != -1) {
//转为读取模式
buf.flip();
while(buf.hasRemaining()){
// 一次读取一个字节
System.out.print((char) buf.get());
}
//清空缓冲区
buf.clear();
//继续写入文件信息到缓冲区
bytesRead = inChannel.read(buf);
}
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
aFile.close();
} catch (IOException e) {
e.printStackTrace();
}
}
从通道读取字节到ByteBuffer。当这个方法调用返回时,你不知道你所需的所有数据是否全部加载完成是否都已经在缓冲区内。
结果如下
BIO、NIO、AIO的JAVA实现
- Java BIO: 同步并阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然可以通过线程池机制改善。
- Java NIO: 同步非阻塞,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器(selector)上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。
- Java AIO(NIO.2): 异步非阻塞,服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由OS先完成了再通知服务器应用去启动线程进行处理,
BIO、NIO、AIO适用场景分析
- BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,但程序直观简单易理解。
- NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂,JDK1.4开始支持。
- AIO方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂,JDK7开始支持。
另外,I/O属于底层操作,需要操作系统支持,并发也需要操作系统的支持,所以性能方面不同操作系统差异会比较明显。