1. 程式人生 > >Java NIO介紹(二)————無堵塞io和Selector簡單介紹

Java NIO介紹(二)————無堵塞io和Selector簡單介紹

無堵塞IO介紹 既然NIO相比於原來的IO在讀取速度上其實並沒有太大區別(因為NIO出來後,IO的低層已經以NIO為基礎重新實現了),那麼NIO的優點是什麼呢? NIO是一種同步非阻塞的I/O模型,也是I/O多路複用的基礎,而且已經被越來越多地應用到大型應用伺服器,成為解決高併發與大量連線、I/O處理問題的有效方式。 傳統的IO模型 讓我們先回憶一下傳統的伺服器端同步阻塞I/O處理(也就是BIO,Blocking I/O)的經典程式設計模型:
 ExecutorService executor = Excutors.newFixedThreadPollExecutor(100);//執行緒池

 ServerSocket serverSocket = new ServerSocket();
 serverSocket.bind(8088);
 while(!Thread.currentThread.isInturrupted()){//主執行緒死迴圈等待新連線到來
 Socket socket = serverSocket.accept();
 executor.submit(new ConnectIOnHandler(socket));//為新的連線建立新的執行緒
}

class ConnectIOnHandler extends Thread{
    private Socket socket;
    public ConnectIOnHandler(Socket socket){
       this.socket = socket;
    }
    public void run(){
      while(!Thread.currentThread.isInturrupted()&&!socket.isClosed()){死迴圈處理讀寫事件
          String someThing = socket.read()....//讀取資料
          if(someThing!=null){
             ......//處理資料
             socket.write()....//寫資料
          }

      }
    }

這是一個經典的每連線每執行緒的模型,之所以使用多執行緒,主要原因在於socket.accept()、socket.read()、socket.write()三個主要函式都是同步阻塞的,當一個連線在處理I/O的時候,系統是阻塞的,如果是單執行緒的話必然就掛死在那裡;但CPU是被釋放出來的,開啟多執行緒,就可以讓CPU去處理更多的事情。
其實這也是所有使用多執行緒的本質: 1、利用多核。 2、當I/O阻塞系統,但CPU空閒的時候,可以利用多執行緒使用CPU資源。 現在的多執行緒一般都使用執行緒池,可以讓執行緒的建立和回收成本相對較低。在活動連線數不是特別高(小於單機1000)的情況下,這種模型是比較不錯的,可以讓每一個連線專注於自己的I/O並且程式設計模型簡單,也不用過多考慮系統的過載、限流等問題。執行緒池本身就是一個天然的漏斗,可以緩衝一些系統處理不了的連線或請求。 不過,這個模型最本質的問題在於,嚴重依賴於執行緒。但執行緒是很"貴"的資源,主要表現在: 1、執行緒的建立和銷燬成本很高,在Linux這樣的作業系統中,執行緒本質上就是一個程序。建立和銷燬都是重量級的系統函式。 2、執行緒本身佔用較大記憶體,像Java的執行緒棧,一般至少分配512K~1M的空間,如果系統中的執行緒數過千,恐怕整個JVM的記憶體都會被吃掉一半。 3、執行緒的切換成本是很高的。作業系統發生執行緒切換的時候,需要保留執行緒的上下文,然後執行系統呼叫。如果執行緒數過高,可能執行執行緒切換的時間甚至會大於執行緒執行的時間,這時候帶來的表現往往是系統load偏高、CPU sy使用率特別高(超過20%以上),導致系統幾乎陷入不可用的狀態。 4、容易造成鋸齒狀的系統負載。因為系統負載是用活動執行緒數或CPU核心數,一旦執行緒數量高但外部網路環境不是很穩定,就很容易造成大量請求的結果同時返回,啟用大量阻塞執行緒從而使系統負載壓力過大。 所以,當面對十萬甚至百萬級連線的時候,傳統的BIO模型是無能為力的。隨著移動端應用的興起和各種網路遊戲的盛行,百萬級長連線日趨普遍,此時,必然需要一種更高效的I/O處理模型。 NIO模型(Reactor輪詢模式)
回憶BIO模型,之所以需要多執行緒,是因為在進行I/O操作的時候,一是沒有辦法知道到底能不能寫、能不能讀,只能"傻等",即使通過各種估算,算出來作業系統沒有能力進行讀寫,也沒法在socket.read()和socket.write()函式中返回,這兩個函式無法進行有效的中斷。所以除了多開執行緒另起爐灶,沒有好的辦法利用CPU。 NIO的讀寫函式可以立刻返回,這就給了我們不開執行緒利用CPU的最好機會: 如果一個連線不能讀寫(socket.read()返回0或者socket.write()返回0),我們可以把這件事記下來,記錄的方式通常是在Selector上註冊標記位,然後切換到其它就緒的連線(channel)繼續進行讀寫。 下面具體看下如何利用事件模型單執行緒處理所有I/O請求:

NIO的主要事件有幾個:讀就緒、寫就緒、有新連線到來。 用一個死迴圈選擇就緒的事件,會執行系統呼叫,還會阻塞的等待新事件的到來。新事件到來的時候,會在selector上註冊標記位,標示可讀、可寫或者有連線到來。 注意,select是阻塞的,無論是通過作業系統的通知還是不停的輪詢,這個函式是阻塞的。所以你可以放心大膽地在一個while(true)裡面呼叫這個函式而不用擔心CPU空轉。 服務端程式碼:
package nio3;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
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 NIOServer {
	
	private  int flag = 0;
	private  int BLOCK = 2048;
	private  ByteBuffer sendbuffer = ByteBuffer.allocate(BLOCK);
	private  ByteBuffer receivebuffer = ByteBuffer.allocate(BLOCK);
	private  Selector selector;

	public NIOServer(int port) throws IOException {
		ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
		serverSocketChannel.configureBlocking(false);
		ServerSocket serverSocket = serverSocketChannel.socket();
		serverSocket.bind(new InetSocketAddress(port));
		selector = Selector.open();
		serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
		System.out.println("Server Start----8888:");
	}

	private void listen() throws IOException {
		//輪詢  事件驅動模式
		while (true) {
			// select()阻塞,等待有事件發生喚醒 
			selector.select();
			Set<SelectionKey> selectionKeys = selector.selectedKeys();
			Iterator<SelectionKey> iterator = selectionKeys.iterator();
			while (iterator.hasNext()) {
				SelectionKey selectionKey = iterator.next();
				//處理事件後,要移除
				iterator.remove();
				handleKey(selectionKey);
			}
		}
	}

	/**
	 * 處理不同事件
	 */
	private void handleKey(SelectionKey selectionKey) throws IOException {
		ServerSocketChannel server = null;
		SocketChannel client = null;
		String receiveText;
		String sendText;
		int count=0;
		//連線事件
		if (selectionKey.isAcceptable()) {
			server = (ServerSocketChannel) selectionKey.channel();
			client = server.accept();
			client.configureBlocking(false);
			client.register(selector, SelectionKey.OP_READ);
			//讀取模式
		} else if (selectionKey.isReadable()) {
			client = (SocketChannel) selectionKey.channel();
			receivebuffer.clear();
			count = client.read(receivebuffer);	
			if (count > 0) {
				receiveText = new String( receivebuffer.array(),0,count);
				System.out.println("讀取到:"+receiveText);
				//註冊對寫的事件感興趣
				client.register(selector, SelectionKey.OP_WRITE);
			}
			//寫模式
		} else if (selectionKey.isWritable()) {
			sendbuffer.clear();
			client = (SocketChannel) selectionKey.channel();
			sendText="message from server--" + flag++;
			sendbuffer.put(sendText.getBytes());
			sendbuffer.flip();
			client.write(sendbuffer);
			System.out.println("向客戶端傳送了"+sendText);
//			client.register(selector, SelectionKey.OP_READ);
			client.close();
		}
	}

	public static void main(String[] args) throws IOException {
		NIOServer server = new NIOServer(9000);
		server.listen();
	}
}

客戶端程式碼(用普通的io寫的):
package nio;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.net.Socket;

public class MultiPortEcho2 {
	
