阻塞非阻塞 同步異步 IO模型及其應(yīng)用 NIO實(shí)現(xiàn)原理

1.同步異步概念

2.阻塞非阻塞概念

3.常見I/O模型:同步阻塞IO,同步非阻塞IO,異步阻塞IO,異步非阻塞IO

4.UNIX系統(tǒng)下的IO多路復(fù)用(OS級(jí)別的I/O多路復(fù)用是重點(diǎn),同步非阻塞I/O的應(yīng)用)***

5.IO中BIO,NIO,AIO簡(jiǎn)介

6.NIO實(shí)現(xiàn)原理(NIO也是同步非阻塞I/O的應(yīng)用)(NIO阻塞代碼實(shí)例 NIO非阻塞代碼實(shí)例 這里的Selector真正體現(xiàn)了多路復(fù)用)(重點(diǎn))***

1.同步異步概念

同步異步是針對(duì)應(yīng)用程序和內(nèi)核的交互而言的。
同步指的是用戶進(jìn)程觸發(fā)IO操作,等待/輪詢的去查看IO操作是否完成。(同步阻塞IO是等待,同步非阻塞是輪詢)
異步指的是用戶進(jìn)程出發(fā)IO操作便開始做別的事,IO操作已經(jīng)完成的時(shí)候會(huì)得到IO完成的通知。

2.阻塞非阻塞概念

阻塞非阻塞是針對(duì)于進(jìn)程在訪問數(shù)據(jù)的時(shí)候,根據(jù)IO操作的就緒狀態(tài)采用的不同操作方式。
阻塞狀態(tài)下,讀取/寫入函數(shù)將一直等待IO操作就緒。
非阻塞狀態(tài)下,讀取/寫入函數(shù)會(huì)立即返回一個(gè)狀態(tài)值。

3.常見的IO模型

同步阻塞IO:在此種模型下,用戶進(jìn)程在發(fā)起一個(gè)IO操作以后,必須等待IO操作的完成。只有當(dāng)真正完成了IO操作以后,用戶進(jìn)程才能運(yùn)行。
應(yīng)用:Java.io包下的傳統(tǒng)IO模型

同步非阻塞IO:在此種模型下,用戶進(jìn)程在發(fā)起一個(gè)IO操作以后邊可返回做其它事情,但是用戶進(jìn)程需要時(shí)不時(shí)的詢問IO操作是否就緒,這就要求用戶進(jìn)程不斷的進(jìn)行輪詢,從而引入不必要的CPU資源浪費(fèi)。
應(yīng)用:jdk1.4出現(xiàn)的java.nio包

異步阻塞IO:在此種模型下,用戶進(jìn)程發(fā)起一個(gè)IO操作以后,不等待內(nèi)核IO操作的完成,等內(nèi)核完成IO操作以后通知應(yīng)用程序。
這就是同步異步的最關(guān)鍵區(qū)別:同步必須等待/主動(dòng)輪詢IO操作是否完成。
為什么說是阻塞的呢?因?yàn)槠涫钦{(diào)用了select函數(shù)來完成,而select函數(shù)本身實(shí)現(xiàn)的方式就是阻塞的。其好處為:可以同時(shí)監(jiān)聽多個(gè)文件句柄,從而提高系統(tǒng)并發(fā)性。

同步非阻塞IO:在此種模型下,用戶進(jìn)程只需要發(fā)起一個(gè)IO操作然后立即返回,等IO操作真正完成以后,應(yīng)用程序會(huì)得到IO操作完成的通知,此時(shí)用戶進(jìn)程只需要對(duì)數(shù)據(jù)進(jìn)行處理,不需要進(jìn)行實(shí)際的IO讀寫操作,因?yàn)檎嬲腎O讀取或?qū)懭氩僮饕呀?jīng)由內(nèi)核完成了。

4.UNIX系統(tǒng)下的IO多路復(fù)用(OS級(jí)別的I/O多路復(fù)用是重點(diǎn),同步非阻塞I/O的應(yīng)用)

UNIX系統(tǒng)秉承了一切皆文件的思想。
IO多路復(fù)用機(jī)制是同步非阻塞IO的應(yīng)用。
它利用單獨(dú)的線程(內(nèi)核級(jí))統(tǒng)一檢測(cè)所有的Socket(套接字),一旦某個(gè)Socket有了IO數(shù)據(jù),則啟動(dòng)響應(yīng)的Application處理。
實(shí)現(xiàn)原理:在select和poll中利用輪詢Socket句柄的方式來實(shí)現(xiàn)監(jiān)測(cè)Socket中是否有IO數(shù)據(jù)到達(dá)。

