BIO,NIO,AIO 總結

熟練掌握 BIO,NIO,AIO 的基本概念以及一些常見問題是你準備面試的過程當中不可或缺的一部分,另外這些知識點也是你學習 Netty 的基礎。java

目錄:程序員

  • 1. BIO (Blocking I/O)面試

    • 1.1 傳統 BIO編程

    • 1.2 僞異步 IO後端

    • 1.3 代碼示例數組

    • 1.4 總結網絡

  • 2. NIO (New I/O)多線程

    • 2.1 NIO 簡介併發

    • 2.2 NIO的特性/NIO與IO區別app

      • 1)Non-blocking IO(非阻塞IO)

      • 2)Buffer(緩衝區)

      • 3)Channel (通道)

      • 4)Selectors(選擇器)

    • 2.3 NIO 讀數據和寫數據方式

    • 2.4 NIO核心組件簡單介紹

    • 2.5 代碼示例

  • 3. AIO (Asynchronous I/O)

  • 參考

BIO,NIO,AIO 總結

Java 中的 BIO、NIO和 AIO 理解爲是 Java 語言對操做系統的各類 IO 模型的封裝。程序員在使用這些 API 的時候,不須要關心操做系統層面的知識,也不須要根據不一樣操做系統編寫不一樣的代碼。只須要使用Java的API就能夠了。

在講 BIO,NIO,AIO 以前先來回顧一下這樣幾個概念:同步與異步,阻塞與非阻塞。

同步與異步

  • 同步: 同步就是發起一個調用後,被調用者未處理完請求以前,調用不返回。

  • 異步: 異步就是發起一個調用後,馬上獲得被調用者的迴應表示已接收到請求,可是被調用者並無返回結果,此時咱們能夠處理其餘的請求,被調用者一般依靠事件,回調等機制來通知調用者其返回結果。

同步和異步的區別最大在於異步的話調用者不須要等待處理結果,被調用者會經過回調等機制來通知調用者其返回結果。

阻塞和非阻塞

  • 阻塞: 阻塞就是發起一個請求,調用者一直等待請求結果返回,也就是當前線程會被掛起,沒法從事其餘任務,只有當條件就緒才能繼續。

  • 非阻塞: 非阻塞就是發起一個請求,調用者不用一直等着結果返回,能夠先去幹其餘事情。

那麼同步阻塞、同步非阻塞和異步非阻塞又表明什麼意思呢?

舉個生活中簡單的例子,你媽媽讓你燒水,小時候你比較笨啊,在哪裏傻等着水開(同步阻塞)。等你稍微再長大一點,你知道每次燒水的空隙能夠去幹點其餘事,而後只須要時不時來看看水開了沒有(同步非阻塞)。後來,大家家用上了水開了會發出聲音的壺,這樣你就只須要聽到響聲後就知道水開了,在這期間你能夠隨便幹本身的事情,你須要去倒水了(異步非阻塞)。

1. BIO (Blocking I/O)

同步阻塞I/O模式,數據的讀取寫入必須阻塞在一個線程內等待其完成。

1.1 傳統 BIO

BIO通訊(一請求一應答)模型圖以下(圖源網絡,原出處不明):

圖片

採用 BIO 通訊模型 的服務端,一般由一個獨立的 Acceptor 線程負責監聽客戶端的鏈接。咱們通常經過在 while(true) 循環中服務端會調用 accept() 方法等待接收客戶端的鏈接的方式監聽請求,請求一旦接收到一個鏈接請求,就能夠創建通訊套接字在這個通訊套接字上進行讀寫操做,此時不能再接收其餘客戶端鏈接請求,只能等待同當前鏈接的客戶端的操做執行完成, 不過能夠經過多線程來支持多個客戶端的鏈接,如上圖所示。

