<noframes id="395jp"><noframes id="395jp"><video id="395jp"><video id="395jp"></video></video>
<i id="395jp"><font id="395jp"><delect id="395jp"></delect></font></i>
<nobr id="395jp"></nobr><noframes id="395jp"><noframes id="395jp"><dl id="395jp"></dl><video id="395jp"></video><noframes id="395jp"><dl id="395jp"></dl>
<video id="395jp"><video id="395jp"><dl id="395jp"></dl></video></video> <nobr id="395jp"><nobr id="395jp"><meter id="395jp"></meter></nobr></nobr>
<video id="395jp"></video><nobr id="395jp"></nobr>
<video id="395jp"></video>

Jack Jiang

我的最新工程MobileIMSDK:http://git.oschina.net/jackjiang/MobileIMSDK
posts - 392, comments - 13, trackbacks - 0, articles - 0

本文由得物技術團隊Uni分享,本文有內容修訂和大量排版優化。

1、引言

關于Java網絡編程中的同步IO和異步IO的區別及原理的文章非常的多,具體來說主要還是在討論Java BIO和Java NIO這兩者,而關于Java AIO的文章就少之又少了(即使用也只是介紹了一下概念和代碼示例)。

在深入了解AIO之前,我注意到以下幾個現象:

  • 1)2011年Java 7發布,它增加了AIO(號稱異步IO網絡編程模型),但12年過去了,平時使用的開發框架和中間件卻還是以NIO為主(例如網絡框架Netty、Mina,Web容器Tomcat、Undertow),這是為什么?
  • 2)Java AIO又稱為NIO 2.0,難道它也是基于NIO來實現的?
  • 3)Netty為什么會舍去了AIO的支持?(點此查看);
  • 4)AIO看起來貌似只是解決了有無,實際是發布了個寂寞?

Java AIO的這些不合常理的現象難免會令人心存疑惑。所以決定寫這篇文章時,我不想只是簡單的把AIO的概念再復述一遍,而是要透過現象,深入分析、思考和并理解Java AIO的本質。

 

技術交流:

(本文已同步發布于:http://www.52im.net/thread-4283-1-1.html

2、我們所理解的異步

AIO的A是Asynchronous(即異步)的意思,在了解AIO的原理之前,我們先理清一下“異步”到底是怎樣的一個概念。

說起異步編程,在平時的開發還是比較常見的。

例如以下的代碼示例:

@Async

publicvoidcreate() {

    //TODO

}

 

publicvoidbuild() {

    executor.execute(() -> build());

}

不管是用@Async注解,還是往線程池里提交任務,他們最終都是同一個結果,就是把要執行的任務,交給另外一個線程來執行。

這個時候,我們可以大致的認為,所謂的“異步”,就是用多線程的方式去并行執行任務。

3、Java BIO和NIO到底是同步還是異步?

Java BIO和NIO到底是同步還是異步,我們先按照異步這個思路,做異步編程。

3.1BIO代碼示例

byte[] data = newbyte[1024];

InputStream in = socket.getInputStream();

in.read(data);

// 接收到數據,異步處理

executor.execute(() -> handle(data));

 

publicvoidhandle(byte[] data) {

    // TODO

}

如上:BIO在read()時,雖然線程阻塞了,但在收到數據時,可以異步啟動一個線程去處理。

3.2NIO代碼示例

selector.select();

Set<SelectionKey> keys = selector.selectedKeys();

Iterator<SelectionKey> iterator = keys.iterator();

while(iterator.hasNext()) {

    SelectionKey key = iterator.next();

    if(key.isReadable()) {

        SocketChannel channel = (SocketChannel) key.channel();

        ByteBuffer byteBuffer = (ByteBuffer) key.attachment();

        executor.execute(() -> {

            try{

                channel.read(byteBuffer);

                handle(byteBuffer);

            } catch(Exception e) {

 

            }

        });

    }

}

 

publicstaticvoidhandle(ByteBuffer buffer) {

    // TODO

}

同理:NIO雖然read()是非阻塞的,通過select()可以阻塞等待數據,在有數據可讀的時候,異步啟動一個線程,去讀取數據和處理數據。

3.3產生的理解偏差

此時我們信誓旦旦地說,Java的BIO和NIO是異步還是同步,取決你的心情,你高興給它個多線程,它就是異步的。

果真如此么?

在翻閱了大量博客文章之后,基本一致的闡明了——BIO和NIO是同步的。

那問題點出在哪呢,是什么造成了我們理解上的偏差呢?

那就是參考系的問題,以前學物理時,公交車上的乘客是運動還是靜止,需要有參考系前提,如果以地面為參考,他是運動的,以公交車為參考,他是靜止的。

Java IO也是一樣,需要有個參考系,才能定義它是同步還是異步。

既然我們討論的是關于Java IO是哪一種模式,那就是要針對IO讀寫操作這件事來理解,而其他的啟動另外一個線程去處理數據,已經是脫離IO讀寫的范圍了,不應該把他們扯進來。

3.4嘗試定義異步

所以以IO讀寫操作這事件作為參照,我們先嘗試的這樣定義,就是:發起IO讀寫的線程(調用read和write的線程),和實際操作IO讀寫的線程,如果是同一個線程,就稱之為同步,否則是異步。

按上述定義:

  • 1)顯然BIO只能是同步,調用in.read()當前線程阻塞,有數據返回的時候,接收到數據的還是原來的線程;
  • 2)而NIO也稱之為同步,原因也是如此,調用channel.read()時,線程雖然不會阻塞,但讀到數據的還是當前線程。