select底層是數(shù)組,poll是鏈表,epoll是哈希表。
select和epoll區(qū)別:

  1. 每次調(diào)用select,都需要把fd集合(句柄集合)從用戶態(tài)拷貝到內(nèi)核態(tài),這個(gè)開銷在fd很多時(shí)開銷很大
  2. 同時(shí)每次調(diào)用select都需要在內(nèi)核遍歷傳遞進(jìn)來的所有的fd,這個(gè)開銷在fd很多時(shí)也很大
  3. select支持的文件描述符數(shù)量太小了,默認(rèn)是1024
    4.epoll為每個(gè)fd指定一個(gè)回調(diào)函數(shù),當(dāng)設(shè)備就緒,喚醒等待隊(duì)列上的等待者時(shí),就會(huì)調(diào)用這個(gè)回調(diào)函數(shù)
  4. epoll所支持的FD上線是最大可以打開的文件數(shù)目

5.BIO,NIO,,AIO簡(jiǎn)介

BIO:同步并阻塞IO模型的應(yīng)用,服務(wù)器實(shí)現(xiàn)模式為一個(gè)連接一個(gè)線程,即客戶端有連接請(qǐng)求時(shí)服務(wù)器就需要啟動(dòng)一個(gè)線程進(jìn)行處理,如果這個(gè)連接不做任何事情會(huì)造成不必要的開銷,所以可以通過線程池機(jī)制改善。
適用場(chǎng)景:適用于連接數(shù)目比較小且固定的架構(gòu),這種方式對(duì)服務(wù)器資源要求比較高,并發(fā)局限于應(yīng)用中,JDK1.4之前唯一的選擇。

NIO:同步非阻塞IO模型的應(yīng)用。服務(wù)器實(shí)現(xiàn)模式為一個(gè)請(qǐng)求一個(gè)線程??蛻舳税l(fā)送的連接請(qǐng)求都會(huì)注冊(cè)到多路復(fù)用器上,多路復(fù)用器輪詢到有I/O請(qǐng)求時(shí)才啟動(dòng)一個(gè)線程進(jìn)行處理。
適用場(chǎng)景:適用于連接數(shù)目多且連續(xù)比較短(輕操作)的架構(gòu),比如聊天服務(wù)器,并發(fā)局限于應(yīng)用中,編程比較復(fù)雜,JDK1.4開始支持。

AIO:異步非阻塞,服務(wù)器實(shí)現(xiàn)模式為一個(gè)有效請(qǐng)求一個(gè)線程,客戶端的IO請(qǐng)求都是由OS先完成了再通知服務(wù)器應(yīng)用去啟動(dòng)線程進(jìn)行處理。
適用場(chǎng)景:連接數(shù)目多且連接比較長(zhǎng)(重操作)的架構(gòu),比如相冊(cè)服務(wù)器,充分調(diào)用OS參與并發(fā)操作,編程比較復(fù)雜,JDK7開始支持。

NIO實(shí)現(xiàn)原理

NIO是同步非阻塞,一個(gè)請(qǐng)求一個(gè)線程,客戶端發(fā)送的連接請(qǐng)求都會(huì)注冊(cè)到多路復(fù)用器上,多路復(fù)用器輪詢到有IO請(qǐng)求時(shí)才進(jìn)行處理。
多路復(fù)用原理:它利用單獨(dú)的線程(內(nèi)核級(jí))統(tǒng)一檢測(cè)所有的Socket(套接字),一旦某個(gè)Socket有了IO數(shù)據(jù),則啟動(dòng)響應(yīng)的Application處理。
實(shí)現(xiàn)原理:在select和poll中利用輪詢Socket句柄的方式來實(shí)現(xiàn)監(jiān)測(cè)Socket中是否有IO數(shù)據(jù)到達(dá)。

NIO的適用場(chǎng)景:
在文件IO中優(yōu)勢(shì)和傳統(tǒng)IO不明顯,但是如果是網(wǎng)絡(luò)IO,則是其適用場(chǎng)景。

實(shí)現(xiàn)NIO的三要素

Buffer緩沖區(qū),Channel管道,Selector選擇器
Buffer是用來存儲(chǔ)數(shù)據(jù)的(數(shù)組形式),Channel是用來運(yùn)輸?shù)摹?/p>

java.nio包下 Buffer為緩沖區(qū)抽象類
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說明:容量,緩沖區(qū)能夠容納數(shù)據(jù)元素的最大數(shù)量(Buffer緩沖區(qū)底層是數(shù)組,所以capacity不可變)
Limit說明:界限,表示緩沖區(qū)中可以操作的數(shù)據(jù)大小(limit后面的數(shù)據(jù)不可以進(jìn)行讀寫操作)
position說明:位置,表示緩沖區(qū)中正在操作的數(shù)據(jù)位置
mark說明:標(biāo)記,表示記錄當(dāng)前position的位置,可以通過reset恢復(fù)到mark的位置
flip說明:調(diào)用此方法后,會(huì)將position置0,limit置為之前position的值。