若是要讓 BIO 通訊模型 可以同時處理多個客戶端請求,就必須使用多線程(主要緣由是 socket.accept()socket.read()socket.write() 涉及的三個主要函數都是同步阻塞的),也就是說它在接收到客戶端鏈接請求以後爲每一個客戶端建立一個新的線程進行鏈路處理,處理完成以後,經過輸出流返回應答給客戶端,線程銷燬。這就是典型的 一請求一應答通訊模型 。咱們能夠設想一下若是這個鏈接不作任何事情的話就會形成沒必要要的線程開銷,不過能夠經過 線程池機制 改善,線程池還可讓線程的建立和回收成本相對較低。使用FixedThreadPool 能夠有效的控制了線程的最大數量,保證了系統有限的資源的控制,實現了N(客戶端請求數量):M(處理客戶端請求的線程數量)的僞異步I/O模型(N 能夠遠遠大於 M),下面一節"僞異步 BIO"中會詳細介紹到。

咱們再設想一下當客戶端併發訪問量增長後這種模型會出現什麼問題?

在 Java 虛擬機中,線程是寶貴的資源,線程的建立和銷燬成本很高,除此以外,線程的切換成本也是很高的。尤爲在 Linux 這樣的操做系統中,線程本質上就是一個進程,建立和銷燬線程都是重量級的系統函數。若是併發訪問量增長會致使線程數急劇膨脹可能會致使線程堆棧溢出、建立新線程失敗等問題,最終致使進程宕機或者僵死,不能對外提供服務。

1.2 僞異步 IO

爲了解決同步阻塞I/O面臨的一個鏈路須要一個線程處理的問題,後來有人對它的線程模型進行了優化一一一後端經過一個線程池來處理多個客戶端的請求接入,造成客戶端個數M:線程池最大線程數N的比例關係,其中M能夠遠遠大於N.經過線程池能夠靈活地調配線程資源,設置線程的最大值,防止因爲海量併發接入致使線程耗盡。

僞異步IO模型圖(圖源網絡,原出處不明):

圖片

採用線程池和任務隊列能夠實現一種叫作僞異步的 I/O 通訊框架,它的模型圖如上圖所示。當有新的客戶端接入時,將客戶端的 Socket 封裝成一個Task(該任務實現java.lang.Runnable接口)投遞到後端的線程池中進行處理,JDK 的線程池維護一個消息隊列和 N 個活躍線程,對消息隊列中的任務進行處理。因爲線程池能夠設置消息隊列的大小和最大線程數,所以,它的資源佔用是可控的,不管多少個客戶端併發訪問,都不會致使資源的耗盡和宕機。

僞異步I/O通訊框架採用了線程池實現,所以避免了爲每一個請求都建立一個獨立線程形成的線程資源耗盡問題。不過由於它的底層任然是同步阻塞的BIO模型,所以沒法從根本上解決問題。

1.3 代碼示例

下面代碼中演示了BIO通訊(一請求一應答)模型。咱們會在客戶端建立多個線程依次鏈接服務端並向其發送"當前時間+:hello world",服務端會爲每一個客戶端線程建立一個線程來處理。代碼示例出自閃電俠的博客,原地址以下:

https://www.jianshu.com/p/a4e03835921a

客戶端

 
 
  1. /**

  2. *

  3. * @author 閃電俠

  4. * @date 2018年10月14日

  5. * @Description:客戶端

  6. */

  7. public class IOClient {


  8.    public static void main(String[] args) {

  9.        // TODO 建立多個線程,模擬多個客戶端鏈接服務端

  10.        new Thread(() -> {

  11.            try {

  12.                Socket socket = new Socket("127.0.0.1", 3333);

  13.                while (true) {

  14.                    try {

  15.                        socket.getOutputStream().write((new Date() + ": hello world").getBytes());

  16.                        Thread.sleep(2000);

  17.                    } catch (Exception e) {

  18.                    }

  19.                }

  20.            } catch (IOException e) {

  21.            }

  22.        }).start();


  23.    }


  24. }

