java提供的IO方式
根據IO的抽象模型,通常分為BIO,NIO,AIO三種
區分同步和非同步(Synchronous/asynchronous):同步是一種比較可靠的執行方式,當進行同步操作時,後續的任務等待當前的呼叫操作返回之後才往下進行。而非同步機制不需要等待當前呼叫操作返回,通常依賴事件、回撥機制來實現任務之間的先後順序關係。
區分阻塞和非阻塞blocking/non-blocking。當進行阻塞操作時,當前執行緒會執行在阻塞狀態(執行緒共有五種狀態:新建New,執行Running,就緒Runnable,阻塞blocked, 死亡Dead),無法處理其他任務,只有當條件就緒之後,才能繼續下去。而非阻塞不管IO操作是否完成,直接返回,後續的操作在後臺繼續處理。
BIO
BIO是阻塞IO,互動方式是同步、阻塞的方式,在讀取輸入流或者寫入輸出流的時候,在讀寫動作完成之前,執行緒會一致阻塞在那裡,他們之間的關係是線性順序的。
傳統的java.io包就是這種方式,它採用流模型是先,提供了常用的輸入輸出流。java.io包的好處就是程式碼簡單、看起來直觀,缺點是IO的效率不高,擴充套件性也不好。
通常情況下,把java.net下面提供的有些網路API,比如Socket,ServerSocket、HttpUELConnection也劃分到同步阻塞IO類庫裡面,因為網路通訊也同樣是IO行為。
NIO
在java1.4的時候引入了NIO框架(java.nio包),提供了Channel、Selector,Buffer這類新的抽象,寫出來的程式可以使多路複用,同步非阻塞的。
NIO的主要組成部分:
- Buffer:一個高效率的資料容器,除了布林型別,其他的原始資料型別都有相應的Buffer實現。(原始資料型別包括 邏輯型別Boolean,文字型別char,整數型別byte,short,int,long。浮點型double,float。 )
- Channel:是NIO中用來支援批量IO操作的一種抽象結構,類似於Linux系統中的檔案描述符。Socket是更高層次的抽象,而channel是更底層的抽象,我們可以通過Socket來獲取Channel。
- Selector:是NIO實現多路複用的基礎,可以檢測註冊到Selector上的多個Channel中,有沒有Channel處於就緒狀態,這樣就實現了單執行緒對多Channel的高效管理。
- Charset,提供Unicode字串定義,IO也提供了物件的編碼解碼器,比如可以通過下面的方式進行字串到ByteBuffer的轉換:
Charset.defaultCharset().encode("Hello world!");
NIO能解決什麼問題?
下面舉一個具體場景,假如要求實現一個伺服器應用,要求能夠同時服務多個客戶端請求。先使用java.io和java.net中的同步、阻塞API簡單實現。
public class DemoServer extends Thread {
private ServerSocket serverSocket;
public int getPort() {
return serverSocket.getLocalPort();
}
try {
serverSocket = new ServerSocket(0);
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();
try(Socket client = new Socket(InetAddress.getLocalHost(), server.getPort())) {
BufferReader bufferReader = new BufferReader(new InputStreamReader(client.getInputStream));
buferReader.lines().forEach(s -> System.out.print(s));
}
}
}
//簡化實現,不做讀取操作,直接傳送字串
class RequestHandler extends Thread {
private Socket socket;
ReqestHandler(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try(PrintWriter out = new PrintWriter(socket.getOutputStream());) {
out.println("Hello world!");
out.flush();
} catch(IOExceptiojn e) {
e.printStackTrace();
}
}
}
上面的程式幾個要點:
- 伺服器端啟動ServerScoket,,埠0自動繫結一個空閒埠
- 呼叫accept方法,阻塞等待客戶端連線
- 利用Socket模擬一個簡單的客戶端 ,只進行連線、讀取,列印
- 當連線建立之後,啟動一個單獨的執行緒負責客戶端的請求。
這個解決方案,在擴充套件性方面嘛有問題,由於java的執行緒實現是比較重量級的操作,啟動銷燬一個執行緒有明顯的開銷。未來解決這個問題,可以引入執行緒池的機制
serverSocket = new ServerSocket(0);
executor = Executor.newFixedThreadPool(8);
while(true) {
Socket socket = serverSocket.accept();
RequestHandler requestHandler = new RequestHandler(socket);
executor.exetute(requestHandler);
}
這樣通用化一個固定大小的執行緒池,來管理執行緒,避免執行緒的頻繁建立和銷燬,這也是構建併發服務的一個典型方式。如果連線不是很多,比如只有幾百個連線的普通應用,這種模式可以很好地工作。但是如果連線數極速上升,在高併發情況下,執行緒的上下文切換開銷就會比較大,這時候同步阻塞方式就遇到了瓶頸了。
NIO提供的多路複用機制提供了另一種解決方案。
public class NIOServer extends Thread {
public void run() {
try(Selector selector = Selector.open();
ServerSocketChannel serverSocket = ServerSocketChannel.open();) {
serverSocket.bind(new InetSocketAddress(InetAddress.getLocalHost(), 8888));
serverSocket.configuerBlocking(false);
//註冊到Selector,並說明關注點
serverSocket.register(selector, SelectionKey.OP_ACCEPT);
while(true) {
selector.select();//阻塞就緒的Chanel,這是關鍵點之一
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();
}
}
public void sayHelloWorld(ServerSocketChannel server) throws IOException{
try(SocketChannel client = server.accept();) {
client.write(Charset.defaultCharset().encode("Hello world"););
}
}
//main函式與前面類似,暫時省略
}
在上面的程式碼中,有幾個主要步驟如下:
- 首先,通過Selector.open()建立一個Selector,角色類似於排程員
- 然後,建立一個ServerSocketChannel,並向Selector註冊,通過指定SelectionKey.OP_ACCEPT,同時排程員,它關注的是新的連線請求。 為什麼在這裡明確要求配置非阻塞模式?因為在阻塞模式下,註冊操作時不允許的,會丟擲IllegalBlockingModeException異常。
- Selector阻塞在select操作,當有Channel發生接入請求,就會被喚醒。
- 在sayHelloWorld方法中,通過SocketChannel和Buffer進行資料操作,在上面程式碼中是傳送了一段字串
當IO處於同步阻塞模式的時候,需要多執行緒實現多工管理,而處於NIO模式的時候,可以通過單執行緒輪詢,高效地找到就緒的Channel,來決定做什麼,僅僅在select階段是阻塞的,這樣可以避免大量客戶端連線的時候,由於頻繁的執行緒切換帶來的效能問題,可以提升應用的擴充套件能力
AIO
在java7當中,NIO引入了非同步非阻塞IO方式,也稱為AIO(Asynchronous IO)。非同步IO操作基於事件和回撥機制,可以處理Read,Accept之類的操作,可以理解為應用操作直接返回,而不用阻塞在那裡等待,當後臺處理完成,作業系統會通知相應執行緒完成後續的工作。
AsynchronousServerSocketChannel serverSock = AsynchronousServerSocketChannel.open().bind(sockAddr);
serverSock.accept(serverSock, new CompletionHandler<>() {
//為非同步操作指定CompletionHandler回撥函式
@override
public void completed(AsynchronousServerSocketChannel sockChannel,
AsynchronousServerSocketChannel serverSock) {
serverSock.accept(serverSock, this);
sayHelloWorld(sockChannel, Charset.defaultCharset().encode("Hello World!"););
}
});