熱點推薦:
您现在的位置: 電腦知識網 >> 編程 >> Java編程 >> Java核心技術 >> 正文

IO的阻塞與非阻塞、同步與異步以及Java網絡IO交互方式

2022-06-13   來源: Java核心技術 

  最近工作中接觸到了Java網絡編程方面的東西SocketNIOMongoDB等也看了tomcat的源碼也加強了線程方面的知識也使用了MINA這樣的框架感覺獲益良多原本技術上的薄弱環節也在慢慢提高很多想寫的東西也在慢慢規劃整理無奈最近在籌備婚禮的事情顯得有些耽擱

  想了很久決定先寫寫IO中經常被提到的概念——同步與異步阻塞與非阻塞以及在Java網絡編程中的簡單運用

  想達到的目的有兩個

  深入的理解同步與異步阻塞與非阻塞這看似爛大街的詞匯很多人已經習慣不停的說但卻說不出其中的所以然包括我

  理解各種IO模型在Java網絡IO中的運用能夠根據不同的應用場景選擇合適的交互方式了解不同的交互方式對IO性能的影響

  前提

  首先先強調上下文下面提到了同步與異步阻塞與非阻塞的概念都是在IO的場合下它們在其它場合下有著不同的含義比如操作系統中通信技術上

  然後借鑒下《Unix網絡編程卷》中的理論

  IO操作中涉及的個主要對象為程序進程系統內核以讀操作為例當一個IO讀操作發生時通常經歷兩個步驟

  等待數據准備

  將數據從系統內核拷貝到操作進程中

  例如在socket上的讀操作步驟會等到網絡數據包到達到達後會拷貝到系統內核的緩沖區步驟會將數據包從內核緩沖區拷貝到程序進程的緩沖區中

  阻塞(blocking)與非阻塞(nonblocking)IO

  IO的阻塞非阻塞主要表現在一個IO操作過程中如果有些操作很慢比如讀操作時需要准備數據那麼當前IO進程是否等待操作完成還是得知暫時不能操作後先去做別的事情?一直等待下去什麼事也不做直到完成這就是阻塞抽空做些別的事情這是非阻塞

  非阻塞IO會在發出IO請求後立即得到回應即使數據包沒有准備好也會返回一個錯誤標識使得操作進程不會阻塞在那裡操作進程會通過多次請求的方式直到數據准備好返回成功的標識

  想象一下下面兩種場景

  A 小明和小剛兩個人都很耿直內向一天小明來找小剛借書小剛啊你那本XXX借我看看 於是小剛就去找書小明就等著找了半天找到了把書給了小明

  B 小明和小剛兩個人都很活潑外向一天小明來找小剛借書嘿小剛你那本XXX借我看看 小剛說我得找一會小明就去打球去了過會又來這次書找到了把書給了小明

  結論A是阻塞的B是非阻塞的

  從CPU角度可以看出非阻塞明顯提高了CPU的利用率進程不會一直在那等待但是同樣也帶來了線程切換的增加增加的 CPU 使用時間能不能補償系統的切換成本需要好好評估

  同步(synchronous)與異步(asynchronous)IO

  先來看看正式點的定義POSIX標准將IO模型分為了兩種同步IO和異步IORichard Stevens在《Unix網絡編程卷》中也總結道

  A synchronous I/O operation causes the requesting process to be blocked until that I/O operation completes;
An asynchronous I/O operation does not cause the requesting process to be blocked;

  可以看出判斷同步和異步的標准在於一個IO操作直到完成是否導致程序進程的阻塞如果阻塞就是同步的沒有阻塞就是異步的這裡的IO操作指的是真實的IO操作也就是數據從內核拷貝到系統進程(讀)的過程

  繼續前面借書的例子異步借書是這樣的

  C 小明很懶一天小明來找小剛借書嘿小剛你那本XXX借我看看 小剛說我得找一會小明就出去打球了並且讓小剛如果找到了就把書拿給他小剛是個負責任的人找到了書送到了小明手上

  A和B的借書方式都是同步的有人要問了B不是非阻塞嘛怎麼還是同步?

  前面說了IO操作的個步驟准備數據和把數據從內核中拷貝到程序進程映射到這個例子書即是准備的數據小剛是內核小明是程序進程小剛把書給小明這是拷貝數據在B方式中小剛找書這段時間小明的確是沒閒著該干嘛干嘛但是小剛找到書把書給小明的這個過程也就是拷貝數據這個步驟小明還是得乖乖的回來候著小剛把書遞手上所以這裡就阻塞了根據上面的定義所以是同步

  在涉及到 IO 處理時通常都會遇到一個是同步還是異步的處理方式的選擇問題同步能夠保證程序的可靠性而異步可以提升程序的性能小明自己去取書不管等著不等著遲早拿到書指望小剛找到了送來萬一小剛忘了或者有急事忙別的了那書就沒了

  討論

  說實話網上關於同步與異步阻塞與非阻塞的文章多之又多大部分是拷貝的也有些寫的非常好的參考了許多也借鑒了許多也經過自己的思考

  同步與異步阻塞與非阻塞之間確實有很多相似的地方很容易混淆wiki更是把異步與非阻塞畫上了等號更多的人還是認為他們是不同的原因可能有很多每個人的知識背景不同設定的上下文也不同

  我的看法是在IO中根據上面同步異步的概念也可以看出來同步與異步往往是通過阻塞非阻塞的形式來表達的並且是通過一種中間處理機制來達到異步的效果同步與異步往往是IO操作請求者和回應者之間在IO實際操作階段的協作方式而阻塞非阻塞更確切的說是一種自身狀態當前進程或者線程的狀態

  在發出IO讀請求後阻塞IO會一直等待有數據可讀當有數據可讀時會等待數據從內核拷貝至系統進程而非阻塞IO都會立即返回至於數據怎麼處理是程序進程自己的事情無關同步和異步 

  兩種方式的組合

  組合的方式當然有四種分別是同步阻塞同步非阻塞異步阻塞異步非阻塞

  Java網絡IO實現和IO模型

  不同的操作系統上有不同的IO模型《Unix網絡編程卷》將unix上的IO模型分為blocking I/Ononblocking I/OI/O multiplexing (select and poll)signal driven I/O (SIGIO)以及asynchronous I/O (the POSIX aio_functions)具體可參考《Unix網絡編程卷章節

  在windows上IO模型也是有select WSAAsyncSelectWSAEventSelectOverlapped I/O 事件通知以及IOCP具體可參考windows五種IO模型
  Java是平台無關的語言在不同的平台上會調用底層操作系統的不同的IO實現下面就來說一下Java提供的網絡IO的工具和實現為了擴大阻塞非阻塞的直觀感受我都使用了長連接

  阻塞IO

  同步阻塞最常用的一種用法使用也是最簡單的但是 I/O 性能一般很差CPU 大部分在空閒狀態下面是一個簡單的基於TCP的同步阻塞的Socket服務端例子

       @Test
     public void testJIoSocket() throws Exception
      {
          ServerSocket serverSocket = new ServerSocket();
         Socket socket = null;
          try
         {
              while (true)
              {
                 socket = serverSocketaccept();
                 Systemoutprintln(socket連接 + socketgetRemoteSocketAddress()toString());
                 BufferedReader in = new BufferedReader(new InputStreamReader(socketgetInputStream()));
                 while(true)                 {
                     String readLine = inreadLine();
                     Systemoutprintln(收到消息 + readLine);
                     if(endequals(readLine))
                     {
                         break;                     }
                     //客戶端斷開連接                     socketsendUrgentData(xFF);
                 }
             }
         }
         catch (SocketException se)
         {             Systemoutprintln(客戶端斷開連接);
         }
         catch (IOException e)
         {
             eprintStackTrace();
         }
         finally
         {
             Systemoutprintln(socket關閉 + socketgetRemoteSocketAddress()toString());
             socketclose();
         }
     }
  使用SocketTest作為客戶端工具進行測試同時開啟個客戶端連接Server端並發送消息如下圖

  


  

    再看下後台的打印
   
    socket連接/:收到消息hello!收到消息my name is client
   
    由於服務器端是單線程的在第一個連接的客戶端阻塞了線程後第二個客戶端必須等待第一個斷開後才能連接當輸入end字符串斷開客戶端這時候看到後台繼續打印
   
    socket連接/:收到消息hello!收到消息my name is client收到消息endsocket關閉/:socket連接/:收到消息hello!收到消息my name is client
   
    所有的客戶端連接在請求服務端時都會阻塞住等待前面的完成即使是使用短連接數據在寫入 OutputStream 或者從 InputStream 讀取時都有可能會阻塞這在大規模的訪問量或者系統對性能有要求的時候是不能接受的
   
    阻塞IO + 每個請求創建線程/線程池
   
    通常解決這個問題的方法是使用多線程技術一個客戶端一個處理線程出現阻塞時只是一個線程阻塞而不會影響其它線程工作為了減少系統線程的開銷采用線程池的辦法來減少線程創建和回收的成本模式如下圖
   
    簡單的實現例子如下使用一個線程(Accptor)接收客戶端請求為每個客戶端新建線程進行處理(Processor)線程池的我就不弄了
   
    public class MultithreadJIoSocketTest{
   
    @Test
   
    public void testMultithreadJIoSocket() throws Exception
   
    {
   
    ServerSocket serverSocket = new ServerSocket(
   
    Thread thread = new Thread(new Accptor(serverSocket))
   
    threadstart()
   
    Scanner scanner = new Scanner(Systemin)
   
    scannernext()
   
    }
   
    public class Accptor implements Runnable
   
    {
   
    private ServerSocket serverSocket;
   
    public Accptor(ServerSocket serverSocket)
   
    {
   
    thisserverSocket = serverSocket;
   
    }
   
    public void run()
   
    {
   
    while (true)
   
    {
   
    Socket socket = null;
   
    try
   
    {
   
    socket = serverSocketaccept()
   
    if(socket != null)
   
    {
   
    Systemoutprintln(收到了socket: + socketgetRemoteSocketAddress()toString())
   
    Thread thread = new Thread(new Processor(socket))
   
    threadstart()
   
    }
   
    }
   
    catch (IOException e)
   
    {
   
    eprintStackTrace()
   
    }
   
    }
   
    }
   
    }
   
    public class Processor implements Runnable
   
    {
   
    private Socket socket;
   
    public Processor(Socket socket)
   
    {
   
    thissocket = socket;
   
    }
   
    @Override
   
    public void run()
   
    {
   
    try
   
    {
   
    BufferedReader in = new BufferedReader(new InputStreamReader(socketgetInputStream()))
   
    String readLine;
   
    while(true)
   
    {
   
    readLine = inreadLine()
   
    Systemoutprintln(收到消息 + readLine)
   
    if(endequals(readLine))
   
    {
   
    break;
   
    }


   
    //客戶端斷開連接
   
    socketsendUrgentData(xFF)
   
    Threadsleep(
   
    }
   
    }
   
    catch (InterruptedException e)
   
    {
   
    eprintStackTrace()
   
    }
   
    catch (SocketException se)
   
    {
   
    Systemoutprintln(客戶端斷開連接
   
    }
   
    catch (IOException e)
   
    {
   
    eprintStackTrace()
   
    }
   
    finally {
   
    try
   
    {
   
    socketclose()
   
    }
   
    catch (IOException e)
   
    {
   
    eprintStackTrace()
   
    }
   
    }
   
    }
   
    }}
   
    使用個客戶端連接這次沒有阻塞成功的收到了個客戶端的消息
   
    收到了socket:/:收到了socket:/:收到消息hello!收到消息hello!
   
    在單個線程處理中我人為的使單個線程read後阻塞就像前面說的出現阻塞也只是在單個線程中沒有影響到另一個客戶端的處理
   
    這種阻塞IO的解決方案在大部分情況下是適用的在出現NIO之前是最通常的解決方案Tomcat裡阻塞IO的實現就是這種方式但是如果是大量的長連接請求呢?不可能創建幾百萬個線程保持連接再退一步就算線程數不是問題如果這些線程都需要訪問服務端的某些競爭資源勢必需要進行同步操作這本身就是得不償失的
   
    非阻塞IO + IO multiplexing
   
    Java從開始提供了NIO工具包這是一種不同於傳統流IO的新的IO方式使得Java開始對非阻塞IO支持NIO並不等同於非阻塞IO只要設置Blocking屬性就可以控制阻塞非阻塞至於NIO的工作方式特點原理這裡一概不說以後會寫模式如下圖
   
    下面是簡單的實現
   
    public class NioNonBlockingSelectorTest{
   
    Selector selector;
   
    private ByteBuffer receivebuffer = ByteBufferallocate(
   
    @Test
   
    public void testNioNonBlockingSelector()
   
    throws Exception
   
    {
   
    selector = Selectoropen()
   
    SocketAddress address = new InetSocketAddress(
   
    ServerSocketChannel channel = ServerSocketChannelopen()
   
    channelsocket()bind(address)
   
    nfigureBlocking(false)
   
    channelregister(selector SelectionKeyOP_ACCEPT)
   
    while(true)
   
    {
   
    selectorselect()
   
    Iterator iterator = selectorselectedKeys()iterator()
   
    while (iteratorhasNext()) {
   
    SelectionKey selectionKey = iteratornext()
   
    iteratorremove()
   
    handleKey(selectionKey)
   
    }
   
    }
   
    }
   
    private void handleKey(SelectionKey selectionKey) throws IOException
   
    {
   
    ServerSocketChannel server = null;
   
    SocketChannel client = null;
   
    if(selectionKeyisAcceptable())
   
    {
   
    server = (ServerSocketChannel)selectionKeychannel()
   
    client = serveraccept()
   
    Systemoutprintln(客戶端 + clientsocket()getRemoteSocketAddress()toString())
   
    nfigureBlocking(false)
   
    clientregister(selector SelectionKeyOP_READ)
   
    }
   
    if(selectionKeyisReadable())
   
    {
   
    client = (SocketChannel)selectionKeychannel()
   
    receivebufferclear()
   
    int count = clientread(receivebuffer)
   
    if (count > ) {
   
    String receiveText = new String( receivebufferarray()count)
   
    Systemoutprintln(服務器端接受客戶端數據: + receiveText)
   
    clientregister(selector SelectionKeyOP_READ)
   
    }
   
    }
   
    }
   
    }
   
    Java NIO提供的非阻塞IO並不是單純的非阻塞IO模式而是建立在Reactor模式上的IO復用模型在IO multiplexing Model中對於每一個socket一般都設置成為nonblocking但是整個用戶進程其實是一直被阻塞的只不過進程是被select這個函數阻塞而不是被socket IO給阻塞所以還是屬於非阻塞的IO
   
    這篇文章中把這種模式歸為了異步阻塞我其實是認為這是同步非阻塞的可能看的角度不一樣
   
    異步IO
   
    Java中提供了異步IO的支持暫時還沒有看過所以以後再討論
   
    網絡IO優化
   
    對於網絡IO有一些基本的處理規則如下
   
    減少交互的次數比如增加緩存合並請求
   
    減少傳輸數據大小比如壓縮後傳輸約定合理的數據協議
   
    減少編碼比如提前將字符轉化為字節再傳輸
   
    根據應用場景選擇合適的交互方式同步阻塞同步非阻塞異步阻塞異步非阻塞
   
    就說到這裡吧感覺有點亂有些地方還是找不到更貼切的語言來描述


From:http://tw.wingwit.com/Article/program/Java/hx/201311/26001.html
    推薦文章
    Copyright © 2005-2022 電腦知識網 Computer Knowledge   All rights reserved.