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


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

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

  想達到的目的有兩個:

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

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

前提

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

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

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

  1,等待數據准備

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

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

阻塞(blocking)與非阻塞(non-blocking)IO

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

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

  想象一下下面兩種場景:

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

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

  結論:A是阻塞的,B是非阻塞的。

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

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

  先來看看正式點的定義,POSIX標准將IO模型分為了兩種:同步IO和異步IO,Richard 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操作的2個步驟:准備數據和把數據從內核中拷貝到程序進程。映射到這個例子,書即是准備的數據,小剛是內核,小明是程序進程,小剛把書給小明這是拷貝數據。在B方式中,小剛找書這段時間小明的確是沒閑着,該干嘛干嘛,但是小剛找到書把書給小明的這個過程也就是拷貝數據這個步驟,小明還是得乖乖的回來候着小剛把書遞手上。所以這里就阻塞了,根據上面的定義,所以是同步。

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

討論

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

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

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

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

兩種方式的組合

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

Java網絡IO實現和IO模型

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

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

阻塞IO

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

 1     @Test
 2     public void testJIoSocket() throws Exception
 3     {
 4         ServerSocket serverSocket = new ServerSocket(10002);
 5         Socket socket = null;
 6         try
 7         {
 8             while (true)
 9             {
10                 socket = serverSocket.accept();
11                 System.out.println("socket連接:" + socket.getRemoteSocketAddress().toString());
12                 BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
13                 while(true)
14                 {
15                     String readLine = in.readLine();
16                     System.out.println("收到消息" + readLine);
17                     if("end".equals(readLine))
18                     {
19                         break;
20                     }
21                     //客戶端斷開連接
22                     socket.sendUrgentData(0xFF);
23                 }
24             }
25         }
26         catch (SocketException se)
27         {
28             System.out.println("客戶端斷開連接");
29         }
30         catch (IOException e)
31         {
32             e.printStackTrace();
33         }
34         finally
35         {
36             System.out.println("socket關閉:" + socket.getRemoteSocketAddress().toString());
37             socket.close();
38         }
39     }

  使用SocketTest作為客戶端工具進行測試,同時開啟2個客戶端連接Server端並發送消息,如下圖:

  再看下后台的打印

socket連接:/127.0.0.1:54080 收到消息hello! 收到消息my name is client1

   由於服務器端是單線程的,在第一個連接的客戶端阻塞了線程后,第二個客戶端必須等待第一個斷開后才能連接。當輸入“end”字符串斷開客戶端1,這時候看到后台繼續打印:

socket連接:/127.0.0.1:54080 收到消息hello! 收到消息my name is client1 收到消息end socket關閉:/127.0.0.1:54080 socket連接:/127.0.0.1:54091 收到消息hello! 收到消息my name is client2

   所有的客戶端連接在請求服務端時都會阻塞住,等待前面的完成。即使是使用短連接,數據在寫入 OutputStream 或者從 InputStream 讀取時都有可能會阻塞。這在大規模的訪問量或者系統對性能有要求的時候是不能接受的。

阻塞IO + 每個請求創建線程/線程池

  通常解決這個問題的方法是使用多線程技術,一個客戶端一個處理線程,出現阻塞時只是一個線程阻塞而不會影響其它線程工作;為了減少系統線程的開銷,采用線程池的辦法來減少線程創建和回收的成本,模式如下圖:

  

  簡單的實現例子如下,使用一個線程(Accptor)接收客戶端請求,為每個客戶端新建線程進行處理(Processor),線程池的我就不弄了:

public class MultithreadJIoSocketTest
{
    @Test
    public void testMultithreadJIoSocket() throws Exception
    {
        ServerSocket serverSocket = new ServerSocket(10002);
        Thread thread = new Thread(new Accptor(serverSocket));
        thread.start();
        
        Scanner scanner = new Scanner(System.in);
        scanner.next();
    }
    
    public class Accptor implements Runnable
    {
        private ServerSocket serverSocket;
        
        public Accptor(ServerSocket serverSocket)
        {
            this.serverSocket = serverSocket;
        }

        public void run()
        {
            while (true)
            {
                Socket socket = null;
                try
                {
                    socket = serverSocket.accept();
                    if(socket != null)
                    {
                        System.out.println("收到了socket:" + socket.getRemoteSocketAddress().toString());
                        Thread thread = new Thread(new Processor(socket));
                        thread.start();
                    }
                }
                catch (IOException e)
                {
                    e.printStackTrace();
                }
            }
        }
    }
    
    public class Processor implements Runnable
    {
        private Socket socket;
        
        public Processor(Socket socket)
        {
            this.socket = socket;
        }
        
