Java I/O 模型的演化,java演化
什麼是同步?什麼是非同步?阻塞和非阻塞又有什麼區別?本文先從 Unix 的 I/O 模型講起,介紹了5種常見的 I/O 模型。而後再引出 Java 的 I/O 模型的演化過程,並用執行個體說明如何選擇合適的 Java I/O 模型來提高系統的並發量和可用性。
由於,Java 的 I/O 依賴於作業系統的實現,所以先瞭解 Unix 的 I/O 模型有助於理解 Java 的 I/O。
相關概念同步和非同步
描述的是使用者線程與核心的互動方式:
- 同步是指使用者線程發起 I/O 請求後需要等待或者輪詢核心 I/O 操作完成後才能繼續執行;
- 非同步是指使用者線程發起 I/O 請求後仍繼續執行,當核心 I/O 操作完成後會通知使用者線程,或者調用使用者線程註冊的回呼函數。
阻塞和非阻塞
描述的是使用者線程調用核心 I/O 操作的方式:
- 阻塞是指 I/O 操作需要徹底完成後才返回到使用者空間;
- 非阻塞是指 I/O 操作被調用後立即返回給使用者一個狀態值,無需等到 I/O 操作徹底完成。
一個 I/O 操作其實分成了兩個步驟:發起 I/O 請求和實際的 I/O 操作。 阻塞 I/O 和非阻塞 I/O 的區別在於第一步,發起 I/O 請求是否會被阻塞,如果阻塞直到完成那麼就是傳統的阻塞 I/O ,如果不阻塞,那麼就是非阻塞 I/O 。 同步 I/O 和非同步 I/O 的區別就在於第二個步驟是否阻塞,如果實際的 I/O 讀寫阻塞請求進程,那麼就是同步 I/O 。
Unix I/O 模型
Unix 下共有五種 I/O 模型:
阻塞 I/O
請求無法立即完成則保持阻塞。
- 階段1:等待資料就緒。網路 I/O 的情況就是等待遠端資料陸續抵達;磁碟I/O的情況就是等待磁碟資料從磁碟上讀取到核心態記憶體中。
- 階段2:資料拷貝。出於系統安全,使用者態的程式沒有許可權直接讀取核心態記憶體,因此核心負責把核心態記憶體中的資料拷貝一份到使用者態記憶體中。
非阻塞 I/O
- socket 設定為 NONBLOCK(非阻塞)就是告訴核心,當所請求的 I/O 操作無法完成時,不要將進程睡眠,而是返回一個錯誤碼(EWOULDBLOCK) ,這樣請求就不會阻塞
- I/O 操作函數將不斷的測試資料是否已經準備好,如果沒有準備好,繼續測試,直到資料準備好為止。整個 I/O 請求的過程中,雖然使用者線程每次發起 I/O 請求後可以立即返回,但是為了等到資料,仍需要不斷地輪詢、重複請求,消耗了大量的 CPU 的資源
- 資料準備好了,從核心拷貝到使用者空間。
一般很少直接使用這種模型,而是在其他 I/O 模型中使用非阻塞 I/O 這一特性。這種方式對單個 I/O 請求意義不大,但給 I/O 多工鋪平了道路.
I/O 多工(非同步阻塞 I/O)
I/O 多工會用到 select 或者 poll 函數,這兩個函數也會使進程阻塞,但是和阻塞 I/O 所不同的的,這兩個函數可以同時阻塞多個 I/O 操作。而且可以同時對多個讀操作,多個寫操作的 I/O 函數進行檢測,直到有資料可讀或可寫時,才真正調用 I/O 操作函數。
從流程上來看,使用 select 函數進行 I/O 請求和同步阻塞模型沒有太大的區別,甚至還多了添加監視 socket,以及調用 select 函數的額外操作,效率更差。但是,使用 select 以後最大的優勢是使用者可以在一個線程內同時處理多個 socket 的 I/O 請求。使用者可以註冊多個 socket,然後不斷地調用 select 讀取被啟用的 socket,即可達到在同一個線程內同時處理多個 I/O 請求的目的。而在同步阻塞模型中,必須通過多線程的方式才能達到這個目的。
I/O 多工模型使用了 Reactor 設計模式實現了這一機制。
調用 select / poll 該方法由一個使用者態線程負責輪詢多個 socket,直到某個階段1的資料就緒,再通知實際的使用者線程執行階段2的拷貝。 通過一個專職的使用者態線程執行非阻塞I/O輪詢,類比實現了階段一的非同步化
訊號驅動 I/O(SIGIO)
首先我們允許 socket 進行訊號驅動 I/O,並安裝一個訊號處理函數,進程繼續運行並不阻塞。當資料準備好時,進程會收到一個 SIGIO 訊號,可以在訊號處理函數中調用 I/O 操作函數處理資料。
非同步 I/O
調用 aio_read 函數,告訴核心描述字,緩衝區指標,緩衝區大小,檔案位移以及通知的方式,然後立即返回。當核心將資料拷貝到緩衝區後,再通知應用程式。
非同步 I/O 模型使用了 Proactor 設計模式實現了這一機制。
告知核心,當整個過程(包括階段1和階段2)全部完成時,通知應用程式來讀資料.
幾種 I/O 模型的比較
前四種模型的區別是階段1不相同,階段2基本相同,都是將資料從核心拷貝到調用者的緩衝區。而非同步 I/O 的兩個階段都不同於前四個模型。
同步 I/O 操作引起請求進程阻塞,直到 I/O 操作完成。非同步 I/O 操作不引起請求進程阻塞。
常見 Java I/O 模型
在瞭解了 UNIX 的 I/O 模型之後,其實 Java 的 I/O 模型也是類似。
“阻塞I/O”模式
在上一節 Socket 章節中的 EchoServer 就是一個簡單的阻塞 I/O 例子,伺服器啟動後,等待用戶端串連。在用戶端串連伺服器後,伺服器就阻塞讀寫取資料流。
EchoServer 代碼:
public class EchoServer { public static int DEFAULT_PORT = 7; public static void main(String[] args) throws IOException { int port; try { port = Integer.parseInt(args[0]); } catch (RuntimeException ex) { port = DEFAULT_PORT; } try ( ServerSocket serverSocket = new ServerSocket(port); Socket clientSocket = serverSocket.accept(); PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true); BufferedReader in = new BufferedReader( new InputStreamReader(clientSocket.getInputStream())); ) { String inputLine; while ((inputLine = in.readLine()) != null) { out.println(inputLine); } } catch (IOException e) { System.out.println("Exception caught when trying to listen on port " + port + " or listening for a connection"); System.out.println(e.getMessage()); } }}改進為“阻塞I/O+多線程”模式
使用多線程來支援多個用戶端來訪問伺服器。
主線程 MultiThreadEchoServer.java
public class MultiThreadEchoServer { public static int DEFAULT_PORT = 7; public static void main(String[] args) throws IOException { int port; try { port = Integer.parseInt(args[0]); } catch (RuntimeException ex) { port = DEFAULT_PORT; } Socket clientSocket = null; try (ServerSocket serverSocket = new ServerSocket(port);) { while (true) { clientSocket = serverSocket.accept(); // MultiThread new Thread(new EchoServerHandler(clientSocket)).start(); } } catch (IOException e) { System.out.println( "Exception caught when trying to listen on port " + port + " or listening for a connection"); System.out.println(e.getMessage()); } }}
處理器類 EchoServerHandler.java
public class EchoServerHandler implements Runnable { private Socket clientSocket; public EchoServerHandler(Socket clientSocket) { this.clientSocket = clientSocket; } @Override public void run() { try (PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true); BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));) { String inputLine; while ((inputLine = in.readLine()) != null) { out.println(inputLine); } } catch (IOException e) { System.out.println(e.getMessage()); } }}
存在問題:每次接收到新的串連都要建立一個線程,處理完成後銷毀線程,代價大。當有大量地短串連出現時,效能比較低。
改進為“阻塞I/O+線程池”模式
針對上面多線程的模型中,出現的線程重複建立、銷毀帶來的開銷,可以採用線程池來最佳化。每次接收到新串連後從池中取一個空閑線程進行處理,處理完成後再放回池中,重用線程避免了頻率地建立和銷毀線程帶來的開銷。
主線程 ThreadPoolEchoServer.java
public class ThreadPoolEchoServer { public static int DEFAULT_PORT = 7; public static void main(String[] args) throws IOException { int port; try { port = Integer.parseInt(args[0]); } catch (RuntimeException ex) { port = DEFAULT_PORT; } ExecutorService threadPool = Executors.newFixedThreadPool(5); Socket clientSocket = null; try (ServerSocket serverSocket = new ServerSocket(port);) { while (true) { clientSocket = serverSocket.accept(); // Thread Pool threadPool.submit(new Thread(new EchoServerHandler(clientSocket))); } } catch (IOException e) { System.out.println( "Exception caught when trying to listen on port " + port + " or listening for a connection"); System.out.println(e.getMessage()); } }}
存在問題:在大量短串連的情境中效能會有提升,因為不用每次都建立和銷毀線程,而是重用串連池中的線程。但在大量長串連的情境中,因為線程被串連長期佔用,不需要頻繁地建立和銷毀線程,因而沒有什麼優勢。
改進為“非阻塞I/O”模式
“阻塞I/O+線程池”網路模型雖然比”阻塞I/O+多線程”網路模型在效能方面有提升,但這兩種模型都存在一個共同的問題:讀和寫操作都是同步阻塞的,面對大並發(持續大量串連同時請求)的情境,需要消耗大量的線程來維持串連。CPU 在大量的線程之間頻繁切換,效能損耗很大。一旦單機的串連超過1萬,甚至達到幾萬的時候,伺服器的效能會急劇下降。
而 NIO 的 Selector 卻很好地解決了這個問題,用主線程(一個線程或者是 CPU 個數的線程)保持住所有的串連,管理和讀取用戶端串連的資料,將讀取的資料交給後面的線程池處理,線程池處理完商務邏輯後,將結果交給主線程發送響應給用戶端,少量的線程就可以處理大量串連的請求。
Java NIO 由以下幾個核心部分組成:
要使用 Selector,得向 Selector 註冊 Channel,然後調用它的 select()方法。這個方法會一直阻塞到某個註冊的通道有事件就緒。一旦這個方法返回,線程就可以處理這些事件,事件的例子有如新串連進來,資料接收等。
主線程 NonBlokingEchoServer.java
public class NonBlokingEchoServer { public static int DEFAULT_PORT = 7; public static void main(String[] args) throws IOException { int port; try { port = Integer.parseInt(args[0]); } catch (RuntimeException ex) { port = DEFAULT_PORT; } System.out.println("Listening for connections on port " + port); ServerSocketChannel serverChannel; Selector selector; try { serverChannel = ServerSocketChannel.open(); InetSocketAddress address = new InetSocketAddress(port); serverChannel.bind(address); serverChannel.configureBlocking(false); selector = Selector.open(); serverChannel.register(selector, SelectionKey.OP_ACCEPT); } catch (IOException ex) { ex.printStackTrace(); return; } while (true) { try { selector.select(); } catch (IOException ex) { ex.printStackTrace(); break; } Set<SelectionKey> readyKeys = selector.selectedKeys(); Iterator<SelectionKey> iterator = readyKeys.iterator(); while (iterator.hasNext()) { SelectionKey key = iterator.next(); iterator.remove(); try { if (key.isAcceptable()) { ServerSocketChannel server = (ServerSocketChannel) key.channel(); SocketChannel client = server.accept(); System.out.println("Accepted connection from " + client); client.configureBlocking(false); SelectionKey clientKey = client.register(selector, SelectionKey.OP_WRITE | SelectionKey.OP_READ); ByteBuffer buffer = ByteBuffer.allocate(100); clientKey.attach(buffer); } if (key.isReadable()) { SocketChannel client = (SocketChannel) key.channel(); ByteBuffer output = (ByteBuffer) key.attachment(); client.read(output); } if (key.isWritable()) { SocketChannel client = (SocketChannel) key.channel(); ByteBuffer output = (ByteBuffer) key.attachment(); output.flip(); client.write(output); output.compact(); } } catch (IOException ex) { key.cancel(); try { key.channel().close(); } catch (IOException cex) { } } } } }}改進為“非同步I/O”模式
Java SE 7 版本之後,引入了非同步 I/O (NIO.2) 的支援,為構建高效能的網路應用提供了一個利器。
主線程 AsyncEchoServer.java
public class AsyncEchoServer { public static int DEFAULT_PORT = 7; public static void main(String[] args) throws IOException { int port; try { port = Integer.parseInt(args[0]); } catch (RuntimeException ex) { port = DEFAULT_PORT; } ExecutorService taskExecutor = Executors.newCachedThreadPool(Executors.defaultThreadFactory()); // 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; } }; taskExecutor.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 taskExecutor.shutdown(); // wait until all threads are finished while (!taskExecutor.isTerminated()) { } break; } } } else { System.out.println("The asynchronous server-socket channel cannot be opened!"); } } catch (IOException ex) { System.err.println(ex); } }}源碼
本章例子的源碼,可以在 https://github.com/waylau/essential-java 中 com.waylau.essentialjava.net.echo 包下找到。
問啊-定製化IT教育平台,牛人一對一服務,有問必答,開發編程社交頭條 官方網站:www.wenaaa.com
QQ群290551701 聚集很多互連網精英,技術總監,架構師,專案經理!開源技術研究,歡迎業內人士,大牛及新手有志於從事IT行業人員進入!