BIO,NIO,AIO 總結(jié)

Java 中的 BIO、NIO和 AIO 理解為是 Java 語(yǔ)言對(duì)操作系統(tǒng)的各種 IO 模型的封裝。程序員在使用這些 API 的時(shí)候,不需要關(guān)心操作系統(tǒng)層面的知識(shí),也不需要根據(jù)不同操作系統(tǒng)編寫不同的代碼。只需要使用Java的API就可以了。

在講 BIO,NIO,AIO 之前先來(lái)回顧一下這樣幾個(gè)概念:同步與異步,阻塞與非阻塞。

關(guān)于同步和異步的概念解讀困擾著很多程序員,大部分的解讀都會(huì)帶有自己的一點(diǎn)偏見。參考了 Stackoverflow相關(guān)問題后對(duì)原有答案進(jìn)行了進(jìn)一步完善:

When you execute something synchronously, you wait for it to finish before moving on to another task. When you execute something asynchronously, you can move on to another task before it finishes.

當(dāng)你同步執(zhí)行某項(xiàng)任務(wù)時(shí),你需要等待其完成才能繼續(xù)執(zhí)行其他任務(wù)。當(dāng)你異步執(zhí)行某些操作時(shí),你可以在完成另一個(gè)任務(wù)之前繼續(xù)進(jìn)行。

  • 同步 :兩個(gè)同步任務(wù)相互依賴,并且一個(gè)任務(wù)必須以依賴于另一任務(wù)的某種方式執(zhí)行。 比如在A->B事件模型中,你需要先完成 A 才能執(zhí)行B。 再換句話說,同步調(diào)用種被調(diào)用者未處理完請(qǐng)求之前,調(diào)用不返回,調(diào)用者會(huì)一直等待結(jié)果的返回。
  • 異步: 兩個(gè)異步的任務(wù)完全獨(dú)立的,一方的執(zhí)行不需要等待另外一方的執(zhí)行。再換句話說,異步調(diào)用種一調(diào)用就返回結(jié)果不需要等待結(jié)果返回,當(dāng)結(jié)果返回的時(shí)候通過回調(diào)函數(shù)或者其他方式拿著結(jié)果再做相關(guān)事情,

阻塞和非阻塞

  • 阻塞: 阻塞就是發(fā)起一個(gè)請(qǐng)求,調(diào)用者一直等待請(qǐng)求結(jié)果返回,也就是當(dāng)前線程會(huì)被掛起,無(wú)法從事其他任務(wù),只有當(dāng)條件就緒才能繼續(xù)。
  • 非阻塞: 非阻塞就是發(fā)起一個(gè)請(qǐng)求,調(diào)用者不用一直等著結(jié)果返回,可以先去干其他事情。

如何區(qū)分 “同步/異步 ”和 “阻塞/非阻塞” 呢?

同步/異步是從行為角度描述事物的,而阻塞和非阻塞描述的當(dāng)前事物的狀態(tài)(等待調(diào)用結(jié)果時(shí)的狀態(tài))。

1. BIO (Blocking I/O)

同步阻塞I/O模式,數(shù)據(jù)的讀取寫入必須阻塞在一個(gè)線程內(nèi)等待其完成。

1.1 傳統(tǒng) BIO

BIO通信(一請(qǐng)求一應(yīng)答)模型圖如下(圖源網(wǎng)絡(luò),原出處不明):

[圖片上傳失敗...(image-6a125e-1611913037326)]

采用 BIO 通信模型 的服務(wù)端,通常由一個(gè)獨(dú)立的 Acceptor 線程負(fù)責(zé)監(jiān)聽客戶端的連接。我們一般通過在while(true) 循環(huán)中服務(wù)端會(huì)調(diào)用 accept() 方法等待接收客戶端的連接的方式監(jiān)聽請(qǐng)求,請(qǐng)求一旦接收到一個(gè)連接請(qǐng)求,就可以建立通信套接字在這個(gè)通信套接字上進(jìn)行讀寫操作,此時(shí)不能再接收其他客戶端連接請(qǐng)求,只能等待同當(dāng)前連接的客戶端的操作執(zhí)行完成, 不過可以通過多線程來(lái)支持多個(gè)客戶端的連接,如上圖所示。

