【java】淺談NIO

一、什麼是NIO

1.概念

NIO是java1.4中引入的,被稱爲new I/O,也有說是non-blocking I/O。non-blocking I/O是包含在new I/O裏面的,而我們大多數講的NIO都是網絡編程裏的概念,但是我們今天主要講的是數據處理的這一塊的內容。

2.跟IO流的區別

  1. IO是面向流的,NIO是面向塊(緩衝區)的
  2. IO的流都是同步阻塞的,而NIO同步非阻塞的
  3. 在IO中也存在緩衝流,這和NIO的緩衝的策略有些不同,在NIO中的緩衝是可以稍後再進行處理的,還可以移動指針來處理想要處理的部分,而IO中的緩衝僅僅是在讀取和寫入時先存入緩存,緩存滿了之後就會必須要被處理
  4. NIO有選擇器,而IO沒有

3.什麼情況下使用NIO

二、如何使用

這裏以文件複製爲例

1.代碼

public class test {
    public static void main(String[] args){
        try{
            File inFile=new File("C:\\Users\\Administrator\\Desktop\\study.PNG");
            File outFile=new File("C:\\Users\\Administrator\\Desktop\\study1.PNG");
            FileInputStream fileInputStream=new FileInputStream(inFile);
            FileOutputStream fileOutputStream=new FileOutputStream(outFile);
            /**
             * RandomAccessFile accessFile=new RandomAccessFile(inFile,"wr");
             *  FileChannel inFileChannel=accessFile.getChannel();
             *  和下面兩行代碼是一樣的,都是可以拿到FileChannel
             */
            FileChannel inFileChannel=fileInputStream.getChannel();
            FileChannel outFileChannel=fileOutputStream.getChannel();

            ByteBuffer buffer=ByteBuffer.allocate(1024*1024);

            while (inFileChannel.read(buffer)!=-1){
                buffer.flip();
                outFileChannel.write(buffer);
                buffer.clear();
            }
            inFileChannel.close();
            outFileChannel.close();
            fileInputStream.close();
            fileOutputStream.close();
        }catch (Exception e){}

    }
}

我的桌面上的確多了一張一模一樣的圖片

2.解釋

使用NIO的話,需要注意幾個步驟:

  1. 打開流
  2. 獲取通道
  3. 創建Buffer
  4. 切換到讀模式 buffer.flip()
  5. 切換到寫模式 buffer.clear();
    其實這裏也看不出來它是怎麼使用緩衝區的,上面這段代碼中的while循環的作用和下面的代碼是一樣的
 while ((i=fileInputStream.read())!=-1){
                fileOutputStream.write(i);
          }

那我們來檢驗一下它們的性能吧

3.IO和NIO的性能區別

代碼

我整理了一下代碼,把複製文件的功能都整合在方法裏了,只需要傳入兩個文件對象的就可以了

 private static void NIOTest(File inFile,File outFile) {
        long startTime = System.currentTimeMillis();
        try{
            //創建文件流
            FileInputStream fileInputStream=new FileInputStream(inFile);
            FileOutputStream fileOutputStream=new FileOutputStream(outFile);
            //獲取通道
            FileChannel inFileChannel=fileInputStream.getChannel();
            FileChannel outFileChannel=fileOutputStream.getChannel();
            //開闢緩衝區,設置緩衝區大小
            ByteBuffer buffer=ByteBuffer.allocate((int)inFile.length());
            //讀取
            while (inFileChannel.read(buffer)!=-1){
            //寫入
                buffer.flip();
                outFileChannel.write(buffer);
                buffer.clear();
            }
            //關閉通道和流
            inFileChannel.close();
            outFileChannel.close();
            fileInputStream.close();
            fileOutputStream.close();
        }catch (Exception e){}
        System.out.println("NIO: "+(System.currentTimeMillis()-startTime)+"ms");
    }
   private static void IOTest(File inFile,File outFile) {
        long startTime = System.currentTimeMillis();
        try{
            FileInputStream fileInputStream=new FileInputStream(inFile);
            FileOutputStream fileOutputStream=new FileOutputStream(outFile);
            int i=0;
            byte[] bytes=new byte[(int)inFile.length()];
            while (fileInputStream.read(bytes)!=-1){
                fileOutputStream.write(bytes);
            }
            fileInputStream.close();
            fileOutputStream.close();
        }catch (Exception e){}
        System.out.println("IO: "+(System.currentTimeMillis()-startTime)+"ms");
    }

