Java面試總結之AIO與NIO


 

1、Java NIO 是一種同步非阻塞的I/O模型

將多個IO的阻塞復用到同一個select的阻塞上,從而使得系統在單線程的情況下處理多個客戶端請求。

NIO三個核心對象:通道(Channel)、緩沖區(Buffer)和選擇器(Selector)

具體說就是Selector會不斷輪詢注冊在其上的Channel,如果某個Channel上有新的TCP連接,讀或者寫事件,這個Channel就處於就緒狀態,會被Selector輪詢出來,然后通過SelectorKey可以獲取就緒Channel的集合,進行后續I/O操作。

NIO單線程輪詢事件,找到可以進行讀寫的網絡描述符進行讀寫。除了事件的輪詢是阻塞的(沒有可干的事情必須要阻塞),剩余的I/O操作都是純CPU操作,沒有必要開啟多線程。並且由於線程的節約,連接數大的時候因為線程切換帶來的問題也隨之解決,進而為處理海量連接提供了可能。

單線程處理I/O的效率確實非常高,沒有線程切換,只是拼命的讀、寫、選擇事件。但現在的服務器,一般都是多核處理器,如果能夠利用多核心進行I/O,無疑對效率會有更大的提高。

 

package java.nio;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;

public class MultiplexerTimeServer implements Runnable {

private Selector selector;

private ServerSocketChannel servChannel;

private volatile boolean stop;

/**
 * 初始化多路復用器、綁定監聽端口
 */
public MultiplexerTimeServer(int port) {
	try {
		selector = Selector.open();
		
		// Channel主要用來讀寫網絡上的數據的。打開ServerSocketChannel,
		// 用於監聽客戶端的連接,它是所有客戶端連接的父管道
		servChannel = ServerSocketChannel.open();
		
		// 設置為非阻塞模式
		servChannel.configureBlocking(false);
		
		// 綁定監聽端口 8080
		servChannel.socket().bind(new InetSocketAddress(port), 1024);
		
		/*
		 * Selector會不斷地輪詢在其上的Channel,如果某個Channel上面有新的TCP
		 * 連接接入、讀和寫事件,這個Channel就處於就緒狀態
		 * 
		 * 注冊到Reactor線程的多路復用器Selector上,監聽ACCEPT事件
		 */
		servChannel.register(selector, SelectionKey.OP_ACCEPT);
		
		System.out.println("The time server is start in port : " + port);
	} catch (IOException e) {
		e.printStackTrace();
		System.exit(1);
	}
}

public void stop() {
	this.stop = true;
}

public void run() {
	while (!stop) {
		try {
			// This method performs a blocking selection operation. 
			// It returns only after at least one channel is selected,
			//(只有在至少有一個事件就緒后才會進行返回,所以是阻塞的) 
			// this selector's wakeup method is invoked, the current thread is 
			// interrupted,or the given timeout period expires, whichever comes first. 
			selector.select(1000); // 阻塞等待,休眠時間為1s
			Set<SelectionKey> selectedKeys = selector.selectedKeys();
			/*
			 * 當有處於就緒狀態的Channel時,selector將返回就緒狀態的Channel的SelectionKey
			 * 集合,通過對就緒狀態的Channel集合進行迭代,可以進行網絡的異步讀寫操作
			 */
			Iterator<SelectionKey> it = selectedKeys.iterator();
			SelectionKey key = null;
			while (it.hasNext()) {
				key = it.next();
				it.remove();
				try {
					// 事件分發器,單線程選擇就緒的事件。
					// I/O處理器,包括connect、read、write等,這種純CPU操作,一般開啟CPU核心
					// 個線程就可以。業務線程,在處理完I/O后,業務一般還會有自己的業務邏輯,有的還
					// 會有其他的阻塞I/O,如DB操作,RPC等。只要有阻塞,就需要單獨的線程。
					handleInput(key); // 所以在這里其實最好是用其它的線程來處理,而不要影響了事件分發器線程
				} catch (Exception e) {
					if (key != null) {
						key.cancel();
						if (key.channel() != null)
							key.channel().close();
					}
				}
			}
		} catch (Throwable t) {
			t.printStackTrace();
		}
	}

	// 多路復用器關閉后,所有注冊在上面的Channel和Pipe等資源都會被自動去注冊並關閉,所以不需要重復釋放資源
	if (selector != null)
		try {
			selector.close();
		} catch (IOException e) {
			e.printStackTrace();
		}
}

private void handleInput(SelectionKey key) throws IOException {

	if (key.isValid()) {
		
		// 處理新接入的請求消息
		if (key.isAcceptable()) {
			// 接受一個新的客戶端接入請求
			ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
			SocketChannel sc = ssc.accept();
			// 設置客戶端為異步非阻塞
			sc.configureBlocking(false); 
			// Add the new connection to the selector
			sc.register(selector, SelectionKey.OP_READ);
		}
		if (key.isReadable()) {
			/*
			 * 讀取數據
			 * 讀取到的字節數,返回值有以下有三種結果:
			 * (1)大於0,讀取到字節,對其進行解編碼
			 * (2)等於0,沒有讀取到字節,南紡股份正常場景,忽略
			 * (3)-1 ,鏈路已經關閉,需要關閉SocketChannel,釋放資源
			 */
			SocketChannel sc = (SocketChannel) key.channel();
			ByteBuffer readBuffer = ByteBuffer.allocate(1024);
			// 由於設置了SocketChannel為異步非阻塞的,所以它的read是非阻塞的
			int readBytes = sc.read(readBuffer);
			if (readBytes > 0) {
				/*
				 * 將緩沖區當前的limit設置為position,position為0,用於后續對緩沖區的讀取操作。
				 * 然后根據緩沖區可讀的字節個數創建字節數組,調用get()操作將緩沖區可讀的字節數
				 * 組復制到新創建的字節數組中
				 */
				readBuffer.flip();
				byte[] bytes = new byte[readBuffer.remaining()];
				readBuffer.get(bytes);
				String body = new String(bytes, "UTF-8");
				System.out.println("The time server receive order : " + body);
				String currentTime = "QUERY TIME ORDER".equalsIgnoreCase(body) ? 
						new java.util.Date(System.currentTimeMillis()).toString() : "BAD ORDER";
				doWrite(sc, currentTime);
			} else if (readBytes < 0) {
				// 對端鏈路關閉
				key.cancel();
				sc.close();
			} else{
				; // 讀到0字節,忽略
			}
				
		}
	}
}

private void doWrite(SocketChannel channel, String response)throws IOException {
	/*
	 * 由於SocketChannel是異步非阻塞的,並不能保證一次能夠把所有需要發送的數據發送,此時會出現寫半包問題。
	 * 需要注冊寫操作???,不斷輪詢Selector將沒有發送完的bytebuffer發送完畢。可以通過byteBuffer的hasRemain()
	 * 方法判斷是否發送完畢。
	 */
	if (response != null && response.trim().length() > 0) {
		// 將應答消息異步發送給客戶端
		byte[] bytes = response.getBytes();
		ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length);
		// 將緩沖區中的字節數據發送
		writeBuffer.put(bytes);
		writeBuffer.flip(); // 緩存區復位
		channel.write(writeBuffer);
	}
}
}

  