如果要讓 BIO 通信模型 能夠同時(shí)處理多個(gè)客戶端請(qǐng)求,就必須使用多線程(主要原因是socket.accept()、socket.read()socket.write() 涉及的三個(gè)主要函數(shù)都是同步阻塞的),也就是說它在接收到客戶端連接請(qǐng)求之后為每個(gè)客戶端創(chuàng)建一個(gè)新的線程進(jìn)行鏈路處理,處理完成之后,通過輸出流返回應(yīng)答給客戶端,線程銷毀。這就是典型的 一請(qǐng)求一應(yīng)答通信模型 。我們可以設(shè)想一下如果這個(gè)連接不做任何事情的話就會(huì)造成不必要的線程開銷,不過可以通過 線程池機(jī)制 改善,線程池還可以讓線程的創(chuàng)建和回收成本相對(duì)較低。使用FixedThreadPool 可以有效的控制了線程的最大數(shù)量,保證了系統(tǒng)有限的資源的控制,實(shí)現(xiàn)了N(客戶端請(qǐng)求數(shù)量):M(處理客戶端請(qǐng)求的線程數(shù)量)的偽異步I/O模型(N 可以遠(yuǎn)遠(yuǎn)大于 M),下面一節(jié)"偽異步 BIO"中會(huì)詳細(xì)介紹到。

我們?cè)僭O(shè)想一下當(dāng)客戶端并發(fā)訪問量增加后這種模型會(huì)出現(xiàn)什么問題?

在 Java 虛擬機(jī)中,線程是寶貴的資源,線程的創(chuàng)建和銷毀成本很高,除此之外,線程的切換成本也是很高的。尤其在 Linux 這樣的操作系統(tǒng)中,線程本質(zhì)上就是一個(gè)進(jìn)程,創(chuàng)建和銷毀線程都是重量級(jí)的系統(tǒng)函數(shù)。如果并發(fā)訪問量增加會(huì)導(dǎo)致線程數(shù)急劇膨脹可能會(huì)導(dǎo)致線程堆棧溢出、創(chuàng)建新線程失敗等問題,最終導(dǎo)致進(jìn)程宕機(jī)或者僵死,不能對(duì)外提供服務(wù)。

1.2 偽異步 IO

為了解決同步阻塞I/O面臨的一個(gè)鏈路需要一個(gè)線程處理的問題,后來(lái)有人對(duì)它的線程模型進(jìn)行了優(yōu)化一一一后端通過一個(gè)線程池來(lái)處理多個(gè)客戶端的請(qǐng)求接入,形成客戶端個(gè)數(shù)M:線程池最大線程數(shù)N的比例關(guān)系,其中M可以遠(yuǎn)遠(yuǎn)大于N.通過線程池可以靈活地調(diào)配線程資源,設(shè)置線程的最大值,防止由于海量并發(fā)接入導(dǎo)致線程耗盡。

偽異步IO模型圖(圖源網(wǎng)絡(luò),原出處不明):

[圖片上傳失敗...(image-d752e8-1611913037325)]

采用線程池和任務(wù)隊(duì)列可以實(shí)現(xiàn)一種叫做偽異步的 I/O 通信框架,它的模型圖如上圖所示。當(dāng)有新的客戶端接入時(shí),將客戶端的 Socket 封裝成一個(gè)Task(該任務(wù)實(shí)現(xiàn)java.lang.Runnable接口)投遞到后端的線程池中進(jìn)行處理,JDK 的線程池維護(hù)一個(gè)消息隊(duì)列和 N 個(gè)活躍線程,對(duì)消息隊(duì)列中的任務(wù)進(jìn)行處理。由于線程池可以設(shè)置消息隊(duì)列的大小和最大線程數(shù),因此,它的資源占用是可控的,無(wú)論多少個(gè)客戶端并發(fā)訪問,都不會(huì)導(dǎo)致資源的耗盡和宕機(jī)。

偽異步I/O通信框架采用了線程池實(shí)現(xiàn),因此避免了為每個(gè)請(qǐng)求都創(chuàng)建一個(gè)獨(dú)立線程造成的線程資源耗盡問題。不過因?yàn)樗牡讓尤匀皇峭阶枞腂IO模型,因此無(wú)法從根本上解決問題。

1.3 代碼示例

下面代碼中演示了BIO通信(一請(qǐng)求一應(yīng)答)模型。我們會(huì)在客戶端創(chuàng)建多個(gè)線程依次連接服務(wù)端并向其發(fā)送"當(dāng)前時(shí)間+:hello world",服務(wù)端會(huì)為每個(gè)客戶端線程創(chuàng)建一個(gè)線程來(lái)處理。代碼示例出自閃電俠的博客,原地址如下:

http://m.itdecent.cn/p/a4e03835921a

客戶端

/**
 * 
 * @author 閃電俠
 * @date 2018年10月14日
 * @Description:客戶端
 */
public class IOClient {