結果

IO: 2ms
NIO: 5ms

這個文件才一百k,換個大的文件試一下,這裏我用了一個六十多M的壓縮包

IO: 712ms
NIO: 737ms

結論

根據我的多次實驗,發現,在開闢的緩存區大小一樣的情況下,NIO並不比IO快
但是,當在處理比較大的文件時,緩存區的大小設置爲‘文件大小/8~64’時,NIO比IO快,當然這個範圍也不是準確的,有興趣的可以一個一個去測試,看看那個比較快
而在處理比較小的文件時,無論緩存區的大小設置爲多少,NIO都比IO慢
我是根據測試結果來做的的總結,可能文件再大一點就會不一樣了,如果有大佬做了其他測試,得出了新結論的話,可以告訴我,我把結論改一下。

4.selector的使用

只有使用套接字(ServerSocketChannel/SocketChannel)才能真正發揮NIO的非阻塞,但是,我對套接字這些不是很懂,就只好貼個別人寫好的客戶機和服務器的網址了
NIO的Selector介紹和例子代碼
就不獻醜了

步驟

在這裏插入圖片描述
畫了個圖來表示,這是關於selector的配置流程,在循環中根據不同key值所進行的操作,跟上面文件複製的例子差不多了,只不過這裏的Channel是通過 key.channel()獲得

三、重要的一個接口和兩個抽象類

在NIO中,有一個接口和兩個抽象類是我們需要重點了解的,ChannelBufferSelector

1.Channel

Channel跟stream差不多,但是Channel是雙向的(可以通過transferTo()或transferFrom()來改變數據的流向)

接口源碼

public interface Channel extends Closeable {
	//Channel接口只有兩個抽象方法
	//需要實現功能:檢查是否開啓
    public boolean isOpen();
    //需要實現功能:關閉通道
    public void close() throws IOException;

}

主要抽象類

這幾個類依然是沒有實現,它們的實現都是類名後加上Impl,如果想深入研究的話,就去看實現類吧,我這裏就只簡單說明一下它們的抽象類

  1. FileChannel
    這個就是文件通道了,上面的代碼中也有使用到,創建方式就不多說了。它的主要功能就是與文件進行數據傳輸
  2. DatagramChannel
    Datagram就是數據報的意思,學過網絡編程的應該都知道,數據報通道就是可以通過UDP在網絡上讀取和寫入數據,它的創建方法:
DatagramChannel ch = DatagramChannel.open();
  1. SocketChannel
    Socket就是套接字,套接字通道可以通過TCP在網絡上讀取和寫入數據,它的創建方法:
SocketChannel ch = SocketChannel.open(); 
//使用它還需要綁定一個ip地址
  1. ServerSocketChannel
    ServerSocketChannel是服務器套接字通道能夠監聽TCP的連接,在註冊到selector後,當selector的迭代器遍歷到相應的ServerSocketChannel時,它會根據SelectionKey獲取到ServerSocketChannel創建的SocketChannel,然後進行操作

2.Buffer

Buffer就是在進行NIO時開闢緩衝區的對象

抽象類源碼

內容太多了,簡單易懂的我就跳過了