服務端

 
 
  1. /**

  2. * @author 閃電俠

  3. * @date 2018年10月14日

  4. * @Description: 服務端

  5. */

  6. public class IOServer {


  7.    public static void main(String[] args) throws IOException {

  8.        // TODO 服務端處理客戶端鏈接請求

  9.        ServerSocket serverSocket = new ServerSocket(3333);


  10.        // 接收到客戶端鏈接請求以後爲每一個客戶端建立一個新的線程進行鏈路處理

  11.        new Thread(() -> {

  12.            while (true) {

  13.                try {

  14.                    // 阻塞方法獲取新的鏈接

  15.                    Socket socket = serverSocket.accept();


  16.                    // 每個新的鏈接都建立一個線程,負責讀取數據

  17.                    new Thread(() -> {

  18.                        try {

  19.                            int len;

  20.                            byte[] data = new byte[1024];

  21.                            InputStream inputStream = socket.getInputStream();

  22.                            // 按字節流方式讀取數據

  23.                            while ((len = inputStream.read(data)) != -1) {

  24.                                System.out.println(new String(data, 0, len));

  25.                            }

  26.                        } catch (IOException e) {

  27.                        }

  28.                    }).start();


  29.                } catch (IOException e) {

  30.                }


  31.            }

  32.        }).start();


  33.    }


  34. }

1.4 總結

在活動鏈接數不是特別高(小於單機1000)的狀況下,這種模型是比較不錯的,可讓每個鏈接專一於本身的 I/O 而且編程模型簡單,也不用過多考慮系統的過載、限流等問題。線程池自己就是一個自然的漏斗,能夠緩衝一些系統處理不了的鏈接或請求。可是,當面對十萬甚至百萬級鏈接的時候,傳統的 BIO 模型是無能爲力的。所以,咱們須要一種更高效的 I/O 處理模型來應對更高的併發量。

2. NIO (New I/O)

2.1 NIO 簡介

NIO是一種同步非阻塞的I/O模型,在Java 1.4 中引入了NIO框架,對應 java.nio 包,提供了 Channel , Selector,Buffer等抽象。

NIO中的N能夠理解爲Non-blocking,不單純是New。它支持面向緩衝的,基於通道的I/O操做方法。 NIO提供了與傳統BIO模型中的 SocketServerSocket 相對應的 SocketChannelServerSocketChannel 兩種不一樣的套接字通道實現,兩種通道都支持阻塞和非阻塞兩種模式。阻塞模式使用就像傳統中的支持同樣,比較簡單,可是性能和可靠性都很差;非阻塞模式正好與之相反。對於低負載、低併發的應用程序,可使用同步阻塞I/O來提高開發速率和更好的維護性;對於高負載、高併發的(網絡)應用,應使用 NIO 的非阻塞模式來開發。

2.2 NIO的特性/NIO與IO區別

若是是在面試中回答這個問題,我以爲首先確定要從 NIO 流是非阻塞 IO 而 IO 流是阻塞 IO 提及。而後,能夠從 NIO 的3個核心組件/特性爲 NIO 帶來的一些改進來分析。若是,你把這些都回答上了我以爲你對於 NIO 就有了更爲深刻一點的認識,面試官問到你這個問題,你也能很輕鬆的回答上來了。

1)Non-blocking IO(非阻塞IO)

IO流是阻塞的,NIO流是不阻塞的。

Java NIO使咱們能夠進行非阻塞IO操做。好比說,單線程中從通道讀取數據到buffer,同時能夠繼續作別的事情,當數據讀取到buffer中後,線程再繼續處理數據。寫數據也是同樣的。另外,非阻塞寫也是如此。一個線程請求寫入一些數據到某通道,但不須要等待它徹底寫入,這個線程同時能夠去作別的事情。

Java IO的各類流是阻塞的。這意味着,當一個線程調用 read()write() 時,該線程被阻塞,直到有一些數據被讀取,或數據徹底寫入。該線程在此期間不能再幹任何事情了

2)Buffer(緩衝區)

IO 面向流(Stream oriented),而 NIO 面向緩衝區(Buffer oriented)。

