上次說了BIO編程。爲了解決同步阻塞I/O面臨的一個鏈路需要一個線程處理的問題,後來人們對它的線程模型進行了優化,服務器端通過一個線程池來處理多個客戶端的請求接入,形成客戶端個數M對應線程池N的比例關係。通過線程池可以靈活調配線程資源,設置線程的最大值,防止由於海量併發客戶端接入導致線程耗盡。這就是僞異步I/O編程。
如下圖所示,採用線程池和任務隊列可以實現僞異步I/O通信框架。
僞異步I/O服務器端通信模型服務器端代碼:
package com.test.aio1;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* 僞異步I/O服務器端
* @author 程就人生
* @Date
*/
public class HelloServer {
public static void main( String[] args ){
int port = 8080;
ServerSocket serverSocket = null;
try {
serverSocket = new ServerSocket(port);
Socket socket = null;
HelloServerHandleExecutePool execute = new HelloServerHandleExecutePool(50, 1000);
// 通過無限循環監聽客戶端連接
while(true){
// 如果沒有客戶端連接,則阻塞在Accept操作上
socket = serverSocket.accept();
// 如果有客戶端連接,則加入線程池執行
execute.execute(new HelloHandler(socket));
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 線程池類
* @author 程就人生
* @Date
*/
class HelloServerHandleExecutePool{
private ExecutorService executorService;
public HelloServerHandleExecutePool(int maxPoolSize, int queueSize) {
executorService = new ThreadPoolExecutor(Runtime.getRuntime().availableProcessors(), maxPoolSize, 120L, TimeUnit.SECONDS, new ArrayBlockingQueue<java.lang.Runnable>(queueSize));
}
public void execute(java.lang.Runnable task){
executorService.execute(task);
}
}
/**
* hello處理類
* @author 程就人生
* @Date
*/
class HelloHandler implements Runnable{
private Socket socket;
public HelloHandler(Socket socket) {
this.socket = socket;
}
public void run() {
BufferedReader reader = null;
PrintWriter writer = null;
try {
reader = new BufferedReader(new InputStreamReader(this.socket.getInputStream()));
// 返回給客戶端,是否刷新設置爲true
writer = new PrintWriter(this.socket.getOutputStream(), true);
String body = null;
while(true){
body = reader.readLine();
if(body == null){
break;
}
System.out.println("服務器端接收到的:" + body);
writer.println("來自服務器端的響應!");
}
} catch (IOException e) {
e.printStackTrace();
// 出現異常時,對資源的釋放
if(reader != null){
try {
reader.close();
} catch (IOException e1) {
e1.printStackTrace();
}
}
if(writer != null){
writer.close();
writer = null;
}
if(socket != null){
try {
socket.close();
} catch (IOException e1) {
e1.printStackTrace();
}
}
}
}
}
僞異步I/O服務器端的主函數有了變化,先創建一個Hello服務處理類的線程池。當接收到新的客戶端連接時,將請求soket封裝成一個Task,然後調用線程池的execute方法執行,從而避免每個客戶端連接時都創建一個新線程。由於線程池和消息隊列都是有界的。因此,無論客戶端併發連接數多大,都不會導致服務器端線程個數過於膨脹而導致內存溢出,相比於傳統的BIO線程模型是一種改良。 運行服務器端結果。
雖然僞異步I/O通信框架採用了線程池實現,因此避免了每個請求都創建一個獨立線程造成的線程資源耗盡問題,但它的底層的通信依舊採用同步阻塞模型,因此無法從根本上解決問題。一起看下socket的InputStream方法。
再看下InputStream類。
當對Socket的輸入流進行讀取操作時,它會一直堵塞下去,直到發生三種事件:
- 有數據可讀。
- 可用數據已經讀完。
- 發生異常。
如果對方發生請求或者應答比較緩慢,或者網絡傳輸比較慢,讀取輸入流一方的通信線程將會被長時間阻塞。如果對方要60s才能將數據發送完成,讀取一方的I/O也會被阻塞60s。在這60s期間,其他接入的客戶端只能在消息隊列中排隊。僞異步I/O只是對BIO線程模型的一個簡單優化,無法從根本上解決同步IO導致的通信線程阻塞問題。
僞異步I/O的缺點顯而易見。
- 如果客戶端請求量增加,會導致服務器端處理應答消息緩慢。
- 如果有故障節點,由於讀取輸入流的阻塞的,它會被阻塞,直到釋放。
- 如果所有的線程都被故障服務器阻塞,那麼後續的IO消息都將在隊列中排隊等等。