傳統的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));
}
}
}