Buffer是一個對象,它包含一些要寫入或者要讀出的數據。在NIO類庫中加入Buffer對象,體現了新庫與原I/O的一個重要區別。在面向流的I/O中·能夠將數據直接寫入或者將數據直接讀到 Stream 對象中。雖然 Stream 中也有 Buffer 開頭的擴展類,但只是流的包裝類,仍是從流讀到緩衝區,而 NIO 倒是直接讀到 Buffer 中進行操做。

在NIO厙中,全部數據都是用緩衝區處理的。在讀取數據時,它是直接讀到緩衝區中的; 在寫入數據時,寫入到緩衝區中。任什麼時候候訪問NIO中的數據,都是經過緩衝區進行操做。

最經常使用的緩衝區是 ByteBuffer,一個 ByteBuffer 提供了一組功能用於操做 byte 數組。除了ByteBuffer,還有其餘的一些緩衝區,事實上,每一種Java基本類型(除了Boolean類型)都對應有一種緩衝區。

3)Channel (通道)

NIO 經過Channel(通道) 進行讀寫。

通道是雙向的,可讀也可寫,而流的讀寫是單向的。不管讀寫,通道只能和Buffer交互。由於 Buffer,通道能夠異步地讀寫。

4)Selectors(選擇器)

NIO有選擇器,而IO沒有。

選擇器用於使用單個線程處理多個通道。所以,它須要較少的線程來處理這些通道。線程之間的切換對於操做系統來講是昂貴的。 所以,爲了提升系統效率選擇器是有用的。

圖片

2.3 NIO 讀數據和寫數據方式

一般來講NIO中的全部IO都是從 Channel(通道) 開始的。

  • 從通道進行數據讀取 :建立一個緩衝區,而後請求通道讀取數據。

  • 從通道進行數據寫入 :建立一個緩衝區,填充數據,並要求通道寫入數據。

數據讀取和寫入操做圖示:

圖片

2.4 NIO核心組件簡單介紹

NIO 包含下面幾個核心的組件:

  • Channel(通道)

  • Buffer(緩衝區)

  • Selector(選擇器)

整個NIO體系包含的類遠遠不止這三個,只能說這三個是NIO體系的「核心API」。咱們上面已經對這三個概念進行了基本的闡述,這裏就很少作解釋了。

2.5 代碼示例

代碼示例出自閃電俠的博客,原地址以下:

https://www.jianshu.com/p/a4e03835921a

