I/O模型與Java

原文已同步至http://liumian.win/2016/11/23/io-model-and-java/html


學習I/O模型以前,首先要明白幾個概念:java

  • 同步、異步
  • 阻塞、非阻塞

這幾個概念每每是成對出現的,咱們經常可以看到同步阻塞,異步非阻塞等描述,正由於如此咱們每每在腦海裏面是一個模糊的概念 - 「哦,他們是這個樣子啊,都差很少嘛」。編程

我剛開始接觸IO知識的時候,也存在上述的問題,分不清他們的區別。隨着學習的深刻,漸漸來到了痛點區域 - 不弄懂全身感受不舒服,非弄懂不可。安全

同步與異步 描述的是用戶線程與內核的交互方式:服務器

  • 同步是指用戶線程發起 I/O 請求後須要等待或者輪詢內核 I/O 操做完成後才能繼續執行;
  • 異步是指用戶線程發起 I/O 請求後仍繼續執行,當內核 I/O 操做完成後會通知用戶線程,或者調用用戶線程註冊的回調函數。

阻塞和非阻塞 描述的是用戶線程調用內核 I/O 操做的方式:多線程

  • 阻塞是指 I/O 操做須要完全完成後才返回到用戶空間;
  • 非阻塞是指 I/O 操做被調用後當即返回給用戶一個狀態值,無需等到 I/O 操做完全完成。

下面來看一種五種常見IO模型的對比,相信你看了這張圖片之後很快就會明白同步、異步、阻塞和非阻塞的區別。框架

五種IO模型

首先咱們得明白一次IO操做是須要兩個階段的:準備數據(內核空間) -> 數據從內核空間拷貝到用戶空間。爲何要這麼作呢?由於操做系統在內存中劃分了兩個區域:一個是內核空間,一個是用戶空間。內核空間是留給操做系統進行系統服務的,而用戶空間就是咱們的程序運行的內存空間。而操做系統爲了系統的安全是不容許咱們的程序直接操做內存空間的,因此咱們必須等待操做系統把磁盤上面的內容讀入到內核空間,而後拷貝到用戶空間才能操做。從圖片的右側也能夠清晰的發現這兩個階段。異步

這以爲這篇博客總結得很是好,他說:socket

一個 I/O 操做其實分紅了兩個步驟:發起 I/O 請求和實際的 I/O 操做。 阻塞 I/O 和非阻塞 I/O 的區別在於第一步,發起 I/O 請求是否會被阻塞,若是阻塞直到完成那麼就是傳統的阻塞 I/O ,若是不阻塞,那麼就是非阻塞 I/O 。 同步 I/O 和異步 I/O 的區別就在於第二個步驟是否阻塞,若是實際的 I/O 讀寫阻塞請求進程,那麼就是同步 I/O 。async

好了,通過上面的解釋是否是對IO相關知識理解又深入一些了呢?又或者是模糊了許多呢?都不要緊,下面開始進行詳細的IO模型分析。

  1. 阻塞IO模型(BIO) 若是IO請求沒法當即完成,那麼當前線程進入阻塞狀態。 不論是第一階段仍是第二階段,所有阻塞。

  2. 非阻塞IO 模型(Non-blinking IO) 第一階段(準備數據)不會阻塞,第二階段(拷貝數據到用戶空間)會阻塞。 由於第一階段不會阻塞,因此咱們只有不斷的輪詢數據在內核空間是否準備完成,這個過程會形成CPU空轉,浪費了寶貴的CPU時間。因此不推薦直接使用這種IO模型進行項目開發。

  3. I/O複用模型 從圖中咱們能夠看到,兩個階段都阻塞了。那麼I/O複用模型和阻塞模型有什麼區別呢? 進(線)程將一個或者多個感興趣的事件(可讀、可寫等)註冊在select方法上面,當事件處於就緒狀態時意味着數據在用戶空間已經準備好(就緒以前爲阻塞狀態),那麼該方法就會返回執行後面的代碼,而後又會阻塞在recvfrom(將數據拷貝到用戶空間)這個過程直至完成。 若是您以前用過Java中的Selector,可能很容易理解這塊知識。

  4. 信號驅動I/O模型 這塊我不是很熟,《Netty權威指南》是這樣解釋的: 首先開啓套接口信號驅動I/O功能,並經過系統調用sigaction執行一個信號處理函數(此係統調用當即返回,進程繼續工做,他是非阻塞的)。當數據準備就緒時,就爲該進程生成一個SIGIO信號,經過信號回調通知應用程序調用recvfrom來讀取數據,並通知主循環函數處理數據。

  5. 異步I/O模型(AIO) 兩個階段均不阻塞線程。工做原理爲:通知內核啓動某個IO操做,內核將數據複製到用戶空間(咱們指定的空間)後通知咱們。這個過程用戶線程不會阻塞。

說了這麼多你們是否是想問,你不是說Java中的I/O嗎?怎麼到目前爲止跟Java好像一點關係都沒有呢?嘿嘿,別急,下面咱們就聊聊Java中的I/O模型~

Java中的I/O模型 首先剛剛說的大多數I/O模型在Java中都有對應的實現。爲何是大多數呢?由於信號驅動I/O模型沒有相應的實現。直接上代碼~

  1. 阻塞I/O 咱們一般在Socket編程入門的時候會這樣寫,
/**
 * 阻塞IO
 * Created by liumian on 2016/11/23.
 */
public class BlockServer {

    public static void main(String[] args) {
        int port = 8080;
        try {
            ServerSocket server = new ServerSocket(port);
            Socket clientSocket = server.accept();
            //client do something
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

}

這就是一個阻塞IO,阻塞在ServerSocket#accept方法上面,直到有數據到達纔會執行後面的代碼。

  1. 非阻塞I/O與多路複用I/O 相對於阻塞I/O,代碼要複雜不少。關於NIO的知識,一時半會也說不完,讀者能夠下去了解一下相關知識~
/**
 * 非阻塞IO
 * Created by liumian on 2016/11/23.
 */
public class NonBlockServer {

    public static void main(String[] args) {
        int port = 8080;

        Selector selector = null;
        try {
            ServerSocketChannel channel = ServerSocketChannel.open();
            channel.socket().bind(new InetSocketAddress(port));
            //設置爲非阻塞IO
            channel.configureBlocking(false);
            //打開一個複用器
            selector = Selector.open();
            //註冊感興趣的事件
            channel.register(selector, SelectionKey.OP_ACCEPT);
        } catch (IOException e) {
            e.printStackTrace();
        }

        while (true){
            try {
                selector.select();
            } catch (IOException e) {
                e.printStackTrace();
            }
            Set<SelectionKey> keySet = selector.selectedKeys();
            Iterator<SelectionKey> iterator = keySet.iterator();
            while (iterator.hasNext()){
                SelectionKey key = iterator.next();
                if (key.isAcceptable()){
                    //do something
                }
            }
        }
    }

}

在NIO中出現了通道channel 的概念。相對於以前阻塞IO模型中的流 - 只能單向移動(讀或者寫),它至關於一根水管能夠雙向移動(既能夠寫又能夠讀或者同時進行)。

  1. 異步I/O Java在JDK7的時候引入了異步IO(NIO2.0) 代碼借鑑了這個博客 Java I/O 模型的演進,(逃
public class AsyncServer {
    public static void main(String[] args) {
        int port = 8080;
        ExecutorService executor = Executors.newCachedThreadPool();
        // create asynchronous server socket channel bound to the default group
        try (AsynchronousServerSocketChannel asynchronousServerSocketChannel = AsynchronousServerSocketChannel.open()) {
            if (asynchronousServerSocketChannel.isOpen()) {
                // set some options
                asynchronousServerSocketChannel.setOption(StandardSocketOptions.SO_RCVBUF, 4 * 1024);
                asynchronousServerSocketChannel.setOption(StandardSocketOptions.SO_REUSEADDR, true);
                // bind the server socket channel to local address
                asynchronousServerSocketChannel.bind(new InetSocketAddress(port));
                // display a waiting message while ... waiting clients
                System.out.println("Waiting for connections ...");
                while (true) {
                    Future<AsynchronousSocketChannel> asynchronousSocketChannelFuture = asynchronousServerSocketChannel
                            .accept();
                    try {
                        final AsynchronousSocketChannel asynchronousSocketChannel = asynchronousSocketChannelFuture
                                .get();
                        Callable<String> worker = new Callable<String>() {
                            @Override
                            public String call() throws Exception {
                                String host = asynchronousSocketChannel.getRemoteAddress().toString();
                                System.out.println("Incoming connection from: " + host);
                                final ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
                                // transmitting data
                                while (asynchronousSocketChannel.read(buffer).get() != -1) {
                                    buffer.flip();
                                    asynchronousSocketChannel.write(buffer).get();
                                    if (buffer.hasRemaining()) {
                                        buffer.compact();
                                    } else {
                                        buffer.clear();
                                    }
                                }
                                asynchronousSocketChannel.close();
                                System.out.println(host + " was successfully served!");
                                return host;
                            }
                        };
                        executor.submit(worker);
                    } catch (InterruptedException | ExecutionException ex) {
                        System.err.println(ex);
                        System.err.println("\n Server is shutting down ...");
                        // this will make the executor accept no new threads
                        // and finish all existing threads in the queue
                        executor.shutdown();
                        // wait until all threads are finished
                        while (!executor.isTerminated()) {
                        }
                        break;
                    }
                }
            } else {
                System.out.println("The asynchronous server-socket channel cannot be opened!");
            }
        } catch (IOException ex) {
            System.err.println(ex);
        }
    }
}
  1. 僞異步I/O 只要理解了異步I/O,那麼僞異步I/O很好理解。 異步I/O無非就是在全部的操做完成以後再來通知用戶線程進行後續操做,咱們徹底能夠經過線程來僞造這種行爲。
/**
 * 利用線程池來實現僞異步
 * Created by liumian on 2016/11/23.
 */
public class NAsyncServer {

    public static void main(String[] args) {
        int port = 8080;
        ExecutorService executor = Executors.newCachedThreadPool();
        try {
            ServerSocket server = new ServerSocket(port);
            while (true){
                Socket client = server.accept();
                executor.execute(new ClientHandler(client));
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    static class ClientHandler implements Runnable{

        private Socket socket;

        public ClientHandler(Socket socket) {
            this.socket = socket;
        }

        @Override
        public void run() {
            //do something
        }
    }

}

總結 經過NIO、AIO咱們能夠得到哪些好處?

  • 得到更好的性能。一般基於塊的傳輸要比流要更高效。
  • 避免多線程。利用多路複用IO,咱們能利用一個線程管理成千上萬的鏈接,而不用爲每個鏈接建立一個線程。
  • 提升CPU的利用率。不論是NIO仍是AIO,都可以大大減小IO阻塞時間,從而充分的利用CPU。

從JDK的發展能夠看到,從阻塞IO到非阻塞IO到異步IO,咱們能夠經過靈活的運用IO構建咱們的高性能服務器。不過從JDK發展的過程也能夠看出,每每越靈活的操做使用起來越困難,因此《Netty權威指南》做者建議直接使用成熟的NIO框架去構建咱們的服務器而不是使用原生的NIO接口,這樣能夠避免不少陷阱。

我的感受I/O這些知識不只要多用,還要去想底層是怎麼實現的。這樣有助於咱們理解爲何要這麼作~ 之前剛接觸異步IO的時候,老是有這些問題:誰幫咱們去完成了IO操做?我如何知道IO操做什麼時候完成?IO操做完成之後數據是放在哪裏的?等等問題。後面隨着學習的深刻,結合操做系統、Java IO API等知識,慢慢也對IO有了本身的理解~~

檢查了不少遍,感受寫的仍是不夠通順,咬咬牙,硬着頭皮發佈(逃


參考資料

Java NIO淺析 - 美團點評技術博客

Java I/O 模型的演進

《Netty 權威指南》

相關文章
相關標籤/搜索