1. 程式人生 > >java提供的IO方式

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!"););
	}
});