1. 程式人生 > >深入理解Java-NIO

深入理解Java-NIO

JDK1.4的java.nio.*包中引入了新的Java-IO類庫,目的是提高速度,實際上,舊的IO包也已經使用nio重新實現過,以便充分利用這種速度提高。速度的提高在檔案IO和網路IO。速度的提高來自於所使用的結構更接近於作業系統執行IO的方式:通道和緩衝器。舊IO中有三個類被修改了,分別是FileInputStream、FileOutputStream和RandomAccessFile,用於產生FileChannel。而Reader和Writer這種字元模式類不能用於產生通道,但是java.nio.channels.Channels類提供了實用方法,用以在通道中產生Reader和Writer。

NIO基於Channel(通道)和Buffer(緩衝區)進行操作,資料總是從通道讀取到緩衝區中,或者緩衝區寫入到通道中。Selector(選擇區)用於監聽多個通道的事件(比如連線開啟、資料到達 ),因此單個執行緒可以監聽多個數據通道。

NIO和IO最大的區別就是IO是面向流的,NIO面向緩衝區的。IO的各種流是阻塞的,這意味著,當一個執行緒呼叫read和writer方法時,該執行緒將被阻塞,直到有一些資料被讀取,或者資料完全被寫入,該執行緒在此期間不能再幹任何事。NIO是非阻塞模式,使一個執行緒從某通道傳送請求讀取資料,但是它僅能得到目前可用資料,如果目前沒有資料可用,就什麼都不會獲取,而不是保持執行緒阻塞,所以在變得資料可以獲取之前,執行緒可以先繼續做其他事情,非阻塞寫也是如此,一個執行緒請求寫入一些資料到某通道,但不需要等待它完全寫完,這個執行緒同時也可以去做其他事情,執行緒通常將非阻塞IO的空閒時間用於其他通道上執行IO操作,所以一個單獨的執行緒現在可以管理多個輸入和輸出通道(channel)。IO是單執行緒的,而NIO是用選擇器來模擬多執行緒的。

Channel的實現:

1.FileChannel:從檔案中讀寫資料。

2.DatagramChannel:能通過UDP讀寫網路中的資料。

3.SocketChannel:能通過TCP讀寫網路中的資料。

4.ServerSocketChannel:可以監聽新近來的TCP協議,像Web伺服器一樣。對每個新進來的連線都會建立一個SocketChannel。

示例:

	 public static void main(String[] args) throws IOException {
		 File file = new File("data.txt");
		 FileOutputStream outputStream = new FileOutputStream(file);
		 FileChannel channel = outputStream.getChannel();
		 ByteBuffer buffer = ByteBuffer.allocate(1024);
		 String string = "java nio";
		 buffer.put(string.getBytes());
		 buffer.flip(); //此處必須要呼叫buffer的flip方法
		 channel.write(buffer);
		 channel.close();
		 outputStream.close();
		 } e.WriteLine("具體裝飾物件A的操作");
		}
	}

Buffer:

buffer的實現包括ByteBuffer、CharBuffer、DoubleBuffer、FloatBuffer、IntBuffer、LongBuffer、ShortBuffer、MapperByteBuffer。

Buffer類有幾個重要的屬性:

1.capacity:作為一個記憶體快,其就代表了當前Buffer能最多暫存多少資料量,儲存的資料型別則是根據上面的Buffer物件型別,一旦Buffer滿了,需要將其清空(通過讀資料或者清除資料)才能繼續寫資料。

2.position:代表當前資料讀或者寫出於哪個位置,讀模式:被重置從0開始,最大值可能為capacity-1或者limit-1,寫模式:被重置從0開始,最大值limit-1

3.limit:讀模式下:最多能往Buffer裡讀多少資料,寫模式下:最多能往Buffer裡寫多少資料,limit大小跟數量大小和capacity有關

示例:

public class ff {