	public static void main(String[] args) throws Exception{
		Socket socket = new Socket("localhost", 9000);
		
		DataOutputStream sendStream =  new DataOutputStream( socket.getOutputStream());
		DataInputStream receiveStream =  new DataInputStream( socket.getInputStream());
		System.out.println(receiveStream.available());
		sendStream.write("aasdaaa".getBytes());
		int line=-1;
		byte[] bytes=new byte[1024];
		while((line=receiveStream.read(bytes))!=-1){
			System.out.println(new String(bytes,0,line));
		}
		socket.close();
	}
	
}


以上程式碼是用了一個執行緒來處理不同的事件,當然我們可以在接受到讀事件或者寫時間的時候開啟多執行緒來進行讀寫的過程(在輪詢的時候已經確定都或者寫模式,因此在新開的執行緒中只是簡單的io讀寫操作,不會堵塞)。 優化執行緒模型 單執行緒處理I/O的效率確實非常高,沒有執行緒切換,只是拼命的讀、寫、選擇事件。但現在的伺服器,一般都是多核處理器,如果能夠利用多核心進行I/O,無疑對效率會有更大的提高。 仔細分析一下我們需要的執行緒,其實主要包括以下幾種: 1、事件分發器,單執行緒選擇就緒的事件。 2、I/O處理器,包括connect、read、write等,這種純CPU操作,一般開啟CPU核心個執行緒就可以。 3、業務執行緒,在處理完I/O後,業務一般還會有自己的業務邏輯,有的還會有其他的阻塞I/O,如DB操作,RPC等。只要有阻塞,就需要單獨的執行緒。 另外連線的處理和讀寫的處理通常可以選擇分開,這樣對於海量連線的註冊和讀寫就可以分發。雖然read()和write()是比較高效無阻塞的函式,但畢竟會佔用CPU,如果面對更高的併發則無能為力。 參考資料:Java NIO淺析 來自:美團點評技術團隊(微訊號:meituantech)