[TOC]html
這篇文章主要是總結本身對於網絡編程中異步,同步,阻塞和非阻塞的理解,這個問題自從學習NIO以來一直困擾着我,,其實想來好久就想寫了,只不過當時理解不夠,無從下手。最近在學習VertX框架,又去熟悉了下Netty的代碼,由於了對於多線程也有了更深的理解,因此纔開始對於這些概念有了理解,用於理清思路,本文須要有良好的多線程和網絡編程基礎,不適合初學者。node
關於這四個概念在IO方面的理解我貼兩個連接,他們已經有了很好的說明我就再也不講述:c++
之前在學習c++中muduo只是記得陳碩說的epoll是一個同步非阻塞的模型,可是網上不少人說Reactor模型是一個異步阻塞的模型,在學習Netty的時候官網是這麼介紹的:web
Netty is an asynchronous event-driven network application framework for rapid development of maintainable high performance protocol servers & clients.sql
Netty是一個異步的高性能網絡框架,那麼究竟是誰說錯了?數據庫
其實你們都沒有錯誤,只是角度不一樣。 先說說什麼IO是異步的?異步實際上是針對數據從內核拷貝到用戶進程空間這個操做是誰完成的,同步IO很是好理解,當用戶進程發起一個read操做的時候發生一次系統調用,而後內核檢查有沒有數據,若是有則複製數據到進程空間,用戶進程繼續執行。而異步IO中複製數據到進程空間這個操做是內核幫你完成的,等完成以後再來通知你,執行你的邏輯。Reactor模型中,EventLoop線程在select到有可讀數據以後,而後在本身去讀取數據,因此從這個角度來說Reactor模型確實是同步的,在Linux的五種IO模型中只有異步IO是異步的。apache
那麼爲何Netty說他是一個異步網絡庫呢,這實際上是另外一個角度的闡述,對於網絡庫的做者來講,他們面向的是Linux提供的這些api,因此說多路複用的Reactor是同步的沒問題。那麼對於Netty的使用者來講,咱們面向的是Netty,Netty進一步封裝了IO操做,在咱們發起IO操做的時候它返回了一個Future,咱們能夠提供一個監聽器來傳入咱們的回調,當IO操做完成時會執行咱們的邏輯,咱們的這個操做相對於Netty就是異步的。編程
因此Reactor是同步非阻塞的,Netty是異步非阻塞的。segmentfault
Java中的Future是異步的嗎?api
對於這個問題,我想相信不少同窗都會認爲是異步的,這裏我認爲是同步的,下面談談個人理解。
先想一想一個異步操做須要哪些元素,我認爲須要發起者,執行者,執行邏輯,回調邏輯。流程: 發起者請求執行者去執行所需邏輯,而後在成功以後調用回調邏輯。Future中缺了什麼?沒錯,就是那個回調!
咱們使用Future的模式通常是:投遞一個任務到線程池得有個Future,而後去執行其餘能夠並行的操做,操做完以後去調用Future的get方法獲取結果或者isDone判斷是否執行完畢。這裏的Future只是對於計算結果的一個建模,咱們在後面須要使用的時候再去輪詢(輪詢也是同步非阻塞的一個標誌)或者阻塞,他提供的了一個很是好的特性:非阻塞!因此我認爲Future是一個同步非阻塞的實現。也正是由於Future沒有實現異步的特性,在jdk1.8以後新增了CompletableFuture提供了異步的特性。
注意異步元素的發起者和執行者能夠是同一個線程,最多見的例子就是NodeJs的單線程模型。拿Netty的線程來具體,你在EventLoop中發起一個寫請求後獲得一個Future,你能夠設置回調,下次執行這個回調的仍是EventLoop線程
這裏主要說說在使用異步編程的一點理解,由於平時仍是用爲主,咱們做爲框架的使用者有必要了解一些常見的使用範式。就我目前接觸的最多仍是CompletableFuture,Netty和VertX,當時也寫過一點Js,Js主要也是回調的用法。我知道的用法以下:
由於異步的高性能,不少時候咱們本身也想把一個操做封裝成異步的,就須要明白到底什麼是異步,明白異步須要的元素,你會發現若是不借助之後的異步組件將一個操做封裝成異步很是的困難,因此最簡單的方案就是將你的回調最終傳遞到已有異步的組件中。
舉2個簡單的例子:
CompletableFuture.supplyAsync(Object::new).thenAccept(o -> System.out.println(o));
這一行很是簡單的代碼實現了一個異步,Object::new
會被投遞到線程池中,而後執行完成後執行打印語句。框架層面的理解有助於咱們在寫代碼中不會用錯。有沒有想過一個異步操做框架給你作了什麼?
當你發起一個操做的時候,框架會去執行你的邏輯,在執行完畢時(成功或異常)去修改狀態並執行你的回調。修改狀態並執行你的回調這個操做在JDK中放在了CompletableFuture中,在Netty中則單獨採用了Promise接口,其實二者的實現是很是相似的(方法名都取的差很少)。以Netty舉例分爲Future和Promise兩個方法,做爲用戶咱們更應該關心Future的接口,Promise是框架層面須要實現的,咱們在本身去實現的時候值得咱們去學習裏面的思想。
不過我認爲咱們直接使用Promise的這種接口的機會不多,Netty和VertX場景下仍是有機會用到,在用到Promise接口的時候應該考慮下是否合理,檢查下是否是在同一個線程中,是否是能夠簡單的接口代替。給一個簡單的錯誤示例:
這裏說下Promise,咱們知道Js中也有一個Promise,千萬不要當成相似的東西,二者毫無干系,Netty的Promise是對完成操做的行爲的建模,Js的Promise是爲了組合各個異步的調用。
import io.VertX.core.Future;
public class AuctionHandler {
public Future<Void> handle() {
// 請求級別變量
Context context = new Context();
context.future.tryComplete();
return context.future;
}
public static class Context {
Future<Void> future = Future.future();
}
public static void main(String[] args) {
// 注意這裏的handle方法返回的Future是VertX的。
// 這裏的方法都是在同一個線程中執行的,徹底沒有異步化,因此能夠改爲傳遞一個普通的接口便可
new AuctionHandler().handle().setHandler(event1 -> System.out.println("handler exec!"));
}
}
複製代碼
雖然這個的代碼錯誤看上去很低級,可是在開發VertX應用時須要時刻保持警戒。另外還有一點須要說明:當返回給你的Future已是完成狀態時,如上面的代碼示例,你再增長回調,這個回調還會被執行,Netty和CompletableFuture在添加回調的時候都是檢查狀態是否完成,完成的話直接投遞到相應線程執行。
爲何要使用異步,相信不少同窗都知道是爲了高性能,那麼異步爲何高性能?
這裏先談談NodeJs和Java,對於NodeJs,不少人據說性能十分高,"秒殺"Java。我當時一直沒法理解,爲何Js能超過Java, 首先Node是單線程的,雖然能夠藉助第三方庫來實現多線程,另外Jvm做爲業界最優秀的虛擬機,那麼Node究竟是靠了什麼超過了Java?這裏的關鍵就在於Node的Io模型採用了Reactor模型,能夠處理大量的鏈接。Java中的Web開發是以Servlet爲主導,採用了同步阻塞模型,雖然用線程池實現n個鏈接用m和線程作優化,可是當有大量鏈接時,線程數量過多致使的線程調度成本會很高,另外在線程處理Io的時候也是同步阻塞,若是對方返回很難會致使當前線程一直沒法釋放,因此Tomcat這種不適合處理大量鏈接的場景。
咱們知道Jetty的底層實現就是Reactor模型,Tomcat在8以後默認也用了Reactor是否是會大幅提升性能?不幸的是,雖然能夠提升一些性能可是仍是沒法和Node一較高低,他解決的是Http鏈接那一塊的阻塞問題,可是因爲Servlet的編程模型,大量的同步阻塞操做仍是沒法避免,好比你在一個請求中去訪問了數據庫,這個線程就會一直被佔用,必定程度上你能夠經過增長線程來緩解可是線程過多又會增長調度的成本,可能會致使虛擬機假死。因此若是你的處理中有這種耗時操做,那他就是你的瓶頸,你的qps的上限就很低。在高併發場景下,Servlet的瓶頸會十分突出,只能經過大量的堆機器來水平擴展,可是沒有很好的榨乾服務器的性能。
因此咱們須要的是編程模型的改變,像Nodejs那樣在同步阻塞的地方進行異步非阻塞或者異步阻塞化。Spring5.0中的 WebFlux給了一個對應的解決方案,提供了響應式編程的模型用以取代Servlet,他對常見同步阻塞的地方進行了重寫,如Redis和Mysql等常見的IO。很早以前VertX(早期名字Node.X,Java版的Nodejs)框架也提供了這樣的編程模型,對不少同步阻塞的地方進行了重寫,這個框架十分輕量級,社區活躍度很是高,使用起來很是方便。這兩個底層都是Netty,不得不說Netty實在是太強大了。也從另一個角度說明設計的重要性,語言反而是其次。NodeJs,WebFlux和VertX都採用了相似的Reactor模型,高性能服務器領域這個模型幾乎已是最佳實踐,理解這個模型就和多線程同樣重要。我以爲拿Servlet和NodeJs來作性能的對比,是十分不公平的。NodeJs在Java領悟的對手應該是VertX這種框架,關於高性能Web框架的對比,techempower這個網站已經給出了詳細的排名,排名前十的大部分是Jvm語言,Nodejs在五十名以後了,因此不要在拿Servlet去和NodeJs作對比了,Servlet這種模型在高併發領悟必定會被逐漸取代。因此要深刻理解響應式編程,擁抱響應式編程,現有的代碼以及將來的開發均可以用響應式編程來作優化。
那麼異步到底解決了什麼問題?
上面舉的例子只是簡單說明了現有的異步非阻塞框架的性能優點。可是這個問題我也沒法給出準確的解釋,只是談談我本身的理解:
程序運行過程當中始終是圍繞着兩個主題:IO、CPU。CPU和IO的速度差距十分大,異步和Reactor模型都是爲了平衡這個差距,讓CPU能充分利用起來,不要由於IO和其餘同步操做致使線程Hang住,始終處於可運行的狀態,可使用少許的線程充分利用CPU。
不少人可能會疑問就算了把這些弄的明明白白到底有什麼用?其實若是你很好的掌握了Reactor的編程模型,不少問題就能想明白了下面談下本身理解的有用的地方:
最後還想說一句,Netty這個框架實在是太強大了,線程模型設計十分優秀,VertX把不少異步操做委託給了底層的Netty,由於Netty實現中的EventLoop具備自然的線程隔離(一個EventLoop對應一個線程,只會被這個線程調用),不少地方免去了同步,VertX一樣繼承了這個優勢,有機會必定好好看看Netty的設計和源碼。
阻塞和同步的四種組合,對於異步阻塞仍是沒法理解,這種模式真的存在嗎?