Java 提供的一些IO方式

       傳統的java.io包基於流模型實現,提供了一些 如File抽象,輸入輸出流等基本功能,交互方式是同步、阻塞的方式(Blocking IO)。也就是說,在讀取輸入流或者寫入輸出流時,在讀、寫操作完成之前,線程會一直阻塞在那裏,他們之間的調用是可靠的線性順序。傳統的Java.io的優勢在於簡單直觀,但IO效率不高,其擴展性受限,很容易成爲項目的瓶頸。
       另外,Java.net中的一些網絡API也可以歸結到同步阻塞的IO類庫,比如Socket、ServerSocket、HttpURLConnection。因爲網絡通信同樣也是IO行爲。
       在Java1.4版本中引入了NIO框架(java.nio),提供了Channel、Buffer、Selector等新的抽象,可以用以構建多路複用、同步非阻塞的IO程序
       在Java7中的NIO2(AIO)框架引入了異步、非阻塞的交互方式,異步IO操作基於事件和回調機制,應用操作直接返回而不會阻塞在那裏,當後臺處理完後,操作系統會通知相應的線程進行後續工作。

  • java.io不僅僅是對文件的操作,網絡通信中,比如Socket通信,都是典型的IO操作的目標。

  • 輸入流/輸出流(InputStream/OutputStream):是用來讀取或者寫入字節的,例如操作圖片文件。

  • Reader/Writer:是用來操作字符的,增加了字符的編解碼功能,適用於從文件中讀取或者寫入文本信息。本質上計算機操作的都是字節,不管是文件讀取還是網絡通信,Reader和Writer構建了應用邏輯和原始數據之間的橋樑。

  • 帶緩衝區的讀寫操作,例如BufferedInputStream、BufferedOutputStream,可以避免頻繁的磁盤讀寫,這種設計利用了緩衝區,一次操作批量數據。在使用中千萬不能忘了flash

  • 參考下面簡化類圖,很多IO工具類都實現了Closeable接口,對資源進行釋放。
    在這裏插入圖片描述

實例:實現一個服務器應用,簡單要求:能夠爲多個客戶端提供服務。

  • 使用java.io和java.net中的同步、阻塞式API來簡單實現
    1.服務端啓動一個ServerSocket,端口0表示自動綁定一個空閒的端口。
    2.調用accept方法,阻塞等待客戶端連接。一旦連接成功,服務端的accept會返回一個負責傳輸數據的已連接socket。(一般,連接建立成功後,雙方就可以開始通過read和write函數來讀寫數據)
    3.連接建立成功後,啓動一個單獨線程回覆客戶端的請求。
    4.利用Socket模擬一個簡單的客戶端,只進行連接、讀取、打印。
    相關模擬代碼:
package Test;

import java.io.*;
import java.net.*;