Selector輪詢是阻塞的,而真正的I/O是異步非阻塞的。

對於NIO來說,緩存可以使用DirectByteBuffer和HeapByteBuffer。如果使用了DirectByteBuffer,一般來說可以減少一次系統空間到用戶空間的拷貝。但Buffer創建和銷毀的成本更高,更不宜維護,一般用來讀取大文件時使用。

 

參考文章:http://blog.csdn.net/szzt_lingpeng/article/details/50612018  

 

2、Java Reactor模式 異步非阻塞IO

下圖是Reactor的多線程模型

  

 

其特點如下:

(1)有專門一個NIO線程-Acceptor線程用於監聽服務端,接收客戶端的TCP連接請求

(2)網絡IO操作-讀、寫等由一個NIO線程池負責,線程池可以采用標准的JDK線程池實現,它包含一個任務隊列和N個可用的線程,由這些NIO線程負責消息的讀取、解碼、編碼和發送。

(3)一個NIO線程可以同時處理N條鏈路,但是一個鏈路只對應一個NIO線程,防止發生並發操作問題。

下圖是主從Reactor的多線程模型。

由於單獨一個Acceptor線程可能會存在性能不中的問題,所以需要主從Reactor模型。

服務端用於接收客戶端的不再是一個單獨的NIO線程,而是一個獨立的NIO線程池。 

 

3、NIO中, 如果不顯式的調用System.gc()那會出現什么問題?

DirectBuffer是分配在操作系統的內存中的,所以省去了應用程序到內核空間的拷貝,而HeapBuffer是分配到堆上的,所以便於垃圾回收。

DirectBuffer的GC規則與堆對象的回收規則是一樣的,只有垃圾對象才會被回收,而判定是否為垃圾對象依然是根據引用樹中的存活節點來判定。

如果DirectByteBuffer的空間夠用,那么System.gc()是不會觸發FullGC的。也就是說在空間不夠用時,顯示調用才能進行回收,如果不顯式調用,那只能是拋出內存異常了。

在垃圾收集時,雖然虛擬機會對DirectMemory進行回收,但是DirectMemory卻不像新生代和老年代那樣,發現空間不足了就通知收集器進行垃圾回收,它只能等待老年代滿了后FullGC,然后“順便地”幫它清理掉內存中廢棄的對象。否則,只能等到拋出內存溢出異常時,在catch塊里調用System.gc()。

參考:http://blog.csdn.net/donsonzhang/article/details/46666353 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 


注意!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系我们删除。



 
粤ICP备14056181号  © 2014-2021 ITdaan.com