阻塞非阻塞|阻塞非阻塞 同步异步 IO模型及其应用 NIO实现原理

1.同步异步概念 2.阻塞非阻塞概念 3.常见I/O模型:同步阻塞IO,同步非阻塞IO,异步阻塞IO,异步非阻塞IO 4.UNIX系统下的IO多路复用(OS级别的I/O多路复用是重点,同步非阻塞I/O的应用)*** 5.IO中BIO,NIO,AIO简介 6.NIO实现原理(NIO也是同步非阻塞I/O的应用)(NIO阻塞代码实例 NIO非阻塞代码实例 这里的Selector真正体现了多路复用)(重点)*** 1.同步异步概念 同步异步是针对应用程序和内核的交互而言的。
同步指的是用户进程触发IO操作,等待/轮询的去查看IO操作是否完成。(同步阻塞IO是等待,同步非阻塞是轮询)
异步指的是用户进程出发IO操作便开始做别的事,IO操作已经完成的时候会得到IO完成的通知。
2.阻塞非阻塞概念 阻塞非阻塞是针对于进程在访问数据的时候,根据IO操作的就绪状态采用的不同操作方式。
阻塞状态下,读取/写入函数将一直等待IO操作就绪。
非阻塞状态下,读取/写入函数会立即返回一个状态值。
3.常见的IO模型 同步阻塞IO:在此种模型下,用户进程在发起一个IO操作以后,必须等待IO操作的完成。只有当真正完成了IO操作以后,用户进程才能运行。
应用:Java.io包下的传统IO模型
同步非阻塞IO:在此种模型下,用户进程在发起一个IO操作以后边可返回做其它事情,但是用户进程需要时不时的询问IO操作是否就绪,这就要求用户进程不断的进行轮询,从而引入不必要的CPU资源浪费。
应用:jdk1.4出现的java.nio包
异步阻塞IO:在此种模型下,用户进程发起一个IO操作以后,不等待内核IO操作的完成,等内核完成IO操作以后通知应用程序。
这就是同步异步的最关键区别:同步必须等待/主动轮询IO操作是否完成。
为什么说是阻塞的呢?因为其是调用了select函数来完成,而select函数本身实现的方式就是阻塞的。其好处为:可以同时监听多个文件句柄,从而提高系统并发性。
同步非阻塞IO:在此种模型下,用户进程只需要发起一个IO操作然后立即返回,等IO操作真正完成以后,应用程序会得到IO操作完成的通知,此时用户进程只需要对数据进行处理,不需要进行实际的IO读写操作,因为真正的IO读取或写入操作已经由内核完成了。
4.UNIX系统下的IO多路复用(OS级别的I/O多路复用是重点,同步非阻塞I/O的应用) UNIX系统秉承了一切皆文件的思想。
IO多路复用机制是同步非阻塞IO的应用。
它利用单独的线程(内核级)统一检测所有的Socket(套接字),一旦某个Socket有了IO数据,则启动响应的Application处理。
实现原理:在select和poll中利用轮询Socket句柄的方式来实现监测Socket中是否有IO数据到达。
【阻塞非阻塞|阻塞非阻塞 同步异步 IO模型及其应用 NIO实现原理】select底层是数组,poll是链表,epoll是哈希表。
select和epoll区别:

  1. 每次调用select,都需要把fd集合(句柄集合)从用户态拷贝到内核态,这个开销在fd很多时开销很大
  2. 同时每次调用select都需要在内核遍历传递进来的所有的fd,这个开销在fd很多时也很大
  3. select支持的文件描述符数量太小了,默认是1024
    4.epoll为每个fd指定一个回调函数,当设备就绪,唤醒等待队列上的等待者时,就会调用这个回调函数
  4. epoll所支持的FD上线是最大可以打开的文件数目
5.BIO,NIO,,AIO简介 BIO:同步并阻塞IO模型的应用,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的开销,所以可以通过线程池机制改善。
适用场景:适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4之前唯一的选择。
NIO:同步非阻塞IO模型的应用。服务器实现模式为一个请求一个线程。客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到有I/O请求时才启动一个线程进行处理。
适用场景:适用于连接数目多且连续比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂,JDK1.4开始支持。
AIO:异步非阻塞,服务器实现模式为一个有效请求一个线程,客户端的IO请求都是由OS先完成了再通知服务器应用去启动线程进行处理。
适用场景:连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂,JDK7开始支持。
NIO实现原理 NIO是同步非阻塞,一个请求一个线程,客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到有IO请求时才进行处理。
多路复用原理:它利用单独的线程(内核级)统一检测所有的Socket(套接字),一旦某个Socket有了IO数据,则启动响应的Application处理。
实现原理:在select和poll中利用轮询Socket句柄的方式来实现监测Socket中是否有IO数据到达。
NIO的适用场景:
在文件IO中优势和传统IO不明显,但是如果是网络IO,则是其适用场景。
实现NIO的三要素 Buffer缓冲区,Channel管道,Selector选择器
Buffer是用来存储数据的(数组形式),Channel是用来运输的。
java.nio包下 Buffer为缓冲区抽象类
public abstract class Buffer extends Object{
public final int capacity(); //1
public final int limit(); //2
public final int position(); //3
public final Buffer mark(); //4
public final Buffer reset();
public final Buffer flip(); //5
}
Capacity说明:容量,缓冲区能够容纳数据元素的最大数量(Buffer缓冲区底层是数组,所以capacity不可变)
Limit说明:界限,表示缓冲区中可以操作的数据大小(limit后面的数据不可以进行读写操作)
position说明:位置,表示缓冲区中正在操作的数据位置
mark说明:标记,表示记录当前position的位置,可以通过reset恢复到mark的位置
flip说明:调用此方法后,会将position置0,limit置为之前position的值。
ByteBuffer缓冲区(Buffer的继承子类)主要方法:(除了以上再加)
put(); //将数据写入缓冲区
get(); //将数据从缓冲区读出
java.nio.channels.channel接口
主要实现类:FileChannel,SocketChannel,ServerSocketChannel,DatagramChannel
获取Channel对象的方法:
本地IO:FileInputStream/FileOutputStream/RandomAccessFile
网络IO:Socket,ServerSocket,DatagramSocket
Selector适用时不能与FileChannel一起使用,Selector使用场景是非阻塞的,而FileChannel是阻塞场景下的文件IO,而SocketChannel可以是非阻塞的,所以Selector常与SocketChannel连用。
以下代码:
客户端的FileChannel=>SocketChannel=>服务器的FileChannel
阻塞下Channel+Buffer:
//这是客户端代码 public class TestDemo {public static void main(String[] args) throws Exception { //1.获取通道,向IP127.0.0.1的9999建立通道 SocketChannel socketChannel=SocketChannel.open(new InetSocketAddress("127.0.0.1", 9999)); //2.发送一张图片给服务器 FileChannel fileChannel=FileChannel.open(Paths.get("E:"+File.separator+"psb.jpg")); //3.创建Buffer ByteBuffer buffer=ByteBuffer.allocate(1024); //4.fileChannel承载本地图片,SocketChannel读取本地图片,两管道配合,传输给服务器 while(fileChannel.read(buffer)!=-1){//Buffer在被socketChannel读取前切换成读模式 buffer.flip(); socketChannel.write(buffer); //读完将Buffer切换成写模式,能让管道继续读取文件 buffer.clear(); }//5.关闭流 fileChannel.close(); socketChannel.close(); }}//这里是服务器端 public class Member { public static void main(String[] args) throws Exception{//1.获取通道 ServerSocketChannel server=ServerSocketChannel.open(); //2.得到文件通道,将客户端传递过来的图片写到本地项目下(写模式,没有则创建) FileChannel outChannel=FileChannel.open(Paths.get("2.jpg"), StandardOpenOption.WRITE,StandardOpenOption.CREATE); //3.绑定连接通道(这里的Socket套接字侧面反映了TCP,UDP) server.bind(new InetSocketAddress(9999)); //监听服务器的6666端口数据请求//4.获取客户端的连接(阻塞的) SocketChannel client=server.accept(); //5.同样与服务器端的SocketChannel对应的Buffer缓冲区 ByteBuffer buffer=ByteBuffer.allocate(1024); //6.将客户端传来的图片保存在本地中 while(client.read(buffer)!=-1){ //在读之前都要切换成读模式 buffer.flip(); outChannel.write(buffer); //读完切换成写模式,能让管道继续读取文件的数据 buffer.clear(); }//7.关闭通道 outChannel.close(); client.close(); server.close(); } }

然后运行服务器端代码,再运行客户端请求。的确将客户端E盘的图片文件通过Socket管道,以FileChannel阻塞方式,上传到了服务器端。
阻塞非阻塞|阻塞非阻塞 同步异步 IO模型及其应用 NIO实现原理
文章图片
image.png 非阻塞下:SocketChannel+Selector+Buffer:
以上阻塞态如果不关闭流,则服务器端一直会读取客户端发来的数据,进而阻塞,所以要使用socketChannel
public class TestDemo {//这是客户端代码 public static void main(String[] args) throws Exception { //1.获取通道,向IP127.0.0.1的9999建立通道 SocketChannel socketChannel=SocketChannel.open(new InetSocketAddress("127.0.0.1", 9999)); socketChannel.configureBlocking(false); //非阻塞状态 //2.发送一张图片给服务器 FileChannel fileChannel=FileChannel.open(Paths.get("E:"+File.separator+"psb.jpg")); //3.创建Buffer ByteBuffer buffer=ByteBuffer.allocate(1024); //4.fileChannel承载本地图片,SocketChannel读取本地图片,两管道配合,传输给服务器 while(fileChannel.read(buffer)!=-1){//Buffer在被socketChannel读取前切换成读模式 buffer.flip(); socketChannel.write(buffer); //读完将Buffer切换成写模式,能让管道继续读取文件 buffer.clear(); }//5.关闭流 fileChannel.close(); socketChannel.close(); }}public class Member { public static void main(String[] args) throws Exception{ //这里是服务器端//1.获取通道 ServerSocketChannel server=ServerSocketChannel.open(); //2.切换成非阻塞模式 server.configureBlocking(false); //3.绑定连接通道(这里的Socket套接字侧面反映了TCP,UDP) server.bind(new InetSocketAddress(9999)); //监听服务器的6666端口数据请求//4.获取选择器 Selector selector=Selector.open(); //4.1将通道注册到选择器上,指定接收监听通道"事件,回调接收就绪事件代码 server.register(selector, SelectionKey.OP_ACCEPT); //5.轮询的获取选择器上已 就绪 的事件(只要select()>0,说明已经就绪)(这里非阻塞才真正体现多路复用) while(selector.select()>0){ //6.使用Iterator遍历句柄 Iteratoriterator=selector.selectedKeys().iterator(); //7.获取已经就绪的事件 while(iterator.hasNext()){ SelectionKey selectionKey=iterator.next(); //接收事件就绪 if(selectionKey.isAcceptable()){ //8.获取客户端连接 SocketChannel client=server.accept(); //8.1 切换成非阻塞态 这样才能使用FileChannel client.configureBlocking(false); //8.2 注册到选择器上-->拿到客户端的连接为了读取通道的数据(指定监听读就绪事件) 回调读状态代码 client.register(selector, SelectionKey.OP_READ); }//读事件就绪 else if(selectionKey.isReadable()){ //9.获取当前选择器读就绪状态的通道 SocketChannel client=(SocketChannel)selectionKey.channel(); //9.1读取数据 ByteBuffer buffer=ByteBuffer.allocate(1024); //9.2得到文件通道,将客户端传递过来的图片写道服务器本地(写模式,没有则创建) FileChannel outChannel=FileChannel.open(Paths.get("2.jpg"), StandardOpenOption.WRITE,StandardOpenOption.CREATE); while(client.read(buffer)!=-1){ //在读之前都要切换成读模式 buffer.flip(); outChannel.write(buffer); //读完切换成写模式,能让管道继续读取文件的数据 buffer.clear(); } }//取消选择键(已经处理过的事情,就取消) iterator.remove(); } }//7.关闭通道 //outChannel.close(); //client.close(); server.close(); } }

同样也上传了图片。这里加入了Selector选择器的NIO非阻塞,才真正实现了IO多路复用,并且通过选择器状态的不同,回调不同的Channel。并且使用Iterator实现了轮询。
JavaNIO与IO的区别: 1.传统的IO面向流,一个字节一个字节处理数据,而NIO是面向缓冲区的,面向内存块处理数据。
2.Java IO的各种流是阻塞的,当一个线程调用read()或write()时,该线程被阻塞,直到有一些数据被读取。
NIO是非阻塞的,使一个线程从某通道发送请求读取数据。
3.Java NIO的选择器允许一个单独的线程来监视多个输入通道。
4.传统IO是单向的流。NIO是双向的Channel管道,读写都是双向的。
分散读取与聚集写入 分散读取(scatter):将一个Channel数据分散读取到多个Buffer中
聚集写入(gather):将多个缓冲区数据集中写入到一个通道中

    推荐阅读