NIO与BIO
Posted 冰点契约丶
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了NIO与BIO相关的知识,希望对你有一定的参考价值。
NIO与BIO
Stream与Channel
- stream 不会自动缓冲数据,channel 会利用系统提供的发送缓冲区、接收缓冲区(更为底层)
- stream 仅支持阻塞 API,channel 同时支持阻塞、非阻塞 API,网络 channel 可配合 selector 实现多路复用
- 二者均为全双工,即读写可以同时进行
- 虽然Stream是单向流动的,但是它也是全双工的
IO模型
- 同步:线程自己去获取结果(一个线程)
- 例如:线程调用一个方法后,需要等待方法返回结果
- 异步:线程自己不去获取结果,而是由其它线程返回结果(至少两个线程)
- 例如:线程A调用一个方法后,继续向下运行,运行结果由线程B返回
当调用一次 channel.read 或 stream.read 后,会由用户态切换至操作系统内核态来完成真正数据读取,而读取又分为两个阶段,分别为:
- 等待数据阶段
- 复制数据阶段
根据UNIX 网络编程 - 卷 I,IO模型主要有以下几种
阻塞IO
- 用户线程进行read操作时,需要等待操作系统执行实际的read操作,此期间用户线程是被阻塞的,无法执行其他操作
非阻塞IO
-
用户线程
在一个循环中一直调用read方法,若内核空间中还没有数据可读,立即返回,只是在等待阶段非阻塞
-
用户线程发现内核空间中有数据后,等待内核空间执行复制数据,待复制结束后返回结果
多路复用
Java中通过Selector实现多路复用
- 当没有事件时,调用select方法会被阻塞住
- 一旦有一个或多个事件发生后,就会处理对应的事件,从而实现多路复用
多路复用与阻塞IO的区别
- 阻塞IO模式下,若线程因accept事件被阻塞,发生read事件后,仍需等待accept事件执行完成后,才能去处理read事件
- 多路复用模式下,一个事件发生后,若另一个事件处于阻塞状态,不会影响该事件的执行
异步IO
- 线程1调用方法后立即返回,不会被阻塞也不需要立即获取结果
- 当方法的运行结果出来以后,由线程2将结果返回给线程1
零拷贝
零拷贝指的是数据无需拷贝到 JVM 内存中,同时具有以下三个优点
- 更少的用户态与内核态的切换
- 不利用 cpu 计算,减少 cpu 缓存伪共享
- 零拷贝适合小文件传输
传统 IO 问题
传统的 IO 将一个文件通过 socket 写出
File f = new File("helloword/data.txt");
RandomAccessFile file = new RandomAccessFile(file, "r");
byte[] buf = new byte[(int)f.length()];
file.read(buf);
Socket socket = ...;
socket.getOutputStream().write(buf);
内部工作流程如下
- Java 本身并不具备 IO 读写能力,因此 read 方法调用后,要从 Java 程序的用户态切换至内核态,去调用操作系统(Kernel)的读能力,将数据读入内核缓冲区。这期间用户线程阻塞,操作系统使用 DMA(Direct Memory Access)来实现文件读,其间也不会使用 CPU
- DMA 也可以理解为硬件单元,用来解放 cpu 完成文件 IO
- 从内核态切换回用户态,将数据从内核缓冲区读入用户缓冲区(即 byte[] buf),这期间 CPU 会参与拷贝,无法利用 DMA
- 调用 write 方法,这时将数据从用户缓冲区(byte[] buf)写入 socket 缓冲区,CPU 会参与拷贝
- 接下来要向网卡写数据,这项能力 Java 又不具备,因此又得从用户态切换至内核态,调用操作系统的写能力,使用 DMA 将 socket 缓冲区的数据写入网卡,不会使用 CPU
可以看到中间环节较多,java 的 IO 实际不是物理设备级别的读写,而是缓存的复制,底层的真正读写是操作系统来完成的
- 用户态与内核态的切换发生了 3 次,这个操作比较重量级
- 数据拷贝了共 4 次
NIO 优化
通过 DirectByteBuf
- ByteBuffer.allocate(10):底层对应 HeapByteBuffer,使用的还是 Java 内存
- ByteBuffer.allocateDirect(10):底层对应DirectByteBuffer,使用的是操作系统内存
大部分步骤与优化前相同,唯有一点:Java 可以使用 DirectByteBuffer 将堆外内存映射到 JVM 内存中来直接访问使用
- 这块内存不受 JVM 垃圾回收的影响,因此内存地址固定,有助于 IO 读写
- Java 中的 DirectByteBuf 对象仅维护了此内存的虚引用,内存回收分成两步
- DirectByteBuffer 对象被垃圾回收,将虚引用加入引用队列
- 当引用的对象ByteBuffer被垃圾回收以后,虚引用对象Cleaner就会被放入引用队列中,然后调用Cleaner的clean方法来释放直接内存
- DirectByteBuffer 的释放底层调用的是 Unsafe 的 freeMemory 方法
- 通过专门线程访问引用队列,根据虚引用释放堆外内存
- 减少了一次数据拷贝,用户态与内核态的切换次数没有减少
进一步优化
以下两种方式都是零拷贝,即无需将数据拷贝到用户缓冲区中(JVM内存中)
底层采用了 linux 2.1 后提供的 sendFile 方法,Java 中对应着两个 channel 调用 transferTo/transferFrom 方法拷贝数据
- ava 调用 transferTo 方法后,要从 Java 程序的用户态切换至内核态,使用 DMA将数据读入内核缓冲区,不会使用 CPU
- 数据从内核缓冲区传输到 socket 缓冲区,CPU 会参与拷贝
- 最后使用 DMA 将 socket 缓冲区的数据写入网卡,不会使用 CPU
这种方法下
- 只发生了1次用户态与内核态的切换
- 数据拷贝了 3 次
进一步优化2
linux 2.4 对上述方法再次进行了优化
- Java 调用 transferTo 方法后,要从 Java 程序的用户态切换至内核态,使用 DMA将数据读入内核缓冲区,不会使用 CPU
- 只会将一些 offset 和 length 信息拷入 socket 缓冲区,几乎无消耗
- 使用 DMA 将 内核缓冲区的数据写入网卡,不会使用 CPU
整个过程仅只发生了1次用户态与内核态的切换,数据拷贝了 2 次
AIO
AIO 用来解决数据复制阶段的阻塞问题
- 同步意味着,在进行读写操作时,线程需要等待结果,还是相当于闲置
- 异步意味着,在进行读写操作时,线程不必等待结果,而是将来由操作系统来通过回调方式由另外的线程来获得结果
异步模型需要底层操作系统(Kernel)提供支持
- Windows 系统通过 IOCP 实现了真正的异步 IO
- Linux 系统异步 IO 在 2.6 版本引入,但其底层实现还是用多路复用模拟了异步 IO,性能没有优势
文件 AIO
public class AIOTest
public static void main(String[] args) throws IOException
try
AsynchronousFileChannel async = AsynchronousFileChannel.open(Paths.get("b.txt"), StandardOpenOption.READ);
ByteBuffer buffer = ByteBuffer.allocate(16);
async.read(buffer, 0, null, new CompletionHandler<Integer, ByteBuffer>()
@Override
public void completed(Integer result, ByteBuffer attachment)
System.out.println("read finish! "+result);
buffer.flip();
ByteBufferUtil.debugAll(buffer);
@Override
public void failed(Throwable exc, ByteBuffer attachment)
System.out.println("read failed!");
);
catch (Exception e)
e.printStackTrace();
System.out.println("do other things");
System.in.read();
do other things
read finish! 14
+--------+-------------------- all ------------------------+----------------+
position: [0], limit: [14]
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 68 65 6c 6c 6f 77 6f 72 6c 64 31 32 33 34 00 00 |helloworld1234..|
+--------+-------------------------------------------------+----------------+
可以看到
- 响应文件读取成功的是另一个线程
- 主线程并没有 IO 操作阻塞
网络AIO
public class Aioserver
public static void main(String[] args) throws IOException
AsynchronousServerSocketChannel ssc = AsynchronousServerSocketChannel.open();
ssc.bind(new InetSocketAddress(8080));
ssc.accept(null, new AcceptHandler(ssc));
System.in.read();
private static void closeChannel(AsynchronousSocketChannel sc)
try
System.out.printf("[%s] %s close\\n", Thread.currentThread().getName(), sc.getRemoteAddress());
sc.close();
catch (IOException e)
e.printStackTrace();
private static class ReadHandler implements CompletionHandler<Integer, ByteBuffer>
private final AsynchronousSocketChannel sc;
public ReadHandler(AsynchronousSocketChannel sc)
this.sc = sc;
@Override
public void completed(Integer result, ByteBuffer attachment)
try
if (result == -1)
closeChannel(sc);
return;
System.out.printf("[%s] %s read\\n", Thread.currentThread().getName(), sc.getRemoteAddress());
attachment.flip();
System.out.println(Charset.defaultCharset().decode(attachment));
attachment.clear();
// 处理完第一个 read 时,需要再次调用 read 方法来处理下一个 read 事件
sc.read(attachment, attachment, this);
catch (IOException e)
e.printStackTrace();
@Override
public void failed(Throwable exc, ByteBuffer attachment)
closeChannel(sc);
exc.printStackTrace();
private static class WriteHandler implements CompletionHandler<Integer, ByteBuffer>
private final AsynchronousSocketChannel sc;
private WriteHandler(AsynchronousSocketChannel sc)
this.sc = sc;
@Override
public void completed(Integer result, ByteBuffer attachment)
// 如果作为附件的 buffer 还有内容,需要再次 write 写出剩余内容
if (attachment.hasRemaining())
sc.write(attachment);
@Override
public void failed(Throwable exc, ByteBuffer attachment)
exc.printStackTrace();
closeChannel(sc);
private static class AcceptHandler implements CompletionHandler<AsynchronousSocketChannel, Object>
private final AsynchronousServerSocketChannel ssc;
public AcceptHandler(AsynchronousServerSocketChannel ssc)
this.ssc = ssc;
@Override
public void completed(AsynchronousSocketChannel sc, Object attachment)
try
System.out.printf("[%s] %s connected\\n", Thread.currentThread().getName(), sc.getRemoteAddress());
catch (IOException e)
e.printStackTrace();
ByteBuffer buffer = ByteBuffer.allocate(16);
// 读事件由 ReadHandler 处理
sc.read(buffer, buffer, new ReadHandler(sc));
// 写事件由 WriteHandler 处理
sc.write(Charset.defaultCharset().encode("server hello!"), ByteBuffer.allocate(16), new WriteHandler(sc));
// 处理完第一个 accpet 时,需要再次调用 accept 方法来处理下一个 accept 事件
ssc.accept(null, this);
@Override
public void failed(Throwable exc, Object attachment)
exc.printStackTrace();
Java BIO与NIO以及AIO分析
一.BIO与NIO以及AIO的概念
BIO是同步阻塞式的IO
NIO是同步非阻塞的IO (NIO1.0,JDK1.4)
AIO是非同步非阻塞的IO(NIO2.0,JDK1.7)
二.BIO简单分析
1.简单分析
BIO是阻塞的IO,原因在于accept和read会阻塞。所以单线程的BIO是无法处理并发的。
2.案例
服务端:
public class BioServer
public static void main(String[] args) throws IOException //在开发中不要直接抛出去,有可能出现异常导致连接未关闭
ServerSocket serverSocket = new ServerSocket();
serverSocket.bind(new InetSocketAddress("127.0.0.1",9999));
byte[] b = new byte[1024];
StringBuilder sb = new StringBuilder();
System.out.println("服务端正在等待连接......");
Socket socketAccept = serverSocket.accept();//这里会阻塞
System.out.println("有客户端连接成功");
while (true)
System.out.println("等待消息......");
int read = socketAccept.getInputStream().read(b);//这里会阻塞
System.out.println("共读取了"+read+"个字节");
String str = new String(b);
sb.append(str);
System.out.println("读取的数据内容为: " + sb);
客户端:
public class BioClient
public static void main(String[] args) throws IOException
Socket socket = new Socket();
Scanner scanner = new Scanner(System.in);
socket.connect(new InetSocketAddress("127.0.0.1",9999));
while(true)
System.out.println("请输入内容");
String next = scanner.next();
socket.getOutputStream().write(next.getBytes());
测试如下:
3.多线程下的BIO
多线程的BIO是可以处理并发的,但频繁的创建线程,运行线程以及销毁线程无疑是非常消耗资源的(即便可以使用线程池限制线程数量,但海量并发时,BIO的方式效率相比于NIO还是太低了)
下面是一个多线程的BIO案例:
服务端:
public class BioServerMultiThread public static void main(String[] args) throws IOException ServerSocket serverSocket = new ServerSocket(); serverSocket.bind(new InetSocketAddress("127.0.0.1",9999)); System.out.println("服务端正在等待连接......"); while (true) Socket socketAccept = serverSocket.accept();//这里会阻塞 System.out.println("有客户端连接成功"); new Thread(new Runnable() @Override public void run() try System.out.println("等待消息......"); String msg = ""; while (true) byte[] b = new byte[1024]; int len = socketAccept.getInputStream().read(b); if (len < 0) continue; msg = new String(b,0,len); System.out.println(msg); catch (Exception e) e.printStackTrace(); ).start();
客户端(创建多个客户端,代码相同):
public class BioClient1 public static void main(String[] args) throws IOException Socket socket = new Socket(); Scanner scanner = new Scanner(System.in); socket.connect(new InetSocketAddress("127.0.0.1",9999)); String className = "这是来自"+Thread.currentThread().getStackTrace()[1].getClassName()+"的消息"; while(true) System.out.println("请输入内容"); String next = scanner.next(); socket.getOutputStream().write(className.getBytes()); socket.getOutputStream().write(next.getBytes());
测试如下:
三.NIO简单分析
NIO是非阻塞IO,其原因在于数据准备就绪后,由选择器通知给服务端,而在数据准备完毕之前,服务器无需等待。
1.缓冲区
在BIO操作中,所有的数据都是以流的形式操作的,而在NIO中,则都是使用缓冲区来操作。
NIO中的缓冲区是Buffer类,它是java.nio包下的一个抽象类,详情可以查看JDK的API文档,例如下图
打开Buffer类,可以看到如下图(其中带有的方法可以查看JDK文档)
2.直接缓冲区和非直接缓冲区
直接缓冲区是指将缓冲区建立在物理内存中,通过allocateDirect方法建立(效率高,但不安全)
非直接缓冲区是指将缓冲区建立在JVM中,通过allocate方法建立(效率低,但安全)
3.管道
管道是NIO中传输数据的桥梁,它是java.nio包下的一个接口,若需查看其详细信息可以参考JDK的API文档,例如下图
4.使用NIO实现文件拷贝两种方式案例
直接缓冲区的方式:
@Test public void test1() throws IOException long statTime=System.currentTimeMillis(); //创建管道 FileChannel inChannel= FileChannel.open(Paths.get("E://test/aaa.txt"), StandardOpenOption.READ); FileChannel outChannel= FileChannel.open(Paths.get("E://test/bbb.txt"), StandardOpenOption.READ,StandardOpenOption.WRITE, StandardOpenOption.CREATE); //定义映射文件 MappedByteBuffer inMappedByte = inChannel.map(FileChannel.MapMode.READ_ONLY,0, inChannel.size()); MappedByteBuffer outMappedByte = outChannel.map(FileChannel.MapMode.READ_WRITE,0, inChannel.size()); //直接对缓冲区操作 byte[] dsf=new byte[inMappedByte.limit()]; inMappedByte.get(dsf); outMappedByte.put(dsf); inChannel.close(); outChannel.close(); long endTime=System.currentTimeMillis(); System.out.println("操作直接缓冲区耗时时间:"+(endTime-statTime));
非直接缓冲区的方式:
@Test public void test2() throws IOException long statTime=System.currentTimeMillis(); // 读入流 FileInputStream fst = new FileInputStream("E://test/bbb.txt"); // 写入流 FileOutputStream fos = new FileOutputStream("E://test/ccc.txt"); // 创建通道 FileChannel inChannel = fst.getChannel(); FileChannel outChannel = fos.getChannel(); // 分配指定大小缓冲区 ByteBuffer buf = ByteBuffer.allocate(1024); while (inChannel.read(buf) != -1) // 开启读取模式 buf.flip(); // 将数据写入到通道中 outChannel.write(buf); buf.clear(); // 关闭通道 、关闭连接 inChannel.close(); outChannel.close(); fos.close(); fst.close(); long endTime=System.currentTimeMillis(); System.out.println("操作非直接缓冲区耗时时间:"+(endTime-statTime));
5.选择器(Selector)
它是Java NIO核心组件中的一个,用于检查一个或多个NIO Channel(通道)的状态是否处于可读、可写。如此可以实现单线程管理多个channels,也就是可以管理多个网络链接。使用Selector的好处在于: 使用更少的线程来就可以来处理通道了, 相比使用多个线程,避免了线程上下文切换带来的开销。详细用法可以参考JDK的API文档
四.AIO简单分析
AIO(NIO2.0)和 NIO的主要区别是前者是异步的,而后者是同步的。
与NIO不同,当进行读写操作时,AIO只须直接调用API的read或write方法即可。这两种方法均为异步的,对于读操作而言,当有流可读取时,操作系统会将可读的流传入read方法的缓冲区,并通知应用程序;对于写操作而言,当操作系统将write方法传递的流写入完毕时,操作系统主动通知应用程序。 即可以理解为,read/write方法都是异步的,完成后会主动调用回调函数。
以上是关于NIO与BIO的主要内容,如果未能解决你的问题,请参考以下文章