最近接了一個新需求,業務場景上需要在原有基礎上新增2個欄位,介面新增參數意味著很多類和方法的邏輯都需要改變,需要先判斷是否屬於該業務場景,再做對應的邏輯。原本的打算是在入口處新增變數,在操作數據的時候進行邏輯判斷將變數進行存儲或查詢。 ...
1、引言關於Java網路編程中的同步IO和非同步IO的區別及原理的文章非常的多,具體來說主要還是在討論Java BIO和Java NIO這兩者,而關於Java AIO的文章就少之又少了(即使用也只是介紹了一下概念和代碼示例)。 在深入瞭解AIO之前,我註意到以下幾個現象:
Java AIO的這些不合常理的現象難免會令人心存疑惑。所以決定寫這篇文章時,我不想只是簡單的把AIO的概念再覆述一遍,而是要透過現象,深入分析、思考和並理解Java AIO的本質。 2、我們所理解的非同步
public void create() {
//TODO
}
public void build() {
executor.execute(() -> build());
}
不管是用@Async註解,還是往線程池裡提交任務,他們最終都是同一個結果,就是把要執行的任務,交給另外一個線程來執行。這個時候,我們可以大致的認為,所謂的“非同步”,就是用多線程的方式去並行執行任務。 3、Java BIO和NIO到底是同步還是非同步?Java BIO和NIO到底是同步還是非同步,我們先按照非同步這個思路,做非同步編程。 3.1BIO代碼示例
InputStream in = socket.getInputStream();
in.read(data);
// 接收到數據,非同步處理
executor.execute(() -> handle(data));
public void handle( byte [] data) {
// TODO
}
如上:BIO在read()時,雖然線程阻塞了,但在收到數據時,可以非同步啟動一個線程去處理。3.2NIO代碼示例
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讀寫的線程,如果是同一個線程,就稱之為同步,否則是非同步。 按上述定義:
按照這個思路,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 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平臺上運行,其他平臺略有差異)。如下圖所示。 分析線程棧,發現,程式啟動了那麼幾個線程:
6、 AIO示例引發思考2:AIO註冊事件監聽和執行回調是如何實現的?
註:註冊事件調用EPoll.ctl(...)函數,這個函數在最後的參數用於指定是一次性的,還是永久性。上面代碼events | EPOLLONSHOT字面意思看來,是一次性的。 監聽事件:
處理事件:
核心流程總結: 在分析完上面的代碼流程後會發現:每一次IO讀寫都要經歷的這三個事件是一次性的,也就是在處理事件完,本次流程就結束了,如果想繼續下一次的IO讀寫,就得從頭開始再來一遍。這樣就會存在所謂的死亡回調(回調方法里再添加下一個回調方法),這對於編程的複雜度大大提高了。 7、 AIO示例引發思考3:監聽回調的本質是什麼?
7.1概述
7.2系統調用與函數調用
7.3用戶態和內核態之間的通信
7.4用實際例子驗證結論
定位到具體的代碼上:可以看到“AWT-XAWT”正在做while迴圈,調用waitForEvents函數等待事件返回。如果沒有事件,線程就一直阻塞在那邊。如下圖所示。
8、Java AIO的本質是什麼?
8.1Java AIO的本質,就是只在用戶態實現了非同步
8.2Java AIO的其它真相
|