Java IO 以及 NIO 詳解

上一篇:Java 隊列詳解

IO 介紹

IO 是 Input/Output 的縮寫,它是基於流模型實現的,比如操作文件時使用輸入流和輸出流來寫入和讀取文件等。

IO 分類

傳統的 IO,按照流類型我們可以分爲:

  • 字符流

  • 字節流

其中,字符流包括 Reader、Writer;字節流包括 InputStream、OutputStream。
傳統 IO 的類關係圖,如下圖所示:

enter image description here

IO 使用

瞭解了 IO 之間的關係,下面我們正式進入實戰環節,分別來看字符流(Reader、Writer)和字節流(InputStream、OutputStream)的使用。

① Writer 使用

Writer 可用來寫入文件,請參考以下代碼:

// 給指定目錄下的文件追加信息Writer writer = new FileWriter("d:\\io.txt",true);
writer.append("老王");
writer.close();

這幾行簡單的代碼就可以實現把信息 老王 追加到 d:\\io.txt 的文件下,參數二表示的是覆蓋文字還是追加文字。

② Reader 使用

Reader 可用來讀取文件,請參考以下代碼:

Reader reader = new FileReader("d:\\io.txt");
BufferedReader bufferedReader = new BufferedReader(reader);
String str = null;
// 逐行讀取信息
while (null != (str = bufferedReader.readLine())) {
    System.out.println(str);
}
bufferedReader.close();
reader.close();

③ InputStream 使用

InputStream 可用來讀取文件,請參考以下代碼:

InputStream inputStream = new FileInputStream(new File("d:\\io.txt"));
byte[] bytes = new byte[inputStream.available()];
// 讀取到 byte 數組
inputStream.read(bytes);
// 內容轉換爲字符串
String content = new String(bytes, "UTF-8");
inputStream.close();

④ OutputStream 使用

OutputStream 可用來寫入文件,請參考以下代碼:

OutputStream outputStream = new FileOutputStream(new File("d:\\io.txt"),true);
outputStream.write("老王".getBytes());
outputStream.close();

NIO 介紹

上面講的內容都是 java.io 包下的知識點,但隨着 Java 的不斷髮展,在 Java 1.4 時新的 IO 包出現了 java.nio,NIO(Non-Blocking IO)的出現解決了傳統 IO,也就是我們經常說的 BIO(Blocking IO)同步阻塞的問題,NIO 提供了 Channel、Selector 和 Buffer 等概念,可以實現多路複用和同步非阻塞 IO 操作,從而大大提升了 IO 操作的性能。
前面提到同步和阻塞的問題,那下面來看看同步和阻塞結合都有哪些含義。

組合方式 性能分析
同步阻塞 最常用的一種用法,使用也是最簡單的,但是 I/O 性能一般很差,CPU 大部分在空閒狀態
同步非阻塞 提升 I/O 性能的常用手段,就是將 I/O 的阻塞改成非阻塞方式,尤其在網絡 I/O 是長連接,同時傳輸數據也不是很多的情況下,提升性能非常有效。 這種方式通常能提升 I/O 性能,但是會增加 CPU 消耗,要考慮增加的 I/O 性能能不能補償 CPU 的消耗,也就是系統的瓶頸是在 I/O 還是在 CPU 上
異步阻塞 這種方式在分佈式數據庫中經常用到。例如,在往一個分佈式數據庫中寫一條記錄,通常會有一份是同步阻塞的記錄,而還有兩至三份是備份記錄會寫到其他機器上,這些備份記錄通常都是採用異步阻塞的方式寫 I/O;異步阻塞對網絡 I/O 能夠提升效率,尤其像上面這種同時寫多份相同數據的情況
異步非阻塞 這種組合方式用起來比較複雜,只有在一些非常複雜的分佈式情況下使用,像集羣之間的消息同步機制一般用這種 I/O 組合方式。例如,Cassandra 的 Gossip 通信機制就是採用異步非阻塞的方式。它適合同時要傳多份相同的數據到集羣中不同的機器,同時數據的傳輸量雖然不大,但是卻非常頻繁。這種網絡 I/O 用這個方式性能能達到最高

瞭解了同步和阻塞的含義,下面來看 NIO 的具體使用,請參考以下代碼:

int port = 6666;
new Thread(new Runnable() {
    @Override
    public void run() {
        try (Selector selector = Selector.open();
             ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();) {
            serverSocketChannel.bind(new InetSocketAddress(InetAddress.getLocalHost(), port));
            serverSocketChannel.configureBlocking(false);
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
            while (true) {
                selector.select(); // 阻塞等待就緒的 Channel
                Set<SelectionKey> selectionKeys = selector.selectedKeys();
                Iterator<SelectionKey> iterator = selectionKeys.iterator();
                while (iterator.hasNext()) {
                    SelectionKey key = iterator.next();
                    try (SocketChannel channel = ((ServerSocketChannel) key.channel()).accept()) {
                        channel.write(Charset.defaultCharset().encode("老王,你好~"));
                    }
                    iterator.remove();
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}).start();
 
new Thread(new Runnable() {
    @Override
    public void run() {
        // Socket 客戶端 1(接收信息並打印)
        try (Socket cSocket = new Socket(InetAddress.getLocalHost(), port)) {
            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(cSocket.getInputStream()));
            bufferedReader.lines().forEach(s -> System.out.println("客戶端 1 打印:" + s));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}).start();
 
new Thread(new Runnable() {
    @Override
    public void run() {
        // Socket 客戶端 2(接收信息並打印)
        try (Socket cSocket = new Socket(InetAddress.getLocalHost(), port)) {
            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(cSocket.getInputStream()));
            bufferedReader.lines().forEach(s -> System.out.println("客戶端 2 打印:" + s));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}).start();

以上代碼創建了兩個 Socket 客戶端,用於收取和打印服務器端的消息。
其中,服務器端通過 SelectionKey(選擇鍵)獲取到 SocketChannel(通道),而通道都註冊到 Selector(選擇器)上,所有的客戶端都可以獲得對應的通道,而不是所有客戶端都排隊堵塞等待一個服務器連接,這樣就實現多路複用的效果了。多路指的是多個通道(SocketChannel),而複用指的是一個服務器端連接重複被不同的客戶端使用。

AIO 介紹

AIO(Asynchronous IO)是 NIO 的升級,也叫 NIO2,實現了異步非堵塞 IO ,異步 IO 的操作基於事件和回調機制。
AIO 實現簡單的 Socket 服務器,代碼如下:

int port = 8888;
new Thread(new Runnable() {
    @Override
    public void run() {
        AsynchronousChannelGroup group = null;
        try {
            group = AsynchronousChannelGroup.withThreadPool(Executors.newFixedThreadPool(4));
            AsynchronousServerSocketChannel server = AsynchronousServerSocketChannel.open(group).bind(new InetSocketAddress(InetAddress.getLocalHost(), port));
            server.accept(null, new CompletionHandler<AsynchronousSocketChannel, AsynchronousServerSocketChannel>() {
                @Override
                public void completed(AsynchronousSocketChannel result, AsynchronousServerSocketChannel attachment) {
                    server.accept(null, this); // 接收下一個請求
                    try {
                        Future<Integer> f = result.write(Charset.defaultCharset().encode("Hi, 老王"));
                        f.get();
                        System.out.println("服務端發送時間:" + DateFormat.getDateTimeInstance().format(new Date()));
                        result.close();
                    } catch (InterruptedException | ExecutionException | IOException e) {
                        e.printStackTrace();
                    }
                }
                @Override
                public void failed(Throwable exc, AsynchronousServerSocketChannel attachment) {
                }
            });
            group.awaitTermination(Long.MAX_VALUE, TimeUnit.SECONDS);
        } catch (IOException | InterruptedException e) {
            e.printStackTrace();
        }
    }
}).start();
 
// Socket 客戶端
AsynchronousSocketChannel client = AsynchronousSocketChannel.open();
Future<Void> future = client.connect(new InetSocketAddress(InetAddress.getLocalHost(), port));
future.get();
ByteBuffer buffer = ByteBuffer.allocate(100);
client.read(buffer, null, new CompletionHandler<Integer, Void>() {
    @Override
    public void completed(Integer result, Void attachment) {
        System.out.println("客戶端打印:" + new String(buffer.array()));
    }
 
    @Override
    public void failed(Throwable exc, Void attachment) {
        exc.printStackTrace();
        try {
            client.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
});
Thread.sleep(10 * 1000);

相關面試題

1.使用以下哪個方法來判斷一個文件是否存在?

A:createFile
B:exists
C:read
D:exist

答:B

2.以下說法錯誤的是?

A:同步操作不一定會阻塞
B:異步操作不一定會阻塞
C:阻塞一定是同步操作
D:同步或異步都可能會阻塞

答:C

題目解析:異步操作也可能會阻塞,比如分佈式集羣消息同步,採用的就是異步阻塞的方式。

3.BIO、NIO、AIO 的區別是什麼?

答:它們三者的區別如下。

  • BIO 就是傳統的 java.io 包,它是基於流模型實現的,交互的方式是同步、阻塞方式,也就是說在讀入輸入流或者輸出流時,在讀寫動作完成之前,線程會一直阻塞在那裏,它們之間的調用是可靠的線性順序。它的優點就是代碼比較簡單、直觀;缺點就是 IO 的效率和擴展性很低,容易成爲應用性能瓶頸。

  • NIO 是 Java 1.4 引入的 java.nio 包,提供了 Channel、Selector、Buffer 等新的抽象,可以構建多路複用的、同步非阻塞 IO 程序,同時提供了更接近操作系統底層高性能的數據操作方式。

  • AIO 是 Java 1.7 之後引入的包,是 NIO 的升級版本,提供了異步非堵塞的 IO 操作方式,因此人們叫它 AIO(Asynchronous IO),異步 IO 是基於事件和回調機制實現的,也就是應用操作之後會直接返回,不會堵塞在那裏,當後臺處理完成,操作系統會通知相應的線程進行後續的操作。

簡單來說 BIO 就是傳統 IO 包,產生的最早;NIO 是對 BIO 的改進提供了多路複用的同步非阻塞 IO,而 AIO 是 NIO 的升級,提供了異步非阻塞 IO。

4.讀取和寫入文件最簡潔的方式是什麼?

答:使用 Java 7 提供的 Files 讀取和寫入文件是最簡潔,請參考以下代碼:

// 讀取文件
byte[] bytes = Files.readAllBytes(Paths.get("d:\\io.txt"));
// 寫入文件
Files.write(Paths.get("d:\\io.txt"), "追加內容".getBytes(), StandardOpenOption.APPEND);

讀取和寫入都是一行代碼搞定,可以說很簡潔了。

5.Files 常用方法都有哪些?

答:Files 是 Java 1.7 提供的,使得文件和文件夾的操作更加方便,它的常用方法有以下幾個:

  • Files. exists():檢測文件路徑是否存在

  • Files. createFile():創建文件

  • Files. createDirectory():創建文件夾

  • Files. delete():刪除一個文件或目錄

  • Files. copy():複製文件

  • Files. move():移動文件

  • Files. size():查看文件個數

  • Files. read():讀取文件

  • Files. write():寫入文件

6.FileInputStream 可以實現什麼功能?

答:FileInputStream 可以實現文件的讀取。

題目解析:因爲 FileInputStream 和 FileOutputStream 很容易被記反,FileOutputStream 纔是用來寫入文件的,所以也經常被面試官問到。

7.不定項選擇:爲了提高讀寫性能,可以採用什麼流?

A:InputStream
B:DataInputStream
C:BufferedReader
D:BufferedInputStream
E:OutputStream
F:BufferedOutputStream

答:D、F

題目解析:BufferedInputStream 是一種帶緩存區的輸入流,在讀取字節數據時可以從底層流中一次性讀取多個字節到緩存區,而不必每次都調用系統底層;同理,BufferedOutputStream 也是一種帶緩衝區的輸出流,通過緩衝區輸出流,應用程序先把字節寫入緩衝區,緩存區滿後再調用操作系統底層,從而提高系統性能,而不必每次都去調用系統底層方法。

8.FileInputStream 和 BufferedInputStream 的區別是什麼?

答:FileInputStream 在小文件讀寫時性能較好,而在大文件操作時使用 BufferedInputStream 更有優勢。

9.以下這段代碼運行在 Windwos 平臺,執行的結果是?

Files.createFile(Paths.get("c:\\pf.txt"), PosixFilePermissions.asFileAttribute(
    EnumSet.of(PosixFilePermission.OWNER_READ)));

A:在指定的盤符產生了對應的文件,文件只讀
B:在指定的盤符產生了對應的文件,文件只寫
C:在指定的盤符產生了對應的文件,文件可讀寫
D:程序報錯

答:D

題目解析:本題目考察的是 Files.createFile 參數傳遞的問題,PosixFilePermissions 不支持 Windows,因此在 Windows 執行會報錯 java.lang.UnsupportedOperationException: 'posix:permissions' not supported as initial attribute。

總結

在 Java 1.4 之前只有 BIO(Blocking IO)可供使用,也就是 java.io 包下的那些類,它的缺點是同步阻塞式運行的。隨後在 Java 1.4 時,提供了 NIO(Non-Blocking IO)屬於 BIO 的升級,提供了同步非阻塞的 IO 操作方式,它的重要組件是 Selector(選擇器)、Channel(通道)、Buffer(高效數據容器)實現了多路複用的高效 IO 操作。而 AIO(Asynchronous IO)也叫 NIO 2.0,屬於 NIO 的補充和升級,提供了異步非阻塞的 IO 操作。

還有另一個重要的知識點,是 Java 7.0 時新增的 Files 類,極大地提升了文件操作的便利性,比如讀、寫文件 Files.write()、Files.readAllBytes() 等,都是非常簡便和實用的方法。

下一篇:Java 反射和動態代理

在公衆號菜單中可自行獲取專屬架構視頻資料,包括不限於 java架構、python系列、人工智能系列、架構系列,以及最新面試、小程序、大前端均無私奉獻,你會感謝我的哈

往期精選

分佈式數據之緩存技術,一起來揭開其神祕面紗

分佈式數據複製技術,今天就教你真正分身術

數據分佈方式之哈希與一致性哈希,我就是個神算子

分佈式存儲系統三要素,掌握這些就離成功不遠了

想要設計一個好的分佈式系統,必須搞定這個理論

分佈式通信技術之發佈訂閱,乾貨滿滿

分佈式通信技術之遠程調用:RPC

消息隊列Broker主從架構詳細設計方案,這一篇就搞定主從架構

消息中間件路由中心你會設計嗎,不會就來學學

消息隊列消息延遲解決方案,跟着做就行了

秒殺系統每秒上萬次下單請求,我們該怎麼去設計

【分佈式技術】分佈式系統調度架構之單體調度,非掌握不可

CDN加速技術,作爲開發的我們真的不需要懂嗎?

煩人的緩存穿透問題,今天教就你如何去解決

分佈式緩存高可用方案,我們都是這麼幹的

每天百萬交易的支付系統,生產環境該怎麼設置JVM堆內存大小

你的成神之路我已替你鋪好,沒鋪你來捶我

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章