按照這個思路,AIO應該是發起IO讀寫的線程,和實際收到數據的線程,可能不是同一個線程。

是不是這樣呢?我們將在上一節直接上Java AIO的代碼,我們從 實際代碼中一窺究竟吧。

4、一個Java AIO的網絡編程示例

4.1AIO服務端程序代碼

publicclassAioServer {

 

    publicstaticvoidmain(String[] args) throwsIOException {

        System.out.println(Thread.currentThread().getName() + " AioServer start");

        AsynchronousServerSocketChannel serverChannel = AsynchronousServerSocketChannel.open()

                .bind(newInetSocketAddress("127.0.0.1", 8080));

        serverChannel.accept(null, newCompletionHandler<AsynchronousSocketChannel, Void>() {

 

            @Override

            publicvoidcompleted(AsynchronousSocketChannel clientChannel, Void attachment) {

                System.out.println(Thread.currentThread().getName() + " client is connected");

                ByteBuffer buffer = ByteBuffer.allocate(1024);

                clientChannel.read(buffer, buffer, newClientHandler());

            }

 

            @Override

            publicvoidfailed(Throwable exc, Void attachment) {

                System.out.println("accept fail");

            }

        });

        System.in.read();

    }

}

 

publicclassClientHandler implementsCompletionHandler<Integer, ByteBuffer> {

    @Override

    publicvoidcompleted(Integer result, ByteBuffer buffer) {

        buffer.flip();

        byte[] data = newbyte[buffer.remaining()];

        buffer.get(data);

        System.out.println(Thread.currentThread().getName() + " received:"+ newString(data, StandardCharsets.UTF_8));

    }

 

    @Override

    publicvoidfailed(Throwable exc, ByteBuffer buffer) {

 

    }

}

4.2AIO客戶端程序

publicclassAioClient {

    publicstaticvoidmain(String[] args) throwsException {

        AsynchronousSocketChannel channel = AsynchronousSocketChannel.open();

        channel.connect(newInetSocketAddress("127.0.0.1", 8080));

        ByteBuffer buffer = ByteBuffer.allocate(1024);

        buffer.put("Java AIO".getBytes(StandardCharsets.UTF_8));

        buffer.flip();

        Thread.sleep(1000L);

        channel.write(buffer);

 }

}

4.3異步的定義猜想結論

分別運行服務端和客戶端程序:

在服務端運行結果里:

1)main線程發起serverChannel.accept的調用,添加了一個CompletionHandler監聽回調,當有客戶端連接過來時,Thread-5線程執行了accep的completed回調方法。

2)緊接著Thread-5又發起了clientChannel.read調用,也添加了個CompletionHandler監聽回調,當收到數據時,是Thread-1的執行了read的completed回調方法。

這個結論和上面異步猜想一致:發起IO操作(例如accept、read、write)調用的線程,和最終完成這個操作的線程不是同一個,我們把這種IO模式稱之AIO。

當然了,這樣定義AIO只是為了方便我們理解,實際中對異步IO的定義可能更抽象一點。

5、 AIO示例引發思考1:“執行completed()方法的線程是誰創建、什么時候創建?”

一般,這樣的問題,需要從程序的入口的開始了解,但跟線程相關,其實是可以從線程棧的運行情況來定位線程是怎么運行。

只運行AIO服務端程序,客戶端不運行,打印一下線程棧(備注:程序在Linux平臺上運行,其他平臺略有差異)。如下圖所示。

分析線程棧,發現,程序啟動了那么幾個線程:

  • 1)線程Thread-0阻塞在EPoll.wait()方法上;
  • 2)線程Thread-1、Thread-2~Thread-n(n和CPU核心數量一致)從阻塞隊列里take()任務,阻塞等待有任務返回。

此時可以暫定下一個結論:AIO服務端程序啟動之后,就開始創建了這些線程,且線程都處于阻塞等待狀態。

另外:發現這些線程的運行都跟epoll有關系!

提到epoll,我們印象中,Java NIO在Linux平臺底層就是用epoll來實現的,難道Java AIO也是用epoll來實現么?

為了證實這個結論,我們從下一個問題來展開討論。

6、 AIO示例引發思考2:AIO注冊事件監聽和執行回調是如何實現的?

帶著這個問題,去閱讀JDK分析源碼時,發現源碼特別的長,而源碼解析是一項枯燥乏味的過程,很容易把閱讀者給逼走勸退掉。

對于長流程和邏輯復雜的代碼的理解,我們可以抓住它幾個脈絡,找出哪幾個核心流程。

以注冊監聽read為例clientChannel.read(...),它主要的核心流程是:注冊事件 -> 監聽事件 -> 處理事件。

注冊事件:

注:注冊事件調用EPoll.ctl(...)函數,這個函數在最后的參數用于指定是一次性的,還是永久性。上面代碼events | EPOLLONSHOT字面意思看來,是一次性的。

監聽事件:

處理事件:

 

核心流程總結:

在分析完上面的代碼流程后會發現:每一次IO讀寫都要經歷的這三個事件是一次性的,也就是在處理事件完,本次流程就結束了,如果想繼續下一次的IO讀寫,就得從頭開始再來一遍。這樣就會存在所謂的死亡回調(回調方法里再添加下一個回調方法),這對于編程的復雜度大大提高了。

7、 AIO示例引發思考3:監聽回調的本質是什么?

7.1概述

先說一下結論:所謂監聽回調的本質,就是用戶態線程調用內核態的函數(準確的說是API,例如read、write、epollWait),該函數還沒有返回時,用戶線程被阻塞了。當函數返回時,會喚醒阻塞的線程,執行所謂回調函數。

對于這個結論的理解,要先引入幾個概念。

7.2系統調用與函數調用

函數調用:找到某個函數,并執行函數里的相關命令。

系統調用:操作系統對用戶應用程序提供了編程接口,所謂API。

系統調用執行過程:

  • 1)傳遞系統調用參數;
  • 2)執行陷入指令,用用戶態切換到核心態(這是因為系統調用一般都需要再核心態下執行);
  • 3)執行系統調用程序;
  • 4)返回用戶態。

7.3用戶態和內核態之間的通信

用戶態->內核態:通過系統調用方式即可。

內核態->用戶態:內核態根本不知道用戶態程序有什么函數,參數是啥,地址在哪里。所以內核是不可能去調用用戶態的函數,只能通過發送信號,比如kill 命令關閉程序就是通過發信號讓用戶程序優雅退出的。

既然內核態是不可能主動去調用用戶態的函數,為什么還會有回調呢,只能說這個所謂回調其實就是用戶態的自導自演。它既做了監聽,又做了執行回調函數。

7.4用實際例子驗證結論

為了驗證這個結論是否有說服力,舉個例子:平時開發寫代碼用的IntelliJ IDEA,它是如何監聽鼠標、鍵盤事件和處理事件的。

按照慣例,先打印一下線程棧,會發現鼠標、鍵盤等事件的監聽是由“AWT-XAWT”線程負責的,處理事件則是“AWT-EventQueue”線程負責。如下圖所示。

定位到具體的代碼上:可以看到“AWT-XAWT”正在做while循環,調用waitForEvents函數等待事件返回。如果沒有事件,線程就一直阻塞在那邊。如下圖所示。

8、Java AIO的本質是什么?

8.1Java AIO的本質,就是只在用戶態實現了異步

由于內核態無法直接調用用戶態函數,Java AIO的本質,就是只在用戶態實現異步,并沒有達到理想意義上的異步。

1)理想中的異步:

何謂理想意義上的異步?這里舉個網購的例子。

兩個角色,消費者A、快遞員B:

  • 1)A在網上購物時,填好家庭地址付款提交訂單,這個相當于注冊監聽事件;
  • 2)商家發貨,B把東西送到A家門口,這個相當于回調。

A在網上下完單,后續的發貨流程就不用他來操心了,可以繼續做其他事。B送貨也不關心A在不在家,反正就把貨扔到家門口就行了,兩個人互不依賴,互不相干擾。

假設A購物是用戶態來做,B送快遞是內核態來做,這種程序運行方式過于理想了,實際中實現不了。

2)現實中的異步:

A住的是高檔小區,不能隨意進去,快遞只能送到小區門口。

A買了一件比較重的商品,比如一臺電視,因為A要上班不在家里,所以找了一個好友C幫忙把電視搬到他家。

A出門上班前,跟門口的保安D打聲招呼,說今天有一臺電視送過來,送到小區門口時,請電話聯系C,讓他過來拿。

具體就是:

  • 1)此時,A下單并跟D打招呼,相當于注冊事件。在AIO中就是EPoll.ctl(...)注冊事件;
  • 2)保安在門口蹲著相當于監聽事件,在AIO中就是Thread-0線程,做EPoll.wait(..);
  • 3)快遞員把電視送到門口,相當于有IO事件到達;
  • 4)保安通知C電視到了,C過來搬電視,相當于處理事件(在AIO中就是Thread-0往任務隊列提交任務,Thread-1 ~n去取數據,并執行回調方法)。

整個過程中,保安D必須一直蹲著,寸步不能離開,否則電視送到門口,就被人偷了。

好友C也必須在A家待著,受人委托,東西到了,人卻不在現場,這有點失信于人。

所以實際的異步和理想中的異步,在互不依賴,互不干擾,這兩點相違背了。保安的作用最大,這是他人生的高光時刻。

異步過程中的注冊事件、監聽事件、處理事件,還有開啟多線程,這些過程的發起者全是用戶態一手操辦。所以說Java AIO本質只是在用戶態實現了異步,這個和BIO、NIO先阻塞,阻塞喚醒后開啟異步線程處理的本質一致。

8.2Java AIO的其它真相

Java AIO跟NIO一樣:在各個平臺的底層實現方式也不同,在Linux是用epoll、Windows是IOCP、Mac OS是KQueue。原理是大同小異,都是需要一個用戶線程阻塞等待IO事件,一個線程池從隊列里處理事件。

Netty之所以移除掉AIO:很大的原因是在性能上AIO并沒有比NIO高。Linux雖然也有一套原生的AIO實現(類似Windows上的IOCP),但Java AIO在Linux并沒有采用,而是用epoll來實現。

Java AIO不支持UDP。

AIO編程方式略顯復雜,比如“死亡回調”。

9、參考資料

[1] 少啰嗦!一分鐘帶你讀懂Java的NIO和經典IO的區別

[2] 史上最強Java NIO入門:擔心從入門到放棄的,請讀這篇!

[3] Java的BIO和NIO很難懂?用代碼實踐給你看,再不懂我轉行!

[4] Java新一代網絡編程模型AIO原理及Linux系統AIO介紹

[5] 從0到1的快速裂變:詳解快的打車架構設計及技術實踐

[6] 新手入門:目前為止最透徹的的Netty高性能原理和框架架構解析

[7] 史上最通俗Netty框架入門長文:基本介紹、環境搭建、動手實戰

[8] 高性能網絡編程(五):一文讀懂高性能網絡編程中的I/O模型

[9] 高性能網絡編程(六):一文讀懂高性能網絡編程中的線程模型

[10] 高性能網絡編程(七):到底什么是高并發?一文即懂!

[11] 從根上理解高性能、高并發(二):深入操作系統,理解I/O與零拷貝技術

[12] 從根上理解高性能、高并發(三):深入操作系統,徹底理解I/O多路復用

[13] 從根上理解高性能、高并發(四):深入操作系統,徹底理解同步與異步

[14] 從根上理解高性能、高并發(五):深入操作系統,理解高并發中的協程

(本文已同步發布于:http://www.52im.net/thread-4283-1-1.html



作者:Jack Jiang (點擊作者姓名進入Github)
出處:http://www.52im.net/space-uid-1.html
交流:歡迎加入即時通訊開發交流群 215891622
討論:http://www.52im.net/
Jack Jiang同時是【原創Java Swing外觀工程BeautyEye】【輕量級移動端即時通訊框架MobileIMSDK】的作者,可前往下載交流。
本博文 歡迎轉載,轉載請注明出處(也可前往 我的52im.net 找到我)。


只有注冊用戶登錄后才能發表評論。


網站導航:
 
Jack Jiang的 Mail: jb2011@163.com, 聯系QQ: 413980957, 微信: hellojackjiang
久久一级片
<noframes id="395jp"><noframes id="395jp"><video id="395jp"><video id="395jp"></video></video>
<i id="395jp"><font id="395jp"><delect id="395jp"></delect></font></i>
<nobr id="395jp"></nobr><noframes id="395jp"><noframes id="395jp"><dl id="395jp"></dl><video id="395jp"></video><noframes id="395jp"><dl id="395jp"></dl>
<video id="395jp"><video id="395jp"><dl id="395jp"></dl></video></video> <nobr id="395jp"><nobr id="395jp"><meter id="395jp"></meter></nobr></nobr>
<video id="395jp"></video><nobr id="395jp"></nobr>
<video id="395jp"></video>