  public static void main(String[] args) {
    // TODO 創(chuàng)建多個(gè)線程,模擬多個(gè)客戶端連接服務(wù)端
    new Thread(() -> {
      try {
        Socket socket = new Socket("127.0.0.1", 3333);
        while (true) {
          try {
            socket.getOutputStream().write((new Date() + ": hello world").getBytes());
            Thread.sleep(2000);
          } catch (Exception e) {
          }
        }
      } catch (IOException e) {
      }
    }).start();

  }

}

服務(wù)端

/**
 * @author 閃電俠
 * @date 2018年10月14日
 * @Description: 服務(wù)端
 */
public class IOServer {

  public static void main(String[] args) throws IOException {
    // TODO 服務(wù)端處理客戶端連接請(qǐng)求
    ServerSocket serverSocket = new ServerSocket(3333);

    // 接收到客戶端連接請(qǐng)求之后為每個(gè)客戶端創(chuàng)建一個(gè)新的線程進(jìn)行鏈路處理
    new Thread(() -> {
      while (true) {
        try {
          // 阻塞方法獲取新的連接
          Socket socket = serverSocket.accept();

          // 每一個(gè)新的連接都創(chuàng)建一個(gè)線程,負(fù)責(zé)讀取數(shù)據(jù)
          new Thread(() -> {
            try {
              int len;
              byte[] data = new byte[1024];
              InputStream inputStream = socket.getInputStream();
              // 按字節(jié)流方式讀取數(shù)據(jù)
              while ((len = inputStream.read(data)) != -1) {
                System.out.println(new String(data, 0, len));
              }
            } catch (IOException e) {
            }
          }).start();

        } catch (IOException e) {
        }

      }
    }).start();

  }

}

1.4 總結(jié)

在活動(dòng)連接數(shù)不是特別高(小于單機(jī)1000)的情況下,這種模型是比較不錯(cuò)的,可以讓每一個(gè)連接專注于自己的 I/O 并且編程模型簡(jiǎn)單,也不用過多考慮系統(tǒng)的過載、限流等問題。線程池本身就是一個(gè)天然的漏斗,可以緩沖一些系統(tǒng)處理不了的連接或請(qǐng)求。但是,當(dāng)面對(duì)十萬(wàn)甚至百萬(wàn)級(jí)連接的時(shí)候,傳統(tǒng)的 BIO 模型是無(wú)能為力的。因此,我們需要一種更高效的 I/O 處理模型來(lái)應(yīng)對(duì)更高的并發(fā)量。

2. NIO (New I/O)

2.1 NIO 簡(jiǎn)介

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

NIO中的N可以理解為Non-blocking,不單純是New。它支持面向緩沖的,基于通道的I/O操作方法。 NIO提供了與傳統(tǒng)BIO模型中的 SocketServerSocket 相對(duì)應(yīng)的 SocketChannelServerSocketChannel 兩種不同的套接字通道實(shí)現(xiàn),兩種通道都支持阻塞和非阻塞兩種模式。阻塞模式使用就像傳統(tǒng)中的支持一樣,比較簡(jiǎn)單,但是性能和可靠性都不好;非阻塞模式正好與之相反。對(duì)于低負(fù)載、低并發(fā)的應(yīng)用程序,可以使用同步阻塞I/O來(lái)提升開發(fā)速率和更好的維護(hù)性;對(duì)于高負(fù)載、高并發(fā)的(網(wǎng)絡(luò))應(yīng)用,應(yīng)使用 NIO 的非阻塞模式來(lái)開發(fā)。

2.2 NIO的特性/NIO與IO區(qū)別

如果是在面試中回答這個(gè)問題,我覺得首先肯定要從 NIO 流是非阻塞 IO 而 IO 流是阻塞 IO 說起。然后,可以從 NIO 的3個(gè)核心組件/特性為 NIO 帶來(lái)的一些改進(jìn)來(lái)分析。如果,你把這些都回答上了我覺得你對(duì)于 NIO 就有了更為深入一點(diǎn)的認(rèn)識(shí),面試官問到你這個(gè)問題,你也能很輕松的回答上來(lái)了。

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

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

Java NIO使我們可以進(jìn)行非阻塞IO操作。比如說,單線程中從通道讀取數(shù)據(jù)到buffer,同時(shí)可以繼續(xù)做別的事情,當(dāng)數(shù)據(jù)讀取到buffer中后,線程再繼續(xù)處理數(shù)據(jù)。寫數(shù)據(jù)也是一樣的。另外,非阻塞寫也是如此。一個(gè)線程請(qǐng)求寫入一些數(shù)據(jù)到某通道,但不需要等待它完全寫入,這個(gè)線程同時(shí)可以去做別的事情。

