到底什麼是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
  • 示例項目結構 在 Visual Studio 中創建一個 WinForms 應用程式後,項目結構如下所示: MyWinFormsApp/ │ ├───Properties/ │ └───Settings.settings │ ├───bin/ │ ├───Debug/ │ └───Release/ ...
  • [STAThread] 特性用於需要與 COM 組件交互的應用程式,尤其是依賴單線程模型(如 Windows Forms 應用程式)的組件。在 STA 模式下,線程擁有自己的消息迴圈,這對於處理用戶界面和某些 COM 組件是必要的。 [STAThread] static void Main(stri ...
  • 在WinForm中使用全局異常捕獲處理 在WinForm應用程式中,全局異常捕獲是確保程式穩定性的關鍵。通過在Program類的Main方法中設置全局異常處理,可以有效地捕獲並處理未預見的異常,從而避免程式崩潰。 註冊全局異常事件 [STAThread] static void Main() { / ...
  • 前言 給大家推薦一款開源的 Winform 控制項庫,可以幫助我們開發更加美觀、漂亮的 WinForm 界面。 項目介紹 SunnyUI.NET 是一個基於 .NET Framework 4.0+、.NET 6、.NET 7 和 .NET 8 的 WinForm 開源控制項庫,同時也提供了工具類庫、擴展 ...
  • 說明 該文章是屬於OverallAuth2.0系列文章,每周更新一篇該系列文章(從0到1完成系統開發)。 該系統文章,我會儘量說的非常詳細,做到不管新手、老手都能看懂。 說明:OverallAuth2.0 是一個簡單、易懂、功能強大的許可權+可視化流程管理系統。 有興趣的朋友,請關註我吧(*^▽^*) ...
  • 一、下載安裝 1.下載git 必須先下載並安裝git,再TortoiseGit下載安裝 git安裝參考教程:https://blog.csdn.net/mukes/article/details/115693833 2.TortoiseGit下載與安裝 TortoiseGit,Git客戶端,32/6 ...
  • 前言 在項目開發過程中,理解數據結構和演算法如同掌握蓋房子的秘訣。演算法不僅能幫助我們編寫高效、優質的代碼,還能解決項目中遇到的各種難題。 給大家推薦一個支持C#的開源免費、新手友好的數據結構與演算法入門教程:Hello演算法。 項目介紹 《Hello Algo》是一本開源免費、新手友好的數據結構與演算法入門 ...
  • 1.生成單個Proto.bat內容 @rem Copyright 2016, Google Inc. @rem All rights reserved. @rem @rem Redistribution and use in source and binary forms, with or with ...
  • 一:背景 1. 講故事 前段時間有位朋友找到我,說他的窗體程式在客戶這邊出現了卡死,讓我幫忙看下怎麼回事?dump也生成了,既然有dump了那就上 windbg 分析吧。 二:WinDbg 分析 1. 為什麼會卡死 窗體程式的卡死,入口門檻很低,後續往下分析就不一定了,不管怎麼說先用 !clrsta ...
  • 前言 人工智慧時代,人臉識別技術已成為安全驗證、身份識別和用戶交互的關鍵工具。 給大家推薦一款.NET 開源提供了強大的人臉識別 API,工具不僅易於集成,還具備高效處理能力。 本文將介紹一款如何利用這些API,為我們的項目添加智能識別的亮點。 項目介紹 GitHub 上擁有 1.2k 星標的 C# ...