public abstract class Buffer {
    static final int SPLITERATOR_CHARACTERISTICS =
        Spliterator.SIZED | Spliterator.SUBSIZED | Spliterator.ORDERED;
	// Invariants: mark <= position <= limit <= capacity
	//這四個數是整個Buffer的關鍵
    private int mark = -1;//標記,可以通過這個標記回溯到標記的位置
    private int position = 0;//當前位置,讀和寫的操作都是從這個位置開始的
    private int limit;//界限,讀和寫都不能超過這個界限
    private int capacity;//容量,能夠寫入的個數,以具體實現類爲準,例如IntBuffer,最大就能寫capacity個int
    long address;//地址,這是指向緩衝區的地址

    Buffer(int mark, int pos, int lim, int cap) {       
        if (cap < 0)
            throw new IllegalArgumentException("Negative capacity: " + cap);
        this.capacity = cap;
        limit(lim);
        position(pos);
        if (mark >= 0) {
            if (mark > pos)
                throw new IllegalArgumentException("mark > position: ("+ mark + " > " + pos + ")");
            this.mark = mark;
        }
    }
    public final int capacity() {
        return capacity;
    }
    public final int position() {
        return position;
    }
    //這是重新定義position,如果大於limit或小於0則報錯
    //如果mark大於新的position,則丟去mark
    public final Buffer position(int newPosition) {
        if ((newPosition > limit) || (newPosition < 0))
            throw new IllegalArgumentException();
        position = newPosition;
        if (mark > position) mark = -1;
        return this;
    }
    public final int limit() {
        return limit;
    }
    //重新定義limit,如果limit大於capacity或小於0,則報錯
    //如果position大於新的limit,則重新定位到limit
    //如果mark大於新的limit,則丟棄
    public final Buffer limit(int newLimit) {
        if ((newLimit > capacity) || (newLimit < 0))
            throw new IllegalArgumentException();
        limit = newLimit;
        if (position > limit) position = limit;
        if (mark > limit) mark = -1;
        return this;
    }
    public final Buffer mark() {
        mark = position;
        return this;
    }
    //將position重新定位到mark上
    public final Buffer reset() {
        int m = mark;
        if (m < 0)
            throw new InvalidMarkException();
        position = m;
        return this;
    }
    //俗稱的切換到寫模式
    public final Buffer clear() {
        position = 0;
        limit = capacity;
        mark = -1;
        return this;
    }
    //俗稱的切換到讀模式
    public final Buffer flip() {
        limit = position;
        position = 0;
        mark = -1;
        return this;
    }
    //這個也可以用來切換到讀模式,但是沒有修改limit
    public final Buffer rewind() {
        position = 0;
        mark = -1;
        return this;
    }
    public final int remaining() {
        return limit - position;
    }
    public final boolean hasRemaining() {
        return position < limit;
    }
    //需要實現的功能:
    //返回這個buffer是否只可讀
    public abstract boolean isReadOnly();
    //需要實現的功能:
    //返回這個buffer的緩衝區,是否是一個可讀的數組構成的
    public abstract boolean hasArray();
    //需要實現的功能:
    //如果這個buffer緩衝區的數組不是隻可讀,就返回一個數組
    public abstract Object array();
    //需要實現的功能:
    //如果這個buffer緩衝區的數組不是隻可讀,返回數組的地址
    public abstract int arrayOffset();
    //需要實現的功能:
    //判斷緩衝區是否是直接緩衝區,也就是是否存儲在物理內存中,而不是存儲在JVM中
    public abstract boolean isDirect();
    final int nextGetIndex() {                          
        if (position >= limit)
            throw new BufferUnderflowException();
        return position++;
    }
    //將position後移nb個
    final int nextGetIndex(int nb) {                    
        if (limit - position < nb)
            throw new BufferUnderflowException();
        int p = position;
        position += nb;
        return p;
    }
    //僅是拋出的異常不同
    //上面是下溢異常,下面是上溢異常
    final int nextPutIndex() {                         
        if (position >= limit)
            throw new BufferOverflowException();
        return position++;
    }
    final int nextPutIndex(int nb) {                    
        if (limit - position < nb)
            throw new BufferOverflowException();
        int p = position;
        position += nb;
        return p;
    }
    //檢查是否能夠將position定位到i上
    final int checkIndex(int i) {                       
        if ((i < 0) || (i >= limit))
            throw new IndexOutOfBoundsException();
        return i;
    }
    //檢查是否能夠將position定位到i+nb上
    final int checkIndex(int i, int nb) {              
        if ((i < 0) || (nb > limit - i))
            throw new IndexOutOfBoundsException();
        return i;
    }
    final int markValue() {                            
        return mark;
    }
    //清空
    final void truncate() {                            
        mark = -1;
        position = 0;
        limit = 0;
        capacity = 0;
    }
    final void discardMark() {                         
        mark = -1;
    }
    //檢查開始位置,長度,尺寸是否符合條件
    static void checkBounds(int off, int len, int size) { // package-private
        if ((off | len | (off + len) | (size - (off + len))) < 0)
            throw new IndexOutOfBoundsException();
    }

}