Java IO的各種流是阻塞的。這意味著,當(dāng)一個(gè)線程調(diào)用 read()write() 時(shí),該線程被阻塞,直到有一些數(shù)據(jù)被讀取,或數(shù)據(jù)完全寫入。該線程在此期間不能再干任何事情了

2)Buffer(緩沖區(qū))

IO 面向流(Stream oriented),而 NIO 面向緩沖區(qū)(Buffer oriented)。

Buffer是一個(gè)對(duì)象,它包含一些要寫入或者要讀出的數(shù)據(jù)。在NIO類庫(kù)中加入Buffer對(duì)象,體現(xiàn)了新庫(kù)與原I/O的一個(gè)重要區(qū)別。在面向流的I/O中·可以將數(shù)據(jù)直接寫入或者將數(shù)據(jù)直接讀到 Stream 對(duì)象中。雖然 Stream 中也有 Buffer 開頭的擴(kuò)展類,但只是流的包裝類,還是從流讀到緩沖區(qū),而 NIO 卻是直接讀到 Buffer 中進(jìn)行操作。

在NIO厙中,所有數(shù)據(jù)都是用緩沖區(qū)處理的。在讀取數(shù)據(jù)時(shí),它是直接讀到緩沖區(qū)中的; 在寫入數(shù)據(jù)時(shí),寫入到緩沖區(qū)中。任何時(shí)候訪問NIO中的數(shù)據(jù),都是通過緩沖區(qū)進(jìn)行操作。

最常用的緩沖區(qū)是 ByteBuffer,一個(gè) ByteBuffer 提供了一組功能用于操作 byte 數(shù)組。除了ByteBuffer,還有其他的一些緩沖區(qū),事實(shí)上,每一種Java基本類型(除了Boolean類型)都對(duì)應(yīng)有一種緩沖區(qū)。

3)Channel (通道)

NIO 通過Channel(通道) 進(jìn)行讀寫。

通道是雙向的,可讀也可寫,而流的讀寫是單向的。無(wú)論讀寫,通道只能和Buffer交互。因?yàn)?Buffer,通道可以異步地讀寫。

4)Selector (選擇器)

NIO有選擇器,而IO沒有。

選擇器用于使用單個(gè)線程處理多個(gè)通道。因此,它需要較少的線程來(lái)處理這些通道。線程之間的切換對(duì)于操作系統(tǒng)來(lái)說是昂貴的。 因此,為了提高系統(tǒng)效率選擇器是有用的。

[圖片上傳失敗...(image-cb65d4-1611913166435)]

2.3 NIO 讀數(shù)據(jù)和寫數(shù)據(jù)方式

通常來(lái)說NIO中的所有IO都是從 Channel(通道) 開始的。

  • 從通道進(jìn)行數(shù)據(jù)讀取 :創(chuàng)建一個(gè)緩沖區(qū),然后請(qǐng)求通道讀取數(shù)據(jù)。
  • 從通道進(jìn)行數(shù)據(jù)寫入 :創(chuàng)建一個(gè)緩沖區(qū),填充數(shù)據(jù),并要求通道寫入數(shù)據(jù)。

數(shù)據(jù)讀取和寫入操作圖示:

[圖片上傳失敗...(image-68d295-1611913037325)]

2.4 NIO核心組件簡(jiǎn)單介紹

NIO 包含下面幾個(gè)核心的組件:

  • Channel(通道)
  • Buffer(緩沖區(qū))
  • Selector(選擇器)

整個(gè)NIO體系包含的類遠(yuǎn)遠(yuǎn)不止這三個(gè),只能說這三個(gè)是NIO體系的“核心API”。我們上面已經(jīng)對(duì)這三個(gè)概念進(jìn)行了基本的闡述,這里就不多做解釋了。

2.5 代碼示例

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

http://m.itdecent.cn/p/a4e03835921a

客戶端 IOClient.java 的代碼不變,我們對(duì)服務(wù)端使用 NIO 進(jìn)行改造。以下代碼較多而且邏輯比較復(fù)雜,大家看看就好。

/**
 * 
 * @author 閃電俠
 * @date 2019年2月21日
 * @Description: NIO 改造后的服務(wù)端
 */