    public static void nio() {
        RandomAccessFile aFile = null;
        try {

            aFile = new RandomAccessFile("src/nio.txt", "rw");
            // channel獲取資料
            FileChannel fileChannel = aFile.getChannel();
            // 初始化Buffer,設定Buffer每次可以儲存資料量
            // 建立的Buffer是1024byte的,如果實際資料本身就小於1024,那麼limit就是實際資料大小
            ByteBuffer buf = ByteBuffer.allocate(1024);
            // channel中的資料寫入Buffer
            int bytesRead = fileChannel.read(buf);
            System.out.println(bytesRead);

            while (bytesRead != -1) {
                // Buffer切換為讀取模式
                buf.flip();
                // 讀取資料
                while (buf.hasRemaining()) {
                    System.out.print((char) buf.get());
                }
                // 清空Buffer區
                buf.compact();
                // 繼續將資料寫入快取區
                bytesRead = fileChannel.read(buf);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (aFile != null) {
                    aFile.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    public static void main(String[] args) {
    	ff.nio();
    }

}

Buffer讀寫資料步驟:

1.寫入資料到Buffer(fileChannel.read(buf))

2.呼叫flip()方法

3.從Buffer中讀取資料

4.呼叫clear()方法或者compact方法

Buffer的方法:

flip():將Buffer寫模式切換到讀模式,比且將position置為0.

clear():清除整個緩衝區

compact():只會清除已經讀過的資料,任何未讀的資料都被轉移到緩衝區起始處,新寫入的資料將放到緩衝區未讀資料的後面。

allocate(1024):初始化Buffer,設定的值就是capacity的大小。

rewind():position設回0,所以你可以重讀Buffer中的所有資料。limit保持不變,仍然表示能從Buffer中讀取多少個元素。

mark()和reset():通過呼叫Buffer.mark()方法,可以標記Buffer中的一個特定的position。之後可以通過呼叫Buffer,reset()方法恢復到這個position

equals():兩個相等的Buffer,滿足相同型別,剩餘的元素數量相等,所剩餘的元素也都相同。

Selector:

允許單執行緒處理多個Channel。如果你的應用打開了多個連線(通道),但是每個連線的流量很低,使用Selector就會很方便。

Selector的建立:

    通過呼叫Selector.open方法建立一個Selector,如下:

	Selector selector = Selector.open();

向Selector註冊通道:

為了將Channel和Selector配合使用,必須將channel註冊到selector上。通過SelectableChannel.register()方法來實現。如下:

	channel.configureBlocking(false);
	SelectionKey key = channel.register(selector,Selectionkey.OP_READ);

與Selector一起使用,Channel必須出於非阻塞模式下。這意味著不能將FileChannel與Selector一起使用,因為FileChannel不能切換到非阻塞模式。而套接字通道都可以。注意register()方法的第二個引數。這是一個“interest集合”,意思是在通過Selector監聽Channel時對什麼事件感興趣。可以監聽四種不同型別的事件:1.Connect,2.Accept。3.Read。4.Write。通道出發了一個事件意思是該事件已經準備就緒。所以某個channel成功連線到另一個伺服器稱為“連線就緒”。一個server socket channel準備好接收新進入的連線稱為“接收就緒”。一個有資料可讀的通道就可以說是“讀就緒”。等待寫資料的通道可以說“寫就緒”。上面四種事件對應的SelectionKey的四個常量來表示:1.SelectionKey.OP_CONNECT。2.SelectionKey.OP_ACCEPT。3.SelectionKey.OP_READ。4.SelectionKey.OP_WRITE。還可以用“位或”操作符把多個事件連起來。如下:

	int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;

Selector註冊了Channel時,register()方法會返回一個SelectionKey物件。這個物件包含了一些你感興趣的屬性:interest集合、ready集合、Channel、Selector、附加的物件(可選)

interest集合:是你所選擇的感興趣的事件集合。可以通過SelectionKey讀寫interest集合,就像這樣:

	int interestSet = selectionKey.interestOps();
	boolean isInterestedInAccept  = (interestSet & SelectionKey.OP_ACCEPT) == SelectionKey.OP_ACCEPT;
	boolean isInterestedInConnect = interestSet & SelectionKey.OP_CONNECT;
	boolean isInterestedInRead    = interestSet & SelectionKey.OP_READ;
	boolean isInterestedInWrite   = interestSet & SelectionKey.OP_WRITE;

可以看到,用“位與”操作interest集合和給定的SelecctionKey常量,可以確定某個確定的事件是否在interest集合中。

ready集合:是通道已經準備就緒的操作的集合。在一次選擇後,你會首先訪問這個ready set。Selection將在下一小節進行解釋。可以這樣訪問ready集合:

	int readySet = selectionKey.readyOps();

可以用像檢測interest集合那樣的方法,來檢測channel中什麼時間或操作已經就緒:

	selectionKey.isAcceptable();
	selectionKey.isConnectable();
	selectionKey.isReadable();
	selectionKey.isWritable();

訪問channel和selector:

	Channel  channel  = selectionKey.channel();
	Selector selector = selectionKey.selector();

附加物件:

可以將一個物件或者更多的資訊附著在SelectionKey上,這樣就能方便的識別某個給定的通道。例如,可以附加與通道一起使用的Buffer,或者包含聚集資料的某個物件,使用方法如下:

	selectionKey.attach(theObject);
	Object attachedObj = selectionKey.attachment();
	//還可以在用register()方法向Selector註冊Channel的時候附加物件
	SelectionKey key = channel.register(selector, SelectionKey.OP_READ, theObject);

通過Selector選擇通道:

一旦向Selector註冊了一或多個通道,就可以呼叫幾個過載的select()方法。這些方法返回你所感興趣的事件已經準備就緒的那些通道。換句話說,如果你對“讀就緒”的通道感興趣,select()方法會返回讀事件已經就緒的那些通道。

下面是select方法:

select()阻塞到至少有一個通道在你註冊的事件上就緒。

select(long timeout)和select()一樣,除了最長會阻塞timeout毫秒

selectNow()不會阻塞,不管什麼通道就緒都立刻返回。

select返回的int值表示有多少通道就緒,即自從上次呼叫select方法後有多少通道程式設計就緒狀態。

一旦呼叫了select方法,並且返回值表明由一個或更多個通道就緒了,然後可以通過呼叫selector的selectedKey方法,訪問“已經選擇鍵集”中的就緒通道:

	Set selectedKeys = selector.selectedKeys();
	Iterator keyIterator = selectedKeys.iterator();
	while(keyIterator.hasNext()) {
	    SelectionKey key = keyIterator.next();
	    if(key.isAcceptable()) {
	        // a connection was accepted by a ServerSocketChannel.
	    } else if (key.isConnectable()) {
	        // a connection was established with a remote server.
	    } else if (key.isReadable()) {
	        // a channel is ready for reading
	    } else if (key.isWritable()) {
	        // a channel is ready for writing
	    }
	    //每次迭代末尾的keyIterator.remove()呼叫。
	    //Selector不會自己從已選擇鍵集中移除SelectionKey例項。必須在處理完通道時自己移除。
	    //下次該通道變成就緒時,Selector會再次將其放入已選擇鍵集中。
	    keyIterator.remove();
	}

wakeUp():某個執行緒呼叫select方法後阻塞,即使沒有通道就緒,也有辦法讓其select方法返回。只要放其他執行緒在第一個執行緒呼叫select方法的那個物件上呼叫Selector.wakeUp()方法即可,阻塞在select方法上的執行緒會立馬返回。如果其他執行緒呼叫了wakeUp方法,但當前沒有執行緒阻塞在select方法上,下個調研select方法的執行緒會立即“醒來”。

close():用完Selector後呼叫其close關閉Selector,且使註冊到該Selector上的所有SelectionKey例項無效。通道本身不會關閉。

以下是一個完整的示例:

	Selector selector = Selector.open();
	channel.configureBlocking(false);
	SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
	while(true) {
	  int readyChannels = selector.select();
	  if(readyChannels == 0) continue;
	  Set selectedKeys = selector.selectedKeys();
	  Iterator keyIterator = selectedKeys.iterator();
	  while(keyIterator.hasNext()) {
	    SelectionKey key = keyIterator.next();
	    if(key.isAcceptable()) {
	        // a connection was accepted by a ServerSocketChannel.
	    } else if (key.isConnectable()) {
	        // a connection was established with a remote server.
	    } else if (key.isReadable()) {
	        // a channel is ready for reading
	    } else if (key.isWritable()) {
	        // a channel is ready for writing
	    }
	    keyIterator.remove();
	  }
	}