主要抽象類

主要的抽象類有七個,ByteBuffer、CharBuffe、DoubleBuffer、FloatBuffer、 IntBuffer、 LongBuffer、 ShortBuffer
分別對應基本數據類型: byte, char, double, float, int, long, short。
它們也都是沒有實現的,它們的實現類就更加多樣化了,這裏就不展開講述了,功能基本上都是差不多的,只是對應的數據類型不一樣

3.Selector

Selector被稱爲選擇器,也被稱爲多路複用器,是整個NIO中最核心的組件(個人覺得,不適用Selector就沒有必要使用NIO了)
Channel能夠通過register()把自己註冊到Selector中並設置一個SelectionKey,然後Selector根據SelectionKey的迭代器不斷的循環,當有符合某個key時,就可以進行操作了,操作時根據SelectionKey就可以獲取到對應的Channel了。

selector
channel
channel
channel
channel

抽象類源碼


public abstract class Selector implements Closeable {
    protected Selector() { }
    //這是通過SelectorProvider的內部靜態對象provider創建一個Selector的實現類實例
    //最後的實例對象是WindowsSelectorImpl的實例
    public static Selector open() throws IOException {
        return SelectorProvider.provider().openSelector();
    }
    public abstract boolean isOpen();
	//需要實現的功能:
	//返回一個通道的創建者
    public abstract SelectorProvider provider();
    //需要實現的功能:
	//返回選擇器的鍵集,該鍵集不能被修改
    public abstract Set<SelectionKey> keys();
    //需要實現的功能:
	//返回選擇器的鍵集,該鍵集可以被修改
    public abstract Set<SelectionKey> selectedKeys();
	//需要實現的功能:
	//開始監聽通道,如果沒有通道是已經準備好的,就返回0
    public abstract int selectNow() throws IOException;
	//需要實現的功能:
	//開始監聽通道,設置一個超時時間
    public abstract int select(long timeout)
        throws IOException;
	//需要實現的功能:
	//開始監聽通道
    public abstract int select() throws IOException;
    //需要實現的功能:
	//喚醒那些阻塞在select方法上的線程
	//在實現類WindowsSelectorImpl中,有一個線程數組,應該是喚醒這個數組中的線程
    public abstract Selector wakeup();
    public abstract void close() throws IOException;

}

最終的實現類是WindowsSelectorImpl

四、總結

使用NIO比使用IO繁瑣,除開Selector,NIO與IO之間不同的就是緩衝區有所不同IO的緩衝區就是一個單純的數組,而NIO的緩衝區則有很多定位的屬性,操作起來也比IO的數組麻煩
而Selector則讓NIO實現了多路複用的功能,通過SelectorKey的迭代器就可以實現對多個Channel的不斷輪詢,從而實現多路複用

最後,我也有一個問題,一直沒有搞懂,就是NIO的非阻塞到底體現在哪裏?
是它底層的實現和IO流的不一樣?還是因爲有了Selector?
——————————————————————————————
如果本文章內容有問題,請直接評論或者私信我。
未經允許,不得轉載!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章