        @Override
        public void run()
        {
            try
            {
                BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                String readLine;
                while(true)
                {
                    readLine = in.readLine();
                    System.out.println("收到消息" + readLine);
                    if("end".equals(readLine))
                    {
                        break;
                    }
                    //客戶端斷開連接
                    socket.sendUrgentData(0xFF);
                    Thread.sleep(5000);
                }
            }
            catch (InterruptedException e)
            {
               e.printStackTrace();
            }
            catch (SocketException se)
            {
                System.out.println("客戶端斷開連接");
            }
            catch (IOException e)
            {
                e.printStackTrace();
            }
            finally {
                try
                {
                    socket.close();
                }
                catch (IOException e)
                {
                    e.printStackTrace();
                }
            }
        }
        
    }
}

   使用2個客戶端連接,這次沒有阻塞,成功的收到了2個客戶端的消息。

收到了socket:/127.0.0.1:55707 收到了socket:/127.0.0.1:55708 收到消息hello! 收到消息hello!

   在單個線程處理中,我人為的使單個線程read后阻塞5秒,就像前面說的,出現阻塞也只是在單個線程中,沒有影響到另一個客戶端的處理。

  這種阻塞IO的解決方案在大部分情況下是適用的,在出現NIO之前是最通常的解決方案,Tomcat里阻塞IO的實現就是這種方式。但是如果是大量的長連接請求呢?不可能創建幾百萬個線程保持連接。再退一步,就算線程數不是問題,如果這些線程都需要訪問服務端的某些競爭資源,勢必需要進行同步操作,這本身就是得不償失的。

非阻塞IO + IO multiplexing

  Java從1.4開始提供了NIO工具包,這是一種不同於傳統流IO的新的IO方式,使得Java開始對非阻塞IO支持;NIO並不等同於非阻塞IO,只要設置Blocking屬性就可以控制阻塞非阻塞。至於NIO的工作方式特點原理這里一概不說,以后會寫。模式如下圖:

  

  下面是簡單的實現:

public class NioNonBlockingSelectorTest
{
    Selector selector;
    private ByteBuffer receivebuffer = ByteBuffer.allocate(1024);
    
    @Test
    public void testNioNonBlockingSelector()
        throws Exception
    {
        selector = Selector.open();
        SocketAddress address = new InetSocketAddress(10002);
        ServerSocketChannel channel = ServerSocketChannel.open();
        channel.socket().bind(address);
        channel.configureBlocking(false);
        channel.register(selector, SelectionKey.OP_ACCEPT);
        
        while(true)
        {
            selector.select();
            Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
            while (iterator.hasNext()) {          
                SelectionKey selectionKey = iterator.next();  
                iterator.remove();  
                handleKey(selectionKey);  
            }  
        }
    }
    
    private void handleKey(SelectionKey selectionKey) throws IOException
    {
        ServerSocketChannel server = null;
        SocketChannel client = null;
        if(selectionKey.isAcceptable())
        {
            server = (ServerSocketChannel)selectionKey.channel();
            client = server.accept();
            System.out.println("客戶端: " + client.socket().getRemoteSocketAddress().toString());
            client.configureBlocking(false);
            client.register(selector, SelectionKey.OP_READ);
        }
        if(selectionKey.isReadable())
        {
            client = (SocketChannel)selectionKey.channel();
            receivebuffer.clear();  
            int count = client.read(receivebuffer);   
            if (count > 0) {  
                String receiveText = new String( receivebuffer.array(),0,count);  
                System.out.println("服務器端接受客戶端數據--:" + receiveText);  
                client.register(selector, SelectionKey.OP_READ);  
            }
        }
    }
    
}

  Java NIO提供的非阻塞IO並不是單純的非阻塞IO模式,而是建立在Reactor模式上的IO復用模型;在IO multiplexing Model中,對於每一個socket,一般都設置成為non-blocking,但是整個用戶進程其實是一直被阻塞的。只不過進程是被select這個函數阻塞,而不是被socket IO給阻塞,所以還是屬於非阻塞的IO。

  這篇文章中把這種模式歸為了異步阻塞,我其實是認為這是同步非阻塞的,可能看的角度不一樣。

異步IO

  Java1.7中提供了異步IO的支持,暫時還沒有看過,所以以后再討論。

網絡IO優化

  對於網絡IO有一些基本的處理規則如下:

  1。減少交互的次數。比如增加緩存,合並請求。

  2。減少傳輸數據大小。比如壓縮后傳輸、約定合理的數據協議。

  3。減少編碼。比如提前將字符轉化為字節再傳輸。

  4。根據應用場景選擇合適的交互方式,同步阻塞,同步非阻塞,異步阻塞,異步非阻塞。

就說到這里吧,感覺有點亂,有些地方還是找不到更貼切的語言來描述。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM