Java SE基礎鞏固(六):Java IO

到如今爲止,Java IO可分爲三類:BIO、NIO、AIO。最先出現的是BIO,而後是NIO,最近的是AIO,BIO即Blocking IO,NIO有的文章說是New NIO,也有的文章說是No Blocking IO,我查了一些資料,官網說的應該是No Blocking IO,提供了Selector,Channle,SelectionKey抽象,AIO即Asynchronous IO(異步IO),提供了Fauture等異步操做。java

1 BIO

i81QZn.png

上圖是BIO的架構體系圖。能夠看到BIO主要分爲兩類IO,即字符流IO和字節流IO,字符流即把輸入輸出數據當作字符來看待,Writer和Reader是其繼承體系的最高層,字節流即把輸入輸出當作字節來看待,InputStream和OutputStream是其繼承體系的最高層。下面以文件操做爲例,其餘的實現類也很是相似。shell

順便說一下,整個BIO體系大量使用了裝飾者模式,例如BufferedInputStream包裝了InputStream,使其擁有了緩衝的能力。編程

1.1 字節流

public class Main {

    public static void main(String[] args) throws IOException {
		//寫入文件
        FileOutputStream out = new FileOutputStream("E:\\Java_project\\effective-java\\src\\top\\yeonon\\io\\bio\\test.txt");
        out.write("hello,world".getBytes("UTF-8"));
        out.flush();
        out.close();

        //讀取文件
        FileInputStream in = new FileInputStream("E:\\Java_project\\effective-java\\src\\top\\yeonon\\io\\bio\\test.txt");
        byte[] buffer = new byte[in.available()];
        in.read(buffer);
        System.out.println(new String(buffer, "UTF-8"));
        in.close();
    }
}

複製代碼

向FileOutputStream構造函數中傳入文件名來建立FileOutputStream對象,即打開了一個字節流,以後使用write方法向字節流中寫入數據,完成以後調用flush刷新緩衝區,最後記得要關閉字節流。讀取文件也是相似的,先打開一個字節流,而後從字節流中讀取數據並存入內存中(buffer數組),而後再關閉字節流。數組

由於InputStream和OutputStream都繼承了AutoCloseable接口,因此若是使用的是try-resource的語法來進行字節流的IO操做,可不須要手動顯式調用close方法了,這也是很是推薦的作法,在示例中我沒有這樣作只是爲了方便。緩存

1.2 字符流

字節流主要使用的是InputStream和OutputStream,而字符流主要使用的就是與之對應的Reader和Writer。下面來看一個示例,該示例的功能和上述示例的同樣,只不過實現手段不一樣:服務器

public class Main {

    public static void main(String[] args) throws IOException {
        Writer writer = new FileWriter("E:\\Java_project\\effective-java\\src\\top\\yeonon\\io\\bio\\test.txt");
        writer.write("hello,world\n");
        writer.write("hello,yeonon\n");
        writer.flush();
        writer.close();

        BufferedReader reader = new BufferedReader(new FileReader("E:\\Java_project\\effective-java\\src\\top\\yeonon\\io\\bio\\test.txt"));

        String line = "";
        int lineCount = 0;
        while ((line = reader.readLine()) != null) {
            System.out.println(line);
            lineCount++;
        }
        reader.close();
        System.out.println(lineCount);
    }
}
複製代碼

Writer很是簡單,沒法就是打開字符流,而後向字符流中寫入字符,而後關閉。關鍵是Reader,示例代碼中使用了BufferedReader來包裝FileReader,使得本來沒有緩衝功能的FileReader有了緩衝功能,這就是上面提到過的裝飾者模式,BufferedReader還提供了方便使用的API,例如readLine(),這個方法每次調用會讀取文件中的一行。網絡

以上就是BIO的簡單使用,源碼的話由於涉及太多的底層,因此若是對底層不是很瞭解的話會很難理解源碼。架構

2 NIO

BIO是同步阻塞的IO,而NIO是同步非阻塞的IO。NIO中有幾個比較重要的組件:Selector,SelectionKey,Channel,ByteBuffer,其中Selector就是所謂的選擇器,SelectionKey能夠簡單理解爲選擇鍵,這個鍵將Selector和Channle進行一個綁定(或者所Channle註冊到Selector上),當有數據到達Channel的時候,Selector會從阻塞狀態中恢復過來,並對該Channle進行操做,而且,咱們不能直接對Channle進行讀寫操做,只能對ByteBuffer操做。以下圖所示:框架

i8YB80.png

下面是一個Socket網絡編程的例子:異步

//服務端
public class SocketServer {

    private Selector selector;
    private final static int port = 9000;
    private final static int BUF = 10240;

    private void init() throws IOException {
        //獲取一個Selector
        selector = Selector.open();
	    //獲取一個服務端socket Channel
        ServerSocketChannel channel = ServerSocketChannel.open();
        //設置爲非阻塞模式
        channel.configureBlocking(false);
        //綁定端口
        channel.socket().bind(new InetSocketAddress(port));
	    //把channle註冊到Selector上,並表示對ACCEPT事件感興趣
        SelectionKey selectionKey = channel.register(selector, SelectionKey.OP_ACCEPT);

        while (true) {
            //該方法會阻塞,直到和其綁定的任何一個channel有數據過來
            selector.select();
            //獲取該Selector綁定的SelectionKey
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = selectionKeys.iterator();
            while (iterator.hasNext()) {
                SelectionKey key = iterator.next();
                //記得刪除,不然就無限循環了
                iterator.remove();
                //若是該事件是一個ACCEPT,那麼就執行doAccept方法,其餘的也同樣
                if (key.isAcceptable()) {
                    doAccept(key);
                } else if (key.isReadable()) {
                    doRead(key);
                } else if (key.isWritable()) {
                    doWrite(key);
                } else if (key.isConnectable()) {
                    System.out.println("鏈接成功!");
                }
            }
        }
    }

    //寫方法,注意不能直接對channle進行讀寫操做,只能對ByteBuffer進行操做
    private void doWrite(SelectionKey key) throws IOException {
        ByteBuffer buffer = ByteBuffer.allocate(BUF);
        buffer.flip();
        SocketChannel socketChannel = (SocketChannel) key.channel();
        while (buffer.hasRemaining()) {
            socketChannel.write(buffer);
        }
        buffer.compact();
    }

    //讀取消息
    private void doRead(SelectionKey key) throws IOException {
        SocketChannel socketChannel = (SocketChannel) key.channel();
        ByteBuffer buffer = ByteBuffer.allocate(BUF);
        long reads = socketChannel.read(buffer);
        while (reads > 0) {
            buffer.flip();
            byte[] data = buffer.array();
            System.out.println("讀取到消息: " + new String(data, "UTF-8"));
            buffer.clear();
            reads = socketChannel.read(buffer);
        }
        if (reads == -1) {
            socketChannel.close();
        }
    }

    //當有鏈接過來的時候,獲取鏈接過來的channle,而後註冊到Selector上,並設置成對讀消息感興趣,當客戶端有消息過來的時候,Selector就可讓其執行doRead方法,而後讀取消息並打印。
    private void doAccept(SelectionKey key) throws IOException {
        ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
        System.out.println("服務端監聽中...");
        SocketChannel socketChannel = serverChannel.accept();
        socketChannel.configureBlocking(false);
        socketChannel.register(key.selector(), SelectionKey.OP_READ);
    }
    
    public static void main(String[] args) throws IOException {
        SocketServer server = new SocketServer();
        server.init();
    }
}

//客戶端,寫得比較簡單
public class SocketClient {

    private final static int port = 9000;
    private final static int BUF = 10240;


    private void init() throws IOException {
        //獲取channel
        SocketChannel channel = SocketChannel.open();
        //鏈接到遠程服務器
        channel.connect(new InetSocketAddress(port));
        //設置非阻塞模式
        channel.configureBlocking(false);
        //往ByteBuffer裏寫消息
        ByteBuffer buffer = ByteBuffer.allocate(BUF);
        buffer.put("Hello,Server".getBytes("UTF-8"));
        buffer.flip();
        //將ByteBuffer內容寫入Channle,即發送消息
        channel.write(buffer);
        channel.close();
    }


    public static void main(String[] args) throws IOException {
        SocketClient client = new SocketClient();
            client.init();
    }
}

複製代碼

嘗試啓動一個服務端,多個客戶端,結果大體以下所示:

服務端監聽中...
讀取到消息: Hello,Server                       
服務端監聽中...
讀取到消息: Hello,Server  
複製代碼

註釋寫得挺清楚了,我這裏只是簡單使用了NIO,但實際上NIO遠遠不止這些東西,光一個ByteBuffer就能說一天,若是有機會,我會在後面Netty相關的文章中詳細說一下這幾個組件。在此就再也不多說了。

吐槽一些,純NIO寫的服務端和客戶端實在是太麻煩了,一不當心就會寫錯,仍是使用Netty相似的框架好一些啊。

3 AIO

在JDK7中新增了一些IO相關的API,這些API稱做AIO。由於其提供了一些異步操做IO的功能,但本質是其實仍是NIO,因此能夠簡單的理解爲是NIO的擴充。AIO中最重要的就是Future了,Future表示未來的意思,即這個操做可能會持續很長時間,但我不會等,而是到未來操做完成的時候,再過來通知我,這就是異步的意思。下面是兩個使用AIO的例子:

public static void main(String[] args) throws ExecutionException, InterruptedException, IOException {
        Path path = Paths.get("E:\\Java_project\\effective-java\\src\\top\\yeonon\\io\\aio\\test.txt");
        AsynchronousFileChannel channel = AsynchronousFileChannel.open(path);
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        Future<Integer> future = channel.read(buffer,0);
        Integer readNum = future.get(); //阻塞,若是不調用該方法,main方法會繼續執行
        buffer.flip();
        System.out.println(new String(buffer.array(), "UTF-8"));
        System.out.println(readNum);
    }
複製代碼

第一個例子使用AsynchronousFileChannel來異步的讀取文件內容,在代碼中,我使用了future.get()方法,該方法會阻塞當前線程,在例子中即主線程,當工做線程,即讀取文件的線程執行完畢後纔會從阻塞狀態中恢復過來,並將結果返回。以後就能夠從ByteBuffer中讀取數據了。這是使用未來時的例子,下面來看看使用回調的例子:

public class Main {

    public static void main(String[] args) throws ExecutionException, InterruptedException, IOException {
        Path path = Paths.get("E:\\Java_project\\effective-java\\src\\top\\yeonon\\io\\aio\\test.txt");
        AsynchronousFileChannel channel = AsynchronousFileChannel.open(path);
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        channel.read(buffer, 0, buffer, new CompletionHandler<Integer, ByteBuffer>() {
            @Override
            public void completed(Integer result, ByteBuffer attachment) {
                System.out.println("完成讀取");
                try {
                    System.out.println(new String(attachment.array(), "UTF-8"));
                } catch (UnsupportedEncodingException e) {
                    e.printStackTrace();
                }
            }

            @Override
            public void failed(Throwable exc, ByteBuffer attachment) {
                System.out.println("讀取失敗");
            }
        });
        System.out.println("繼續執行主線程");
        //調用完成以後不須要等待任務完成,會直接繼續執行主線程
        while (true) {
            Thread.sleep(1000);
        }
    }
}

複製代碼

輸出的結果大體以下所示,但不必定,這取決於線程調度:

繼續執行主線程
完成讀取

hello,world
hello,yeonon
複製代碼

當任務完成,即讀取文件完畢的時候,會調用completed方法,失敗會調用failed方法,這就是回調。詳細接觸過回調的朋友應該不難理解。

4 BIO、NIO、AIO的區別

  1. BIO是同步阻塞的IO,NIO是同步非阻塞IO,AIO異步非阻塞IO,這是最基本的區別。阻塞模式會致使其餘線程被IO線程阻塞,必須等待IO線程執行完畢才能繼續執行邏輯,非阻塞和異步並不等同,非阻塞模式下,通常會採用事件輪詢的方式來執行IO,即IO多路複用,雖然仍然是同步的,但執行效率比傳統的BIO要高不少,AIO則是異步IO,若是把IO工做當作一個任務的話,在當前線程中提交一個任務以後,不會有阻塞,會繼續執行當前線程的後續邏輯,在任務完成以後,當前線程會收到通知,而後再決定如何處理,這種方式的IO,CPU效率是最高的,CPU幾乎沒有發生過停頓,而時一直至於忙狀態,因此效率很是高,但編程難度會比較大。
  2. BIO面向的是流,不管是字符流仍是字節流,通俗的講,BIO在讀寫數據的時候會按照一個接一個的方式讀寫,而NIO和AIO(由於AIO其實是NIO的擴充,因此從這個方面來看,能夠把他們放在一塊)讀寫數據的時候是按照一塊一塊的讀取的,讀取到的數據會緩存在內存中,而後在內存中對數據進行處理,這種方式的好處是減小了硬盤或者網絡的讀寫次數,從而下降了因爲硬盤或網絡速度慢帶來的效率影響。
  3. BIO的API雖然比較底層,但若是熟悉以後編寫起來會比較容易,NIO或者AIO的API抽象層次高,通常來講應該更容易使用纔是,但實際上卻很難「正確」的編寫,並且DEBUG的難度也較大,這也是爲何Netty等NIO框架受歡迎的緣由之一。

以上就是我理解的BIO、NIO和AIO區別。

5 小結

本文簡單粗略的講了一下BIO、NIO、AIO的使用,並未涉及源碼,也沒有涉及太多的原理,若是讀者但願瞭解更多關於三者的內容,建議參看一些書籍,例如老外寫的《Java NIO》,該書全面系統的講解了NIO的各類組件和細節,很是推薦。

相關文章
相關標籤/搜索