阻塞非阻塞 同步异步 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数据到达。

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阻塞方式,上传到了服务器端。

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遍历句柄
            Iterator<SelectionKey>iterator=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):将多个缓冲区数据集中写入到一个通道中

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 162,710评论 4 376
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,839评论 2 308
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 112,295评论 0 255
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,776评论 0 223
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 53,198评论 3 297
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 41,074评论 1 226
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,200评论 2 322
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,986评论 0 214
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,733评论 1 250
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,877评论 2 254
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,348评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,675评论 3 265
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,393评论 3 246
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,209评论 0 9
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,996评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,212评论 2 287
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 36,003评论 2 280

推荐阅读更多精彩内容