ByteBuffer緩沖區(qū)(Buffer的繼承子類)主要方法:(除了以上再加)
put();//將數(shù)據(jù)寫入緩沖區(qū)
get();//將數(shù)據(jù)從緩沖區(qū)讀出

java.nio.channels.channel接口
主要實(shí)現(xiàn)類:FileChannel,SocketChannel,ServerSocketChannel,DatagramChannel

獲取Channel對(duì)象的方法:
本地IO:FileInputStream/FileOutputStream/RandomAccessFile
網(wǎng)絡(luò)IO:Socket,ServerSocket,DatagramSocket

Selector適用時(shí)不能與FileChannel一起使用,Selector使用場(chǎng)景是非阻塞的,而FileChannel是阻塞場(chǎng)景下的文件IO,而SocketChannel可以是非阻塞的,所以Selector常與SocketChannel連用。

以下代碼:
客戶端的FileChannel=>SocketChannel=>服務(wù)器的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.發(fā)送一張圖片給服務(wù)器
        FileChannel fileChannel=FileChannel.open(Paths.get("E:"+File.separator+"psb.jpg"));

        //3.創(chuàng)建Buffer
        ByteBuffer buffer=ByteBuffer.allocate(1024);

        //4.fileChannel承載本地圖片,SocketChannel讀取本地圖片,兩管道配合,傳輸給服務(wù)器
        while(fileChannel.read(buffer)!=-1){

            //Buffer在被socketChannel讀取前切換成讀模式
            buffer.flip();

            socketChannel.write(buffer);

            //讀完將Buffer切換成寫模式,能讓管道繼續(xù)讀取文件
            buffer.clear();
        }

        //5.關(guān)閉流
        fileChannel.close();
        socketChannel.close();
    }

}

//這里是服務(wù)器端
public class Member {
    public static void main(String[] args) throws Exception{
        

        //1.獲取通道
        ServerSocketChannel server=ServerSocketChannel.open();

        //2.得到文件通道,將客戶端傳遞過來的圖片寫到本地項(xiàng)目下(寫模式,沒有則創(chuàng)建)
        FileChannel outChannel=FileChannel.open(Paths.get("2.jpg"), StandardOpenOption.WRITE,StandardOpenOption.CREATE);

        //3.綁定連接通道(這里的Socket套接字側(cè)面反映了TCP,UDP)
        server.bind(new InetSocketAddress(9999));//監(jiān)聽服務(wù)器的6666端口數(shù)據(jù)請(qǐng)求

        //4.獲取客戶端的連接(阻塞的)
        SocketChannel client=server.accept();

        //5.同樣與服務(wù)器端的SocketChannel對(duì)應(yīng)的Buffer緩沖區(qū)
        ByteBuffer buffer=ByteBuffer.allocate(1024);

        //6.將客戶端傳來的圖片保存在本地中
        while(client.read(buffer)!=-1){
            //在讀之前都要切換成讀模式
            buffer.flip();

            outChannel.write(buffer);

            //讀完切換成寫模式,能讓管道繼續(xù)讀取文件的數(shù)據(jù)
            buffer.clear();
        }

        //7.關(guān)閉通道
        outChannel.close();
        client.close();
        server.close();
    }
}

然后運(yùn)行服務(wù)器端代碼,再運(yùn)行客戶端請(qǐng)求。的確將客戶端E盤的圖片文件通過Socket管道,以FileChannel阻塞方式,上傳到了服務(wù)器端。

image.png

非阻塞下:SocketChannel+Selector+Buffer:
以上阻塞態(tài)如果不關(guān)閉流,則服務(wù)器端一直會(huì)讀取客戶端發(fā)來的數(shù)據(jù),進(jìn)而阻塞,所以要使用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);//非阻塞狀態(tài)
        //2.發(fā)送一張圖片給服務(wù)器
        FileChannel fileChannel=FileChannel.open(Paths.get("E:"+File.separator+"psb.jpg"));

        //3.創(chuàng)建Buffer
        ByteBuffer buffer=ByteBuffer.allocate(1024);

        //4.fileChannel承載本地圖片,SocketChannel讀取本地圖片,兩管道配合,傳輸給服務(wù)器
        while(fileChannel.read(buffer)!=-1){

            //Buffer在被socketChannel讀取前切換成讀模式
            buffer.flip();

            socketChannel.write(buffer);

            //讀完將Buffer切換成寫模式,能讓管道繼續(xù)讀取文件
            buffer.clear();
        }

        //5.關(guān)閉流
        fileChannel.close();
        socketChannel.close();
    }

}


public class Member {
    public static void main(String[] args) throws Exception{
        //這里是服務(wù)器端

        //1.獲取通道
        ServerSocketChannel server=ServerSocketChannel.open();

        //2.切換成非阻塞模式
        server.configureBlocking(false);

    

        //3.綁定連接通道(這里的Socket套接字側(cè)面反映了TCP,UDP)
        server.bind(new InetSocketAddress(9999));//監(jiān)聽服務(wù)器的6666端口數(shù)據(jù)請(qǐng)求

        //4.獲取選擇器
        Selector selector=Selector.open();

        //4.1將通道注冊(cè)到選擇器上,指定接收監(jiān)聽通道"事件,回調(diào)接收就緒事件代碼
        server.register(selector, SelectionKey.OP_ACCEPT);

        //5.輪詢的獲取選擇器上已 就緒 的事件(只要select()>0,說明已經(jīng)就緒)(這里非阻塞才真正體現(xiàn)多路復(fù)用)
        while(selector.select()>0){
            //6.使用Iterator遍歷句柄
            Iterator<SelectionKey>iterator=selector.selectedKeys().iterator();

            //7.獲取已經(jīng)就緒的事件
            while(iterator.hasNext()){
                SelectionKey selectionKey=iterator.next();

                //接收事件就緒
                if(selectionKey.isAcceptable()){
                    //8.獲取客戶端連接
                    SocketChannel client=server.accept();
                    //8.1 切換成非阻塞態(tài) 這樣才能使用FileChannel
                    client.configureBlocking(false);
                    //8.2 注冊(cè)到選擇器上-->拿到客戶端的連接為了讀取通道的數(shù)據(jù)(指定監(jiān)聽讀就緒事件) 回調(diào)讀狀態(tài)代碼
                    client.register(selector, SelectionKey.OP_READ);

                }//讀事件就緒
                else if(selectionKey.isReadable()){
                    //9.獲取當(dāng)前選擇器讀就緒狀態(tài)的通道
                    SocketChannel client=(SocketChannel)selectionKey.channel();

                    //9.1讀取數(shù)據(jù)
                    ByteBuffer buffer=ByteBuffer.allocate(1024);

                    //9.2得到文件通道,將客戶端傳遞過來的圖片寫道服務(wù)器本地(寫模式,沒有則創(chuàng)建)
                    FileChannel outChannel=FileChannel.open(Paths.get("2.jpg"), StandardOpenOption.WRITE,StandardOpenOption.CREATE);

                    while(client.read(buffer)!=-1){
                        //在讀之前都要切換成讀模式
                        buffer.flip();
            
                        outChannel.write(buffer);
            
                        //讀完切換成寫模式,能讓管道繼續(xù)讀取文件的數(shù)據(jù)
                        buffer.clear();
                    }
                }

                //取消選擇鍵(已經(jīng)處理過的事情,就取消)
                iterator.remove();
            
            }
        }

    
        

        //7.關(guān)閉通道
        //outChannel.close();
        //client.close();
        server.close();
    }
}


同樣也上傳了圖片。這里加入了Selector選擇器的NIO非阻塞,才真正實(shí)現(xiàn)了IO多路復(fù)用,并且通過選擇器狀態(tài)的不同,回調(diào)不同的Channel。并且使用Iterator實(shí)現(xiàn)了輪詢。

JavaNIO與IO的區(qū)別:

1.傳統(tǒng)的IO面向流,一個(gè)字節(jié)一個(gè)字節(jié)處理數(shù)據(jù),而NIO是面向緩沖區(qū)的,面向內(nèi)存塊處理數(shù)據(jù)。
2.Java IO的各種流是阻塞的,當(dāng)一個(gè)線程調(diào)用read()或write()時(shí),該線程被阻塞,直到有一些數(shù)據(jù)被讀取。
NIO是非阻塞的,使一個(gè)線程從某通道發(fā)送請(qǐng)求讀取數(shù)據(jù)。
3.Java NIO的選擇器允許一個(gè)單獨(dú)的線程來監(jiān)視多個(gè)輸入通道。
4.傳統(tǒng)IO是單向的流。NIO是雙向的Channel管道,讀寫都是雙向的。

分散讀取與聚集寫入

分散讀取(scatter):將一個(gè)Channel數(shù)據(jù)分散讀取到多個(gè)Buffer中
聚集寫入(gather):將多個(gè)緩沖區(qū)數(shù)據(jù)集中寫入到一個(gè)通道中

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容