自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

透過現(xiàn)象看Java AIO的本質(zhì)

開發(fā) 前端
2011年Java 7發(fā)布,里面增加了AIO稱之為異步IO的編程模型,但已經(jīng)過去了近12年,平時使用的開發(fā)框架中間件,還是以NIO為主,例如網(wǎng)絡(luò)框架Netty、Mina,Web容器Tomcat、Undertow。

1、前言

關(guān)于Java BIO、NIO、AIO的區(qū)別和原理,這樣的文章非常的多的,但主要還是在BIO和NIO這兩者之間討論,而關(guān)于AIO這樣的文章就少之又少了,很多只是介紹了一下概念和代碼示例。

在了解AIO時,有注意到以下幾個現(xiàn)象:

  •  2011年Java 7發(fā)布,里面增加了AIO稱之為異步IO的編程模型,但已經(jīng)過去了近12年,平時使用的開發(fā)框架中間件,還是以NIO為主,例如網(wǎng)絡(luò)框架Netty、Mina,Web容器Tomcat、Undertow。
  • Java AIO又稱為NIO 2.0,難道它也是基于NIO來實現(xiàn)的?
  • Netty舍去了AIO的支持。https://github.com/netty/netty/issues/2515
  • AIO看起來只是解決了有無,發(fā)布了個寂寞。

這幾個現(xiàn)象不免會令很多人心存疑惑,所以決定寫這篇文章時,不想簡單的把AIO的概念再復(fù)述一遍,而是要透過現(xiàn)象, 如何分析、思考和理解Java AIO的本質(zhì)。

2、什么是異步

2.1 我們所了解的異步

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

說起異步編程,在平時的開發(fā)還是比較常見,例如以下的代碼示例:

@Asyncpublic void create() {    //TODO}
public void build() { executor.execute(() -> build());}
@Async
public void create() {
//TODO
}


public void build() {
executor.execute(() -> build());
}

不管是用@Async注解,還是往線程池里提交任務(wù),他們最終都是同一個結(jié)果,就是把要執(zhí)行的任務(wù),交給另外一個線程來執(zhí)行。這個時候,可以大致的認(rèn)為,所謂的“異步”,就是多線程,執(zhí)行任務(wù)。

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

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

2.2.1 BIO示例byte [] data = new byte[1024];

byte [] data = new byte[1024];
InputStream in = socket.getInputStream();
in.read(data);
// 接收到數(shù)據(jù),異步處理
executor.execute(() -> handle(data));

public void handle(byte [] data) {
// TODO
}

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()可以阻塞等待數(shù)據(jù),在有數(shù)據(jù)可讀的時候,異步啟動一個線程,去讀取數(shù)據(jù)和處理數(shù)據(jù)。

2.2.3 產(chǎn)生理解的偏差

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

但果真如此么,在翻閱了大量博客文章之后,基本一致的闡明了,BIO和NIO是同步的。

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

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

Java IO也是一樣,需要有個參考系,才能定義它是同步異步,既然我們討論的是IO是哪一種模式,那就是要針對IO讀寫操作這件事來理解,而其他的啟動另外一個線程去處理數(shù)據(jù),已經(jīng)是脫離IO讀寫的范圍了,不應(yīng)該把他們扯進(jìn)來。

2.2.4 嘗試定義異步

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

顯然BIO只能是同步,調(diào)用in.read()當(dāng)前線程阻塞,有數(shù)據(jù)返回的時候,接收到數(shù)據(jù)的還是原來的線程。

而NIO也稱之為同步,原因也是如此,調(diào)用channel.read()時,線程雖然不會阻塞,但讀到數(shù)據(jù)的還是當(dāng)前線程。

按照這個思路,AIO應(yīng)該是發(fā)起IO讀寫的線程,和實際收到數(shù)據(jù)的線程,可能不是同一個線程

是不是這樣呢,現(xiàn)在開始上Java AIO的代碼。

2.3 Java AIO的程序示例

2.3.1 AIO服務(wù)端程序

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 異步的定義猜想結(jié)論

分別運行服務(wù)端和客戶端程序

圖片

在服務(wù)端運行結(jié)果里,

main線程發(fā)起serverChannel.accept的調(diào)用,添加了一個CompletionHandler監(jiān)聽回調(diào),當(dāng)有客戶端連接過來時,Thread-5線程執(zhí)行了accep的completed回調(diào)方法。

緊接著Thread-5又發(fā)起了clientChannel.read調(diào)用,也添加了個CompletionHandler監(jiān)聽回調(diào),當(dāng)收到數(shù)據(jù)時,是Thread-1的執(zhí)行了read的completed回調(diào)方法。

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

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

3、AIO示例引發(fā)思考的問題

  •  執(zhí)行completed()方法的這個線程是誰創(chuàng)建的,什么時候創(chuàng)建的?
  • AIO注冊事件監(jiān)聽和執(zhí)行回調(diào)是如何實現(xiàn)的?
  •  監(jiān)聽回調(diào)的本質(zhì)是什么?

3.1 問題1:執(zhí)行completed()方法的這個線程是誰創(chuàng)建的,什么時候創(chuàng)建的

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

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

圖片

分析線程棧,發(fā)現(xiàn),程序啟動了那么幾個線程

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

此時可以暫定下一個結(jié)論:

AIO服務(wù)端程序啟動之后,就開始創(chuàng)建了這些線程,且線程都處于阻塞等待狀態(tài)。

另外,發(fā)現(xiàn)這些線程的運行都跟Epoll有關(guān)系,提到Epoll,我們印象中,Java NIO在Linux平臺底層就是用Epoll來實現(xiàn)的,難道Java AIO也是用Epoll來實現(xiàn)么?為了證實這個結(jié)論,我們從下一個問題來展開討論

3.2 問題2:AIO注冊事件監(jiān)聽和執(zhí)行回調(diào)是如何實現(xiàn)的

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

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

以注冊監(jiān)聽read為例clientChannel.read(...),它主要的核心流程是:

1、注冊事件 -> 2、監(jiān)聽事件 -> 3、處理事件

3.2.1 1、注冊事件

圖片

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

3.2.2 2、監(jiān)聽事件

圖片

3.2.3 3、處理事件

圖片

圖片

圖片

3.2.4 核心流程總結(jié)

圖片

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

3.3 問題3: 監(jiān)聽回調(diào)的本質(zhì)是什么?

先說一下結(jié)論,所謂監(jiān)聽回調(diào)的本質(zhì),就是用戶態(tài)線程,調(diào)用內(nèi)核態(tài)的函數(shù)(準(zhǔn)確的說是API,例如read,write,epollWait),該函數(shù)還沒有返回時,用戶線程被阻塞了。當(dāng)函數(shù)返回時,會喚醒阻塞的線程,執(zhí)行所謂回調(diào)函數(shù)。

對于這個結(jié)論的理解,要先引入幾個概念

3.3.1 系統(tǒng)調(diào)用與函數(shù)調(diào)用

函數(shù)調(diào)用:

找到某個函數(shù),并執(zhí)行函數(shù)里的相關(guān)命令

系統(tǒng)調(diào)用:

操作系統(tǒng)對用戶應(yīng)用程序提供了編程接口,所謂API。

系統(tǒng)調(diào)用執(zhí)行過程:

1.傳遞系統(tǒng)調(diào)用參數(shù)

2.執(zhí)行陷入指令,用用戶態(tài)切換到核心態(tài),這是因為系統(tǒng)調(diào)用一般都需要再核心態(tài)下執(zhí)行

3.執(zhí)行系統(tǒng)調(diào)用程序

4.返回用戶態(tài)

3.3.2 用戶態(tài)和內(nèi)核態(tài)之間的通信

用戶態(tài)->內(nèi)核態(tài),通過系統(tǒng)調(diào)用方式即可。

內(nèi)核態(tài)->用戶態(tài),內(nèi)核態(tài)根本不知道用戶態(tài)程序有什么函數(shù),參數(shù)是啥,地址在哪里。所以內(nèi)核是不可能去調(diào)用用戶態(tài)的函數(shù),只能通過發(fā)送信號,比如kill 命令關(guān)閉程序就是通過發(fā)信號讓用戶程序優(yōu)雅退出的。

既然內(nèi)核態(tài)是不可能主動去調(diào)用用戶態(tài)的函數(shù),為什么還會有回調(diào)呢,只能說這個所謂回調(diào)其實就是用戶態(tài)的自導(dǎo)自演。它既做了監(jiān)聽,又做了執(zhí)行回調(diào)函數(shù)。

3.3.3 用實際例子驗證結(jié)論

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

按照慣例,先打印一下線程棧,會發(fā)現(xiàn)鼠標(biāo)、鍵盤等事件的監(jiān)聽是由"AWT-XAWT"線程負(fù)責(zé)的,處理事件則是"AWT-EventQueue"線程負(fù)責(zé)。

圖片

定位到具體的代碼上,可以看到"AWT-XAWT"正在做while循環(huán),調(diào)用waitForEvents函數(shù)等待事件返回。如果沒有事件,線程就一直阻塞在那邊。

圖片

4、Java AIO的本質(zhì)是什么?

  • 由于內(nèi)核態(tài)無法直接調(diào)用用戶態(tài)函數(shù),Java AIO的本質(zhì),就是只在用戶態(tài)實現(xiàn)異步。并沒有達(dá)到理想意義上的異步。

理想中的異步

何謂理想意義上的異步?這里舉個網(wǎng)購的例子

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

A在網(wǎng)上購物時,填好家庭地址付款提交訂單,這個相當(dāng)于注冊監(jiān)聽事件

商家發(fā)貨,B把東西送到A家門口,這個相當(dāng)于回調(diào)。

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

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

現(xiàn)實中的異步

A住的是高檔小區(qū),不能隨意進(jìn)去,快遞只能送到小區(qū)門口。

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

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

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

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

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

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

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

  • Java AIO跟NIO一樣,在各個平臺的底層實現(xiàn)方式也不同,在Linux是用EPoll,Windows是IOCP,Mac OS是KQueue。原理是大同小異,都是需要一個用戶線程阻塞等待IO事件,一個線程池從隊列里處理事件。
  •  Netty之所以移除掉AIO,很大的原因是在性能上AIO并沒有比NIO高。Linux雖然也有一套原生的AIO實現(xiàn)(類似Windows上的IOCP),但Java AIO在Linux并沒有采用,而是用EPoll來實現(xiàn)。
  •  Java AIO不支持UDP
  • AIO編程方式略顯復(fù)雜,比如“死亡回調(diào)”
責(zé)任編輯:武曉燕 來源: 得物技術(shù)
相關(guān)推薦

2013-02-26 09:34:10

服務(wù)器虛擬化云部署

2013-02-27 16:03:48

云部署服務(wù)器虛擬化

2015-12-10 15:10:10

2025-02-20 08:10:00

神經(jīng)網(wǎng)絡(luò)模型AI

2009-12-08 15:37:39

2010-09-13 10:51:47

無線路由器的門道

2009-12-23 15:43:52

架構(gòu)師

2021-02-02 15:23:20

Cloudera大數(shù)據(jù)

2012-05-01 21:32:39

蘋果

2013-09-26 11:05:24

云計算虛擬化

2011-09-11 17:22:03

筆記本評測

2015-06-11 10:27:14

集群管理Docker

2021-08-02 13:05:49

瀏覽器HTTP前端

2013-09-25 09:26:03

平臺軟件企業(yè)虛擬化云網(wǎng)絡(luò)

2010-08-26 14:40:55

隱私保護(hù)

2021-08-26 11:21:34

技術(shù)代碼計算

2009-06-30 19:08:36

云計算SaaS服務(wù)器

2013-10-08 11:16:55

谷歌云計算

2009-12-04 10:20:53

2009-10-12 19:39:43

Windows 7多點觸摸
點贊
收藏

51CTO技術(shù)棧公眾號