第2章 NIO入門
阿新 • • 發佈:2019-05-03
復用 失去 lock span output jstack string run cat
2.1 傳統的BIO編程
以服務器為例,在傳統BIO模型下的服務器,每當一個新的請求到來的時候回分配一個線程去處理該請求,並且該線程在執行IO操作的時候會一直阻塞,知道IO操作完成或拋出異常才會返回。當網絡情況不佳時,網絡IO可能會耗費大量時間,那麽就會同時有大量線程在服務器上阻塞著,很容易造成內存溢出。
這種模型被稱為同步阻塞模型,同步指的是只有等待線程IO操作完成該線程才會返回,阻塞指的是IO沒有完成的時候一直等待。
2.1.1 服務端代碼
Server類,監聽8080端口,在while循環裏Server端阻塞在server.accept上,即等待請求傳到8080端口上,從accept方法返回。後續new Thread是新建一個線程去處理請求。
public class TimeServer {
public static void main(String[] args) throws IOException {
int port = 8080;
ServerSocket server = null;
try {
server = new ServerSocket(port);
System.out.println("The server start in port "+port);
Socket socket = null;while (true){
socket = server.accept();
Thread thread = new Thread(new TimeServerHandler(socket));
thread.start();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (server!=null){
System.out.println("Time server close");server.close();
server=null;
}
}
}
}
執行代碼用jstack打印線程狀態,看到server線程在17行“停止”,即阻塞在17行等待請求傳過來。這種阻塞就是BIO裏的B,block,一個IO沒有完成就一直卡在那裏。
有新的客戶端接入新建線程執行處理方法,通過檢查傳過來的字符串是否是要求的“QUERY TIME ORDER”,如果是就返回當前服務器的時間,否則返回錯誤信息。
public class TimeServerHandler implements Runnable { private Socket socket; public TimeServerHandler(Socket socket) { this.socket = socket; } public void run() { BufferedReader in = null; PrintWriter out = null; try { in = new BufferedReader(new InputStreamReader(socket.getInputStream())); out = new PrintWriter(socket.getOutputStream(),true); String currentTime = null; String body = null; while (true){ body = in.readLine(); if (body == null){ break; } System.out.println("The time server receive order: "+body); currentTime = "QUERY TIME ORDER".equalsIgnoreCase(body)?new Date(System.currentTimeMillis()).toString():"BAD ORDER"; out.println(currentTime); } } catch (IOException e) { e.printStackTrace(); if (in!=null){ try { in.close(); } catch (IOException ex) { ex.printStackTrace(); } } if (out!=null){ out.close(); out = null; } if (socket!=null){ try { socket.close(); } catch (IOException ex) { ex.printStackTrace(); }finally { socket=null; } } } } }
同時開啟服務端和客戶端後可以在控制臺看到輸出。
2.1.2 缺點
- 每個請求都需要一個新建線程去處理,扛不住太大的並發,因為沒有給線程數目設置瓶頸。
- BIO會在網絡不佳情況導致大量線程。
2.2 偽異步IO編程
2.2.1 代碼
用線程池代替不停的新建線程,好處是線程池是有界的,避免在極端情況下不停新建線程。
public class TimeServer_ThreadPool { public static void main(String[] args) { int port = 8080; ServerSocket server = null; try { server = new ServerSocket(port); System.out.println("server start at port "+ port); Socket socket = null; ExecutorService pool = Executors.newFixedThreadPool(10); while (true){ socket = server.accept(); pool.execute(new TimeServerHandler(socket)); } } catch (IOException e) { e.printStackTrace(); } } }
2.2.2 弊端
- 使用BIO讀取數據時,線程會一直阻塞直到 1、有數據讀 2、數據讀取完畢 3、拋出異常。當客戶端請求發送較慢或者網絡時延較時,讀取數據的線程會一直阻塞。
- 使用BIO輸出數據時,線程會一直阻塞直到 1、有數據寫 2、數據寫完 3、拋出異常。當服務端寫數據時,如果網絡情況不佳,客戶端不能及時讀取數據,大量數據留在TCP緩沖區,當發送端即服務端的Window size為0的時候寫線程就會無法繼續寫從而阻塞。
- 無論讀還是寫,都是阻塞的,阻塞與否以及阻塞是否嚴重依賴於網絡傳輸的質量。
- 當線程池的隊列使用阻塞隊列時,前臺線程負責把請求封裝成對象加入線程池的阻塞隊列,如果網絡狀況十分的差,阻塞隊列也滿了,那麽復制把請求對象加入阻塞隊列的前臺線程也會阻塞,整個系統失去異步性,所有的請求都會超時。
2.3 NIO 編程
NIO與BIO的區別在兩點
- 面向的對象不同。BIO面向Stream,該Stream是單向通信,只能是讀Stream或者寫Stream。NIO面向Buffer,NIO面向buffer和channel,channel是鐵路,buffer是鐵路上運輸的數據。
- 阻塞性。BIO是阻塞的,NIO通過Selector實現多路復用。
2.3.1 Buffer與Channel
第2章 NIO入門