客戶端 IOClient.java 的代碼不變,咱們對服務端使用 NIO 進行改造。如下代碼較多並且邏輯比較複雜,你們看看就好。

 
 
  1. /**

  2. *

  3. * @author 閃電俠

  4. * @date 2019年2月21日

  5. * @Description: NIO 改造後的服務端

  6. */

  7. public class NIOServer {

  8.    public static void main(String[] args) throws IOException {

  9.        // 1. serverSelector負責輪詢是否有新的鏈接,服務端監測到新的鏈接以後,再也不建立一個新的線程,

  10.        // 而是直接將新鏈接綁定到clientSelector上,這樣就不用 IO 模型中 1w 個 while 循環在死等

  11.        Selector serverSelector = Selector.open();

  12.        // 2. clientSelector負責輪詢鏈接是否有數據可讀

  13.        Selector clientSelector = Selector.open();


  14.        new Thread(() -> {

  15.            try {

  16.                // 對應IO編程中服務端啓動

  17.                ServerSocketChannel listenerChannel = ServerSocketChannel.open();

  18.                listenerChannel.socket().bind(new InetSocketAddress(3333));

  19.                listenerChannel.configureBlocking(false);

  20.                listenerChannel.register(serverSelector, SelectionKey.OP_ACCEPT);


  21.                while (true) {

  22.                    // 監測是否有新的鏈接,這裏的1指的是阻塞的時間爲 1ms

  23.                    if (serverSelector.select(1) > 0) {

  24.                        Set<SelectionKey> set = serverSelector.selectedKeys();

  25.                        Iterator<SelectionKey> keyIterator = set.iterator();


  26.                        while (keyIterator.hasNext()) {

  27.                            SelectionKey key = keyIterator.next();


  28.                            if (key.isAcceptable()) {

  29.                                try {

  30.                                    // (1)

  31.                                    // 每來一個新鏈接,不須要建立一個線程,而是直接註冊到clientSelector

  32.                                    SocketChannel clientChannel = ((ServerSocketChannel) key.channel()).accept();

  33.                                    clientChannel.configureBlocking(false);

  34.                                    clientChannel.register(clientSelector, SelectionKey.OP_READ);

  35.                                } finally {

  36.                                    keyIterator.remove();

  37.                                }

  38.                            }


  39.                        }

  40.                    }

  41.                }

  42.            } catch (IOException ignored) {

  43.            }

  44.        }).start();

  45.        new Thread(() -> {

  46.            try {

  47.                while (true) {

  48.                    // (2) 批量輪詢是否有哪些鏈接有數據可讀,這裏的1指的是阻塞的時間爲 1ms

  49.                    if (clientSelector.select(1) > 0) {

  50.                        Set<SelectionKey> set = clientSelector.selectedKeys();

  51.                        Iterator<SelectionKey> keyIterator = set.iterator();


  52.                        while (keyIterator.hasNext()) {

  53.                            SelectionKey key = keyIterator.next();


  54.                            if (key.isReadable()) {

  55.                                try {

  56.                                    SocketChannel clientChannel = (SocketChannel) key.channel();

  57.                                    ByteBuffer byteBuffer = ByteBuffer.allocate(1024);

  58.                                    // (3) 面向 Buffer

  59.                                    clientChannel.read(byteBuffer);

  60.                                    byteBuffer.flip();

  61.                                    System.out.println(

  62.                                            Charset.defaultCharset().newDecoder().decode(byteBuffer).toString());

  63.                                } finally {

  64.                                    keyIterator.remove();

  65.                                    key.interestOps(SelectionKey.OP_READ);

  66.                                }

  67.                            }


  68.                        }

  69.                    }

  70.                }

  71.            } catch (IOException ignored) {

  72.            }

  73.        }).start();


  74.    }

  75. }

爲何你們都不肯意用 JDK 原生 NIO 進行開發呢?從上面的代碼中你們均可以看出來,是真的難用!除了編程複雜、編程模型難以外,它還有如下讓人詬病的問題:

  • JDK 的 NIO 底層由 epoll 實現,該實現飽受詬病的空輪詢 bug 會致使 cpu 飆升 100%

  • 項目龐大以後,自行實現的 NIO 很容易出現各種 bug,維護成本較高,上面這一坨代碼我都不能保證沒有 bug

Netty 的出現很大程度上改善了 JDK 原生 NIO 所存在的一些讓人難以忍受的問題。

3. AIO (Asynchronous I/O)

AIO 也就是 NIO 2。在 Java 7 中引入了 NIO 的改進版 NIO 2,它是異步非阻塞的IO模型。異步 IO 是基於事件和回調機制實現的,也就是應用操做以後會直接返回,不會堵塞在那裏,當後臺處理完成,操做系統會通知相應的線程進行後續的操做。

AIO 是異步IO的縮寫,雖然 NIO 在網絡操做中,提供了非阻塞的方法,可是 NIO 的 IO 行爲仍是同步的。對於 NIO 來講,咱們的業務線程是在 IO 操做準備好時,獲得通知,接着就由這個線程自行進行 IO 操做,IO操做自己是同步的。(除了 AIO 其餘的 IO 類型都是同步的,這一點能夠從底層IO線程模型解釋,推薦一篇文章:《漫話:如何給女友解釋什麼是Linux的五種IO模型?》

查閱網上相關資料,我發現就目前來講 AIO 的應用還不是很普遍,Netty 以前也嘗試使用過 AIO,不過又放棄了。

參考

  • 《Netty 權威指南》第二版

  • https://zhuanlan.zhihu.com/p/23488863 (美團技術團隊)

轉自公衆號: JavaGuide

相關文章
相關標籤/搜索