透過現象看Java AIO的本質 | 得物技術
1.前言
關於Java BIO、NIO、AIO的區別和原理,這樣的文章非常的多的,但主要還是在BIO和NIO這兩者之間討論,而關於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的支援。https://github.com/netty/netty/issues/2515
4、 AIO看起來只是解決了有無,釋出了個寂寞。
這幾個現象不免會令很多人心存疑惑,所以決定寫這篇文章時,不想簡單的把AIO的概念再複述一遍,而是要透過現象, 如何分析、思考和理解Java AIO的本質。
2.什麼是非同步
2.1 我們所瞭解的非同步
AIO的A是Asynchronous非同步的意思,在瞭解AIO的原理之前,我們先理清一下“非同步”到底是怎樣的一個概念。
說起非同步程式設計,在平時的開發還是比較常見,例如以下的程式碼示例:
@Async
public void create() {
//TODO
}
public void build() {
executor.execute(() -> build());
}
不管是用@Async註解,還是往執行緒池裡提交任務,他們最終都是同一個結果,就是把要執行的任務,交給另外一個執行緒來執行。
這個時候,可以大致的認為,所謂的“非同步”,就是多執行緒,執行任務。
2.2 Java BIO和NIO到底是同步還是非同步?
Java BIO和NIO到底是同步還是非同步,我們先按照非同步這個思路,做非同步程式設計。
2.2.1 BIO示例
byte [] data = new byte[1024];
InputStream in = socket.getInputStream();
in.read(data);
// 接收到資料,非同步處理
executor.execute(() -> handle(data));
public void handle(byte [] data) {
// TODO
}
BIO在read()時,雖然執行緒阻塞了,但在收到資料時,可以非同步啟動一個執行緒去處理。
2.2.2 NIO示例
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()可以阻塞等待資料,在有資料可讀的時候,非同步啟動一個執行緒,去讀取資料和處理資料。
2.2.3 產生理解的偏差
此時我們信誓旦旦的說,Java的BIO和NIO是非同步還是同步,取決你的心情,你高興給它個多執行緒,它就是非同步的。
但果真如此麼,在翻閱了大量部落格文章之後,基本一致的闡明瞭,BIO和NIO是同步的。
那問題點出在哪呢,是什麼造成了我們理解上的偏差呢?
那就是參考系的問題,以前學物理時,公交車上的乘客是運動還是靜止,需要有參考系前提,如果以地面為參考,他是運動的,以公交車為參考,他是靜止的。
Java IO也是一樣,需要有個參考系,才能定義它是同步非同步,既然我們討論的是IO是哪一種模式,那就是要針對IO讀寫操作這件事來理解,而其他的啟動另外一個執行緒去處理資料,已經是脫離IO讀寫的範圍了,不應該把他們扯進來。
2.2.4 嘗試定義非同步
所以以IO讀寫操作這事件作為參照,我們先嚐試的這樣定義,就是發起IO讀寫的執行緒(呼叫read和write的執行緒),和實際操作IO讀寫的執行緒,如果是同一個執行緒,就稱之為同步,否則是非同步。
-
顯然BIO只能是同步,呼叫in.read()當前執行緒阻塞,有資料返回的時候,接收到資料的還是原來的執行緒。
-
而NIO也稱之為同步,原因也是如此,呼叫channel.read()時,執行緒雖然不會阻塞,但讀到資料的還是當前執行緒。
按照這個思路,AIO應該是發起IO讀寫的執行緒,和實際收到資料的執行緒,可能不是同一個執行緒
是不是這樣呢,現在開始上Java AIO的程式碼。
2.3 Java AIO的程式示例
2.3.1 AIO服務端程式
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) {
}
}
2.3.2 AIO客戶端程式
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);
}
}
2.3.3 非同步的定義猜想結論
分別執行服務端和客戶端程式
在服務端執行結果裡,
main執行緒發起serverChannel.accept的呼叫,添加了一個CompletionHandler監聽回撥,當有客戶端連線過來時,Thread-5執行緒執行了accep的completed回撥方法。
緊接著Thread-5又發起了clientChannel.read呼叫,也添加了個CompletionHandler監聽回撥,當收到資料時,是Thread-1的執行了read的completed回撥方法。
這個結論和上面非同步猜想一致,發起IO操作(例如accept、read、write)呼叫的執行緒,和最終完成這個操作的執行緒不是同一個,我們把這種IO模式稱之AIO,
當然了,這樣定義AIO只是為了方便我們理解,實際中對非同步IO的定義可能更抽象一點。
3.AIO示例引發思考的問題
1、 執行completed()方法的這個執行緒是誰建立的,什麼時候建立的?
2、 AIO註冊事件監聽和執行回撥是如何實現的?
3、 監聽回撥的本質是什麼?
3.1 問題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來實現麼?為了證實這個結論,我們從下一個問題來展開討論
3.2 問題2:AIO註冊事件監聽和執行回撥是如何實現的
帶著這個問題,去閱讀分析原始碼時,發現原始碼特別的長,而原始碼解析是一項枯燥乏味的過程,很容易把閱讀者給逼走勸退掉。
對於長流程和邏輯複雜的程式碼的理解,我們可以抓住它幾個脈絡,找出哪幾個核心流程。
以註冊監聽read為例clientChannel.read(…),它主要的核心流程是:
1、註冊事件 -> 2、監聽事件 -> 3、處理事件
3.2.1 1、註冊事件
註冊事件呼叫EPoll.ctl(…)函式,這個函式在最後的引數用於指定是一次性的,還是永久性。上面程式碼events | EPOLLONSHOT字面意思看來,是一次性的。
3.2.2 2、監聽事件
3.2.3 3、處理事件
3.2.4 核心流程總結
在分析完上面的程式碼流程後會發現,每一次IO讀寫都要經歷的這三個事件是一次性的,也就是在處理事件完,本次流程就結束了,如果想繼續下一次的IO讀寫,就得從頭開始再來一遍。這樣就會存在所謂的死亡回撥(回撥方法裡再新增下一個回撥方法),這對於程式設計的複雜度大大提高了。
3.3 問題3: 監聽回撥的本質是什麼?
先說一下結論,所謂監聽回撥的本質,就是使用者態執行緒,呼叫核心態的函式(準確的說是API,例如read,write,epollWait),該函式還沒有返回時,使用者執行緒被阻塞了。當函式返回時,會喚醒阻塞的執行緒,執行所謂回撥函式。
對於這個結論的理解,要先引入幾個概念
3.3.1 系統呼叫與函式呼叫
函式呼叫:
找到某個函式,並執行函式裡的相關命令
系統呼叫:
作業系統對使用者應用程式提供了程式設計介面,所謂API。
系統呼叫執行過程:
1.傳遞系統呼叫引數
2.執行陷入指令,用使用者態切換到核心態,這是因為系統呼叫一般都需要再核心態下執行
3.執行系統呼叫程式
4.返回使用者態
3.3.2 使用者態和核心態之間的通訊
使用者態->核心態,通過系統呼叫方式即可。
核心態->使用者態,核心態根本不知道使用者態程式有什麼函式,引數是啥,地址在哪裡。所以核心是不可能去呼叫使用者態的函式,只能通過傳送訊號,比如kill 命令關閉程式就是通過發訊號讓使用者程式優雅退出的。
既然核心態是不可能主動去呼叫使用者態的函式,為什麼還會有回撥呢,只能說這個所謂回撥其實就是使用者態的自導自演。它既做了監聽,又做了執行回撥函式。
3.3.3 用實際例子驗證結論
為了驗證這個結論是否有說服力,舉個例子,平時開發寫程式碼用的IntelliJ IDEA,它是如何監聽滑鼠、鍵盤事件和處理事件的。
按照慣例,先列印一下執行緒棧,會發現滑鼠、鍵盤等事件的監聽是由"AWT-XAWT"執行緒負責的,處理事件則是"AWT-EventQueue"執行緒負責。
定位到具體的程式碼上,可以看到"AWT-XAWT"正在做while迴圈,呼叫waitForEvents函式等待事件返回。如果沒有事件,執行緒就一直阻塞在那邊。
4.Java AIO的本質是什麼?
1、由於核心態無法直接呼叫使用者態函式,Java AIO的本質,就是隻在使用者態實現非同步。並沒有達到理想意義上的非同步。
理想中的非同步
何謂理想意義上的非同步?這裡舉個網購的例子
兩個角色,消費者A,快遞員B
-
A在網上購物時,填好家庭地址付款提交訂單,這個相當於註冊監聽事件
-
商家發貨,B把東西送到A家門口,這個相當於回撥。
A在網上下完單,後續的發貨流程就不用他來操心了,可以繼續做其他事。B送貨也不關心A在不在家,反正就把貨扔到家門口就行了,兩個人互不依賴,互不相干擾。
假設A購物是使用者態來做,B送快遞是核心態來做,這種程式執行方式過於理想了,實際中實現不了。
現實中的非同步
A住的是高檔小區,不能隨意進去,快遞只能送到小區門口。
A買了一件比較重的商品,比如一臺電視,因為A要上班不在家裡,所以找了一個好友C幫忙把電視搬到他家。
A出門上班前,跟門口的保安D打聲招呼,說今天有一臺電視送過來,送到小區門口時,請電話聯絡C,讓他過來拿。
-
此時,A下單並跟D打招呼,相當於註冊事件。在AIO中就是EPoll.ctl(…)註冊事件。
-
保安在門口蹲著相當於監聽事件,在AIO中就是Thread-0執行緒,做EPoll.wait(…)
-
快遞員把電視送到門口,相當於有IO事件到達。
-
保安通知C電視到了,C過來搬電視,相當於處理事件。
在AIO中就是Thread-0往任務佇列提交任務,
Thread-1 ~n去取資料,並執行回撥方法。
整個過程中,保安D必須一直蹲著,寸步不能離開,否則電視送到門口,就被人偷了。
好友C也必須在A家待著,受人委託,東西到了,人卻不在現場,這有點失信於人。
所以實際的非同步和理想中的非同步,在互不依賴,互不干擾,這兩點相違背了。保安的作用最大,這是他人生的高光時刻。
非同步過程中的註冊事件、監聽事件、處理事件,還有開啟多執行緒,這些過程的發起者全是使用者態一手操辦,所以說Java AIO只在使用者態實現了非同步,這個和BIO、NIO先阻塞,阻塞喚醒後開啟非同步執行緒處理的本質一致。
2、Java AIO跟NIO一樣,在各個平臺的底層實現方式也不同,在Linux是用EPoll,Windows是IOCP,Mac OS是KQueue。原理是大同小異,都是需要一個使用者執行緒阻塞等待IO事件,一個執行緒池從佇列裡處理事件。
3、 Netty之所以移除掉AIO,很大的原因是在效能上AIO並沒有比NIO高。Linux雖然也有一套原生的AIO實現(類似Windows上的IOCP),但Java AIO在Linux並沒有採用,而是用EPoll來實現。
4、 Java AIO不支援UDP
5、 AIO程式設計方式略顯複雜,比如“死亡回撥”
- MySQL MVCC實現原理
- 為什麼專案老夭折?這份專案管理指南請收好
- “伯樂”流量調控平臺工程視角 | 得物技術
- 如何評估某活動帶來的大盤增量 | 得物技術
- 得物榜單|全鏈路生產遷移及B/C端資料儲存隔離
- 透過現象看Java AIO的本質 | 得物技術
- 時效準確率提升之承運商路由網路挖掘 | 得物技術
- 存貨庫存模型升級始末 | 得物技術
- 關於加解密、加簽驗籤的那些事 | 得物技術
- GPU推理服務效能優化之路 | 得物技術
- 得物供應鏈複雜業務實時數倉建設之路
- 從 0 到 1,億級訊息推送的穩定性保障 | 得物技術
- 前端監控穩定性資料分析實踐 | 得物技術
- 得物容器SRE探索與實踐
- 得物熱點探測技術架構設計與實踐
- 今年很火的 AI 繪畫怎麼玩
- 得物社群計數系統設計與實現
- 得物商家客服桌面端Electron技術實踐
- 得物商家客服桌面端Electron技術實踐
- 得物染色環境落地實踐