public class NIOServer {
  public static void main(String[] args) throws IOException {
    // 1\. serverSelector負(fù)責(zé)輪詢是否有新的連接,服務(wù)端監(jiān)測(cè)到新的連接之后,不再創(chuàng)建一個(gè)新的線程,
    // 而是直接將新連接綁定到clientSelector上,這樣就不用 IO 模型中 1w 個(gè) while 循環(huán)在死等
    Selector serverSelector = Selector.open();
    // 2\. clientSelector負(fù)責(zé)輪詢連接是否有數(shù)據(jù)可讀
    Selector clientSelector = Selector.open();

    new Thread(() -> {
      try {
        // 對(duì)應(yīng)IO編程中服務(wù)端啟動(dòng)
        ServerSocketChannel listenerChannel = ServerSocketChannel.open();
        listenerChannel.socket().bind(new InetSocketAddress(3333));
        listenerChannel.configureBlocking(false);
        listenerChannel.register(serverSelector, SelectionKey.OP_ACCEPT);

        while (true) {
          // 監(jiān)測(cè)是否有新的連接,這里的1指的是阻塞的時(shí)間為 1ms
          if (serverSelector.select(1) > 0) {
            Set<SelectionKey> set = serverSelector.selectedKeys();
            Iterator<SelectionKey> keyIterator = set.iterator();

            while (keyIterator.hasNext()) {
              SelectionKey key = keyIterator.next();

              if (key.isAcceptable()) {
                try {
                  // (1) 每來(lái)一個(gè)新連接,不需要?jiǎng)?chuàng)建一個(gè)線程,而是直接注冊(cè)到clientSelector
                  SocketChannel clientChannel = ((ServerSocketChannel) key.channel()).accept();
                  clientChannel.configureBlocking(false);
                  clientChannel.register(clientSelector, SelectionKey.OP_READ);
                } finally {
                  keyIterator.remove();
                }
              }

            }
          }
        }
      } catch (IOException ignored) {
      }
    }).start();
    new Thread(() -> {
      try {
        while (true) {
          // (2) 批量輪詢是否有哪些連接有數(shù)據(jù)可讀,這里的1指的是阻塞的時(shí)間為 1ms
          if (clientSelector.select(1) > 0) {
            Set<SelectionKey> set = clientSelector.selectedKeys();
            Iterator<SelectionKey> keyIterator = set.iterator();

            while (keyIterator.hasNext()) {
              SelectionKey key = keyIterator.next();

              if (key.isReadable()) {
                try {
                  SocketChannel clientChannel = (SocketChannel) key.channel();
                  ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                  // (3) 面向 Buffer
                  clientChannel.read(byteBuffer);
                  byteBuffer.flip();
                  System.out.println(
                      Charset.defaultCharset().newDecoder().decode(byteBuffer).toString());
                } finally {
                  keyIterator.remove();
                  key.interestOps(SelectionKey.OP_READ);
                }
              }

            }
          }
        }
      } catch (IOException ignored) {
      }
    }).start();

  }
}

為什么大家都不愿意用 JDK 原生 NIO 進(jìn)行開發(fā)呢?從上面的代碼中大家都可以看出來(lái),是真的難用!除了編程復(fù)雜、編程模型難之外,它還有以下讓人詬病的問題:

  • JDK 的 NIO 底層由 epoll 實(shí)現(xiàn),該實(shí)現(xiàn)飽受詬病的空輪詢 bug 會(huì)導(dǎo)致 cpu 飆升 100%
  • 項(xiàng)目龐大之后,自行實(shí)現(xiàn)的 NIO 很容易出現(xiàn)各類 bug,維護(hù)成本較高,上面這一坨代碼我都不能保證沒有 bug

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

3. AIO (Asynchronous I/O)

AIO 也就是 NIO 2。在 Java 7 中引入了 NIO 的改進(jìn)版 NIO 2,它是異步非阻塞的IO模型。異步 IO 是基于事件和回調(diào)機(jī)制實(shí)現(xiàn)的,也就是應(yīng)用操作之后會(huì)直接返回,不會(huì)堵塞在那里,當(dāng)后臺(tái)處理完成,操作系統(tǒng)會(huì)通知相應(yīng)的線程進(jìn)行后續(xù)的操作。

AIO 是異步IO的縮寫,雖然 NIO 在網(wǎng)絡(luò)操作中,提供了非阻塞的方法,但是 NIO 的 IO 行為還是同步的。對(duì)于 NIO 來(lái)說,我們的業(yè)務(wù)線程是在 IO 操作準(zhǔn)備好時(shí),得到通知,接著就由這個(gè)線程自行進(jìn)行 IO 操作,IO操作本身是同步的。(除了 AIO 其他的 IO 類型都是同步的,這一點(diǎn)可以從底層IO線程模型解釋,推薦一篇文章:《漫話:如何給女朋友解釋什么是Linux的五種IO模型?》

查閱網(wǎng)上相關(guān)資料,我發(fā)現(xiàn)就目前來(lái)說 AIO 的應(yīng)用還不是很廣泛,Netty 之前也嘗試使用過 AIO,不過又放棄了。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容