到底什麼是Java AIO?為什麼Netty會移除AIO?一文搞懂AIO的本質!

来源:https://www.cnblogs.com/sulnyann/archive/2023/09/04/17676567.html
-Advertisement-
Play Games

最近接了一個新需求,業務場景上需要在原有基礎上新增2個欄位,介面新增參數意味著很多類和方法的邏輯都需要改變,需要先判斷是否屬於該業務場景,再做對應的邏輯。原本的打算是在入口處新增變數,在操作數據的時候進行邏輯判斷將變數進行存儲或查詢。 ...



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的本質。


2、我們所理解的非同步


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

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

例如以下的代碼示例:

@Async

public void create() {     //TODO }   public void build() {     executor.execute(() -> build()); }   不管是用@Async註解,還是往線程池裡提交任務,他們最終都是同一個結果,就是把要執行的任務,交給另外一個線程來執行。

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

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


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

3.1BIO代碼示例

byte [] data = new byte[1024];

InputStream in = socket.getInputStream(); in.read(data); // 接收到數據,非同步處理 executor.execute(() -> handle(data));   public void handle(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) {               }         });     } }   public static void handle(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服務端程式代碼

 public class AioServer {       public static void main(String[] args) throws IOException {         System.out.println(Thread.currentThread().getName() + " AioServer start");         AsynchronousServerSocketChannel serverChannel = AsynchronousServerSocketChannel.open()                 .bind(new InetSocketAddress("127.0.0.1", 8080));         serverChannel.accept(null, new CompletionHandler<AsynchronousSocketChannel, Void>() {               @Override             public void completed(AsynchronousSocketChannel clientChannel, Void attachment) {                 System.out.println(Thread.currentThread().getName() + " client is connected");                 ByteBuffer buffer = ByteBuffer.allocate(1024);                 clientChannel.read(buffer, buffer, new ClientHandler());             }               @Override             public void failed(Throwable exc, Void attachment) {                 System.out.println("accept fail");             }         });         System.in.read();     } }   public class ClientHandler implements CompletionHandler<Integer, ByteBuffer> {     @Override     public void completed(Integer result, ByteBuffer buffer) {         buffer.flip();         byte [] data = new byte[buffer.remaining()];         buffer.get(data);         System.out.println(Thread.currentThread().getName() + " received:"  + new String(data, StandardCharsets.UTF_8));     }       @Override     public void failed(Throwable exc, ByteBuffer buffer) {       } }  

4.2AIO客戶端程式

public class AioClient {

    public static void main(String[] args) throws Exception {         AsynchronousSocketChannel channel = AsynchronousSocketChannel.open();         channel.connect(new InetSocketAddress("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編程方式略顯複雜,比如“死亡回調”。

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • # 第一章HTML #### 1.1 html的定義 html是超文本標記語言,是一個基於HTTP(超文本傳輸協議)協議的網頁語言 #### 1.2 html的版本 HTML 4.01 以及具備完善的網頁編輯 HTML 5.0 移動端網頁編輯 XHTML 語法嚴格 #### 1.3 瀏覽器 保障相容 ...
  • ### 歡迎訪問我的GitHub > 這裡分類和彙總了欣宸的全部原創(含配套源碼):[https://github.com/zq2599/blog_demos](https://github.com/zq2599/blog_demos) ### 本篇概覽 - 本文是《LeetCode952三部曲之三 ...
  • # Python文件的基本操作 - 文件的基本操作 - 文件的讀寫模式 - 文件的讀寫操作相關的方法 - 文件的操作模式 - 文件的練習題 ## 文件的基本操作 ```python 1. 我們能夠操作哪些類型的文件: .txt 沒有尾碼名的文件 # 我們現在不能操作word、Excel、PPT等文件 ...
  • ## 單點登錄服務端搭建 1、下載cas包 `https://github.com/apereo/cas-overlay-template/tree/5.3` 這好像是最後一個maven版本的,之後都是grade版本的 2、使用idea打開代碼,導入依賴 3、新建src目錄、resource目錄 4 ...
  • 部署 操作系統:CentOS:7.4,perl版本:v5.16.3,opensearch版本:3.0.8 1.下載地址:https://www.openssl.org/source/ 2.安裝cmd.pm模塊,不然編譯的時候會引發【Can‘t locate IPC/Cmd.pm in @INC】錯誤 ...
  • PE結構是`Windows`系統下最常用的可執行文件格式,理解PE文件格式不僅可以理解操作系統的載入流程,還可以更好的理解操作系統對進程和記憶體相關的管理知識,DOS頭是PE文件開頭的一個固定長度的結構體,這個結構體的大小為64位元組(0x40)。DOS頭包含了很多有用的信息,該信息可以讓Windows... ...
  • 在日常寫Java的時候,對於字元串的操作是非常普遍的,其中最常見的就是對字元串的組織。也因為這個操作非常普遍,所以誕生了很多方案,總下來大概有這麼幾種: - 使用`+`拼接 - 使用`StringBuffer`和`SpringBuilder` - `String::format` and `Stri ...
  • > 本文深入探討了Go編程語言中的核心概念,包括標識符、關鍵字、具名函數、具名值、定義類型、類型別名、包和模塊管理,以及代碼塊和斷行。這些元素是構成Go程式的基礎,也是編寫高質量代碼的關鍵。 > 關註TechLeadCloud,分享互聯網架構、雲服務技術的全維度知識。作者擁有10+年互聯網服務架構、 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...