public class DemoServer extends Thread {
    private ServerSocket serverSocket;
    public int getPort() {
        return  serverSocket.getLocalPort();
    }
    public void run() {
        try {
            //服務端啓動一個ServerSocket,端口0表示自動綁定一個空閒的端口
            //這裏的ServerSocket是一個監聽的socket
            serverSocket = new ServerSocket(0);
            while (true) {
                //調用accept方法,阻塞等待客戶端連接
                //這裏的Socket是一個用於連接的socket,負責傳輸數據
                Socket socket = serverSocket.accept();
                //當連接建立後單獨啓動一個線程響應客戶端的請求
                RequestHandler requestHandler = new RequestHandler(socket);
                requestHandler.start();
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (serverSocket != null) {
                try {
                    serverSocket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public static void main(String[] args) throws IOException {
        DemoServer server = new DemoServer();
        server.start();
        //利用socket模擬一個簡單的客戶端,只進行連接、讀取、打印
        try (Socket client = new Socket(InetAddress.getLocalHost(), server.getPort())) {
            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(client.getInputStream()));
            bufferedReader.lines().forEach(s -> System.out.println(s));
        }
    }
}


//響應客戶端請求的線程
package Test;

import java.io.PrintWriter;
import java.net.Socket;

// 簡化實現,不做讀取,直接發送字符串
class RequestHandler extends Thread {
    private Socket socket;
    RequestHandler(Socket socket) {
        this.socket = socket;
    }
    @Override
    public void run() {
        try (PrintWriter out = new PrintWriter(socket.getOutputStream())) {
            out.println("Hello world!");
            out.flush();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

使用簡單的java.io與java.net實現的同步、阻塞式API存在的潛在問題:Java目前的線程實現是比較重量級的,創建或者銷燬一個線程有明顯的開銷,每個線程都有單獨的線程棧結構,需要佔用比較明顯的內存,所以,每一個client啓動一個線程,挺浪費的。

解決方案:引入線程池來避免浪費——通過一個固定大小的線程池,來負責管理響應客戶端請求的工作線程,避免頻繁創建、銷燬線程的開銷,這是構建併發服務的典型。參考下圖進行理解:
在這裏插入圖片描述
相關代碼:

package Test;

import java.io.*;
import java.net.*;
import java.util.concurrent.*;

public class DemoServer extends Thread {
    private ServerSocket serverSocket;
    public int getPort() {
        return  serverSocket.getLocalPort();
    }
    public void run() {
        try {
            //服務端啓動一個ServerSocket,端口0表示自動綁定一個空閒的端口
            //這裏的ServerSocket是一個監聽的socket
            serverSocket = new ServerSocket(0);
            //創建一個指定工作數量的線程池
            ExecutorService executor = Executors.newFixedThreadPool(8);
            while (true) {
                //調用accept方法,阻塞等待客戶端連接
                // 這裏的Socket是一個用於連接的socket,負責傳輸數據
                Socket socket = serverSocket.accept();
                //創建一個響應客戶端的請求的工作線程提交到線程池中 
                RequestHandler requestHandler = new RequestHandler(socket);
                executor.execute(requestHandler);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (serverSocket != null) {
                try {
                    serverSocket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public static void main(String[] args) throws IOException {
        //服務端啓動ServerSocket
        DemoServer server = new DemoServer();
        server.start();
        //利用socket模擬一個簡單的客戶端,只進行連接、讀取、打印
        try (Socket client = new Socket(InetAddress.getLocalHost(), server.getPort())) {
            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(client.getInputStream()));
            bufferedReader.lines().forEach(s -> System.out.println(s));
        }
    }
}

線程池解決 重量級線程創建與銷燬的開銷大 的方法只在普通應用——僅幾百個連接的情況下,可以工作得很好。一旦連接數量變得超級多,線程上下文切換的開銷會在高併發是變得非常明顯,這是同步阻塞方式的劣勢——低擴展性。

前兩個示例,IO都是同步阻塞模式,需要利用多線程來實現多任務處理。而NIO多路複用利用了單線程輪詢事件的機制,通過定位就緒的Channel來決定做什麼,僅僅在select階段是阻塞的,可以有效避免當大量的客戶端連接時,頻繁的線程切換帶來的開銷。如下圖所示。
在這裏插入圖片描述

  • NIO引入多路複用機制
    1.通過Selector.open()創建一個selector,作爲類似調度員的角色。
    2.通過ServerSocketChannel.open()創建一個Channel,明確配置其爲非阻塞模式。
    3.Selector阻塞在select操作,當有Channel發生連接請求時,Selector被喚醒。
    4.通過SocketChannel和Buffer進行數據操作,發送字符串。

相關代碼:

package Test;

import java.io.*;
import java.net.*;
import java.nio.channels.*;
import java.nio.charset.Charset;
import java.util.*;

public class NIOServer extends Thread {
    public void run() {
        // 通過Selector.open()創建一個selector,作爲類似調度員的角色
        //通過ServerSocketChannel.open()創建一個channel
        try (Selector selector = Selector.open();
             ServerSocketChannel serverSocket = ServerSocketChannel.open();) {
            //綁定ip和端口
            serverSocket.bind(new InetSocketAddress(InetAddress.getLocalHost(), 8888));
            //需要明確配置是 非阻塞模式,因爲在阻塞模式下,註冊操作是不允許的,會拋出IllegalBlockingModeException異常
            serverSocket.configureBlocking(false);
            // 註冊到 Selector,並指定SelectionKey.OP_ACCEPT,告訴Selector,它關注的是最新連接請求
            serverSocket.register(selector, SelectionKey.OP_ACCEPT);
            while (true) {
                //Selector阻塞在select()請求,當有Channel發生連接請求,就會被喚醒
                // 阻塞等待就緒的 Channel,這是關鍵點之一
                selector.select();
                //採用單線程輪詢事件的機制,通過高效確定就緒的Channel,來決定做什麼
                Set<SelectionKey> selectedKeys = selector.selectedKeys();
                Iterator<SelectionKey> iter = selectedKeys.iterator();
                while (iter.hasNext()) {
                    SelectionKey key = iter.next();
                    // 生產系統中一般會額外進行就緒狀態檢查
                    sayHelloWorld((ServerSocketChannel) key.channel());
                    iter.remove();
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    //通過SocketChannel和Buffer進行數據操作,發送字符串
    private void sayHelloWorld(ServerSocketChannel server) throws IOException {
        try (SocketChannel client = server.accept();) {
            client.write(Charset.defaultCharset().encode("Hello world!"));
        }
    }

    public static void main(String[] args) throws IOException {
        NIOServer nioserver = new NIOServer();
        nioserver.start();
        //利用socket模擬一個簡單的客戶端,只進行連接、讀取、打印
        try (Socket client = new Socket(InetAddress.getLocalHost(), 8888)) {
            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(client.getInputStream()));
            bufferedReader.lines().forEach(s -> System.out.println(s));
        }
    }
}


參考:
Java核心技術
Java BIO|NIO|AIO的學習
Java常用的線程池的幾種比較
併發和並行的區別

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