NIO 學習筆記(一)初遇

前言

NIO是什麼? 這個我仍是老習慣先去翻翻官方寫的指導書《The Java™ Tutorials》
而後《The Java™ Tutorials》只是介紹了基本操做,想了解更多的話,去 OpenJDK: NIO。而後我就在這個頁面找到了NIO的相關介紹。html

NIO的前世此生

NIO 意味 New I/O,主要來自於JSR 203JSR 51。JSR: Java Specification Requests java 規範提案。什麼意思呢?就是建議,就是對java的建議。而後JCP(Java Community Process)就會審議你的提案,若是你的提案得到經過,那麼提案就會進入到JDK中。java

這兩個提案都是建議增強java IO的接口,分紅兩個方面: 定義新的接口和加強過去的接口。JSR 203是 JSR 51的延續。JSR 51被稱爲NIO.1, JSR則被稱爲NIO.2。主要包括三個部分:segmentfault

可以訪問更多文件屬性和改變標記的和能夠避免去使用特定文件系統接口的新文件系統接口。數組

支持異步IO操做的接口,與輪詢相反,不阻塞。因此這部分的接口有時也被稱爲NIO。多線程

完善定義在JSR-51中socket-channel的功能,包括添加對綁定,選項配置,多播數據報的支持。架構

本篇咱們主要介紹的是異步IO,由於是非阻塞的,因此有時候也被稱做NIO(non-blocking i/o)oracle

爲何要引入異步IO呢?

讓咱們從CS架構提及,CS架構的一個典型應用就是聊天軟件,大體的結構像下面這樣。app

服務端用於傳遞客戶端的信息。在沒有異步IO以前,咱們的代碼是這樣寫的:dom

ServerSocket serverSocket = new ServerSocket(6666);
 Socket socket = serverSocket.accept();
 InputStream inputStream = socket.getInputStream();

請注意accept()方法是阻塞的,直到鏈接創建 。那麼比較有侷限的是,當前代碼只能處理一個客戶端,並且是出於一直等待狀態。直到鏈接創建,咱們才能處理客戶端發送過來的信息。想想,這麼設計也有必定的合理之處,服務端和客戶端的鏈接不創建,客戶端怎麼能發送信息呢? 那麼確定有人要問了,你這個服務端只能處理一個客戶端啊,由於你只有一個Socket啊。
那我就開多線程,一個線程處理一個鏈接。異步

那麼在java中常規狀況下,一個線程是要消耗1M內存的,這是一個比較大的消耗。這就是同步I/O的缺點,前面的事情不作完,後面的事情別想作。那麼天然會有人想,可否不要一直監聽啊,鏈接創建了,你在通知我,我再處理你發過來的信息。

這就是異步。這反應到聊天室這裏就是,不少客戶端跟服務端創建鏈接,可是總要有前後順序的,你跟我創建鏈接完成,我再處理該客戶發送過來的信息。這就是爲何要引入異步IO的緣由,本來的IO不夠靈活,消耗資源過多。

Buffer Channel Selector


Buffer(緩衝區)用來存數據,channel(通道)用來從Buffer中讀和寫數據,傳統的流是單向的,像InputStream和OutputStream的子類。那麼Selector(選擇器)呢? 當選擇器遇到它所感興趣的事情以後(好比鏈接創建完成), 就會激活通道,通道從緩衝區中取出客戶端發過來的信息。

Buffer

Buffer在java中就是一個抽象類,咱們能夠將其視做一個存儲特定基本類型數據的容器。在Buffer類中只定義了基本屬性和基本操做。咱們主要看它的八個子類:

  • ByteBuffer
  • IntBuffer
  • StringCharBuffer
  • CharBuffer
  • FloatBuffer
  • LongBuffer
  • ShortBuffer
  • DoubleBuffer

是的沒有BooleanBuffer。
這8個子類中,用來裝數據的都是數組。可是共有的屬性是在Buffer中被定義。

主要屬性

  • capacity 容量
  • limit 指向第一個不能寫或者不能讀
  • position 指向下一個將要被寫入或讀取的元素
  • mark 標記 reset 方法以後 position 指向mark
  • address 堆外內存地址 這個咱們稍候在講

主要操做

咱們主要以ByteBuffer爲例來介紹,Buffer的相關操做。對於容器來講咱們比較關心的就是兩個:

  • 讀 從容器中獲取元素 讀對應 get(byte[] dst) 將緩衝區中的元素放入傳入的字節數組中
  • 寫 向容器中放入元素 寫對應 put(byte[] src)方法 將src中的元素放入緩衝區中

咱們來藉助demo來講明Buffer的常規操做:

// 建立一個長度爲buffer
        ByteBuffer buffer = ByteBuffer.allocate(100); 
        // position = 0 
        System.out.println(buffer.position());
        // limit = 100 
        System.out.println(buffer.limit());
        // capacity = 100 
        System.out.println(buffer.capacity()); 
        // 向buffer,也就是數組中放入元素。一個字符對應數組的一個位置
        buffer.put("hello".getBytes());
        // position = 5
        System.out.println(buffer.position());
         // limit = 100 
        System.out.println(buffer.limit());
        // capacity = 100 
        System.out.println(buffer.capacity());

咱們來思考這麼一個問題,咱們取的時候,也是從緩衝區拿到東西,放入到一個新的容器中,直白點說就是get(byte[] dst)這個方法中的字節數組,咱們給多大比較合適。理想的作法一般是你這裏面有多少,我這個就取多少。position恰好就標識了此時元素的數量。這就是flip方法作的事情。

flip()和compact()這兩個好搭檔
// 從寫轉爲讀能夠用,此時limit爲Buffer的實際容量。
public final Buffer flip() {
        limit = position;
        position = 0;
        mark = -1;
        return this;
    }
buffer.flip();
    // postion = 0
    System.out.println(buffer.position());
    // limit是5
    System.out.println(buffer.limit());
    byte[] array = new byte[buffer.limit()];
    buffer.get(array);
    // 打印hello
    System.out.println(new String(array));

flip和compact經常配合在一塊兒使用。compact()的實際做用爲:

將buffer中position和limit之間的數據到Buffer的0位置和(limit-position-1),注意是[position,limit)->[0,limit-position-1]


常應用於兩個通道之間互相傳輸數據,也就是一個channel用來寫,一個channel用來讀。像下面這樣:

buf.clear();          // Prepare buffer for use
 while (in.read(buf) >= 0 || buf.position != 0) {
      buf.flip();
      out.write(buf);
      buf.compact();    // In case of partial write   
 }

有人這裏可能就會問了,上面你不是說position是下一個將要讀或者寫的元素的位置嗎? 此時position不是還沒值嗎?那你爲啥要複製呢? 由於Channel是非阻塞的,write()並不會將buffer中的數據所有寫入。像下面這樣:

rewind 和 reset、clear

其實這裏也就是介紹API的使用而已,能夠直接看Buffer類上的註釋的就能夠了。寫的話,通常就是從0位置開始寫了。
此時重置Buffer的limit、position、mark屬性便可。這也就是Buffer的clear()方法作的事情。

  • rewind()

rewind的源碼:

public final Buffer rewind() {
        position = 0;
        mark = -1;
        return this;
 }

在寫轉向讀的時候能夠調用, 也有稱爲讀操做到讀操做的。像下面這樣:

out.write(buf);    // Write remaining data
 buf.rewind();      // Rewind buffer
 buf.get(array);

前提是limit已經被合適的設置,那不是要調一下flip方法嗎?這個能夠應用於反覆的從buffer中獲取數據。

  • reset

reset的源碼:

public final Buffer reset() {
        int m = mark;
        if (m < 0)
            throw new InvalidMarkException();
        position = m;
        return this;
    }

reset 有重置的意思,這裏咱們能夠將其理解爲歸檔。默認狀況下: 調用以後position=mark。
mark方法用於標記position的位置。

  • clear

clear的源碼:

public final Buffer clear() {
        position = 0;
        limit = capacity;
        mark = -1;
        return this;
    }

一切從頭開始,在向緩衝區中放入元素以前要調用此方法。此時Buffer的狀態由讀狀態轉入寫狀態。

Channel

Channel自己不負責存儲數據,在讀或者寫的時候,都是從buffer中獲取。
Channel是一個接口,那咱們該如何使用呢,或者說如何獲取呢。
主要是兩種方式:

  • FileChannel的open方法
  • SocketChannel的open方法
  • ServerSocket的getChannel方法
  • DatagramChannel的open方法
  • 字節流的getChannel(緩衝流和字符流拿不到通道),經過這種方式拿到的都是單向的,channel自己是雙向的。

    • FileInputStream的getChannel();
    • FileOutputStream的getChannel();

使用Channel和Buffer實現文件的複製

咱們首先引入兩個概念: 直接緩衝區和非直接緩衝區。

//直接緩衝區 address 指向這塊地址
 ByteBuffer directByteBuffer = ByteBuffer.allocateDirect(1024);
 //非直接緩衝區
 ByteBuffer byteBuffer = ByteBuffer.allocate(1024);

這兩個有什麼區別呢?咱們知道內存從邏輯上能夠視爲一個字節數組。程序在成爲進程的時候,操做系統會分配給進程對應的資源,好比說從字節數組劃出一部分給進程,但並非真實的內存,是虛擬內存。非直接緩衝區在仍是在操做系統分配給JVM的內存中。還在JVM的管轄範圍以內,而直接緩衝區則是在JVM以外,向操做系統申請內存,因此這個方法也是native方法。

private static void studyChannel() throws IOException {
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        FileInputStream fileInputStream = new FileInputStream("D:基礎筆記PDF.zip");
        FileOutputStream fileOutStream = new FileOutputStream("D:基礎筆記PDF3.zip");
        FileChannel inChannel = fileInputStream.getChannel();
        FileChannel outChannel = fileOutStream.getChannel();
        long start = System.currentTimeMillis();
       while (inChannel.read(byteBuffer) != -1){
            byteBuffer.flip();
            outChannel.write(byteBuffer);
            byteBuffer.compact();
       }
        fileInputStream.close();
        fileOutStream.close();
        inChannel.close();
        outChannel.close();
        long end = System.currentTimeMillis();
        System.out.println(end - start);
    }

事實上這跟用流沒多大的差距, 是的你用直接緩衝區和非直接都是同樣的。都沒差多少,那說好的高效呢。其實NIO的確很高效。這麼用把他用低效了。
有關這種拷貝方式爲何這麼慢, 我在《操做系統與通用計算機組成原理簡論》
已經大體介紹過了,這裏咱們在複習一下,程序沒法直接接觸硬件,讓程序直接接觸硬件是系統不穩定的緣由,程序若是想訪問某個文件,那麼只能調用對應的操做系統的函數。

訪問硬件屬於特權指令,在執行指令的時候,CPU出於內核態,CPU使用一種稱爲內存映射I/O(memory-mapped I/O)的技術向I/O設備發射命令,假設磁盤控制器被映射到某個端口,隨後,CPU可能經過執行三個對地址0xa0的存儲指令,發起磁盤讀: 第一條指令是發送一個命令字,告訴磁盤發起一個讀,同時還發送了其餘的參數,例如當讀完成時,是否中斷CPU(若是你不懂什麼是中斷的話,不要緊,等着我)。第二條指令指明應該讀的邏輯塊號。第三條指令指明應該存儲磁盤山區內容的主存地址。

當CPU發出了請求以後,在磁盤執行讀的時候,CPU出於等待狀態,磁盤是很慢的,此時讓CPU一直陷入等待是一種極大的浪費。操做系統的CPU調度器一般會讓CPU去作其餘事情。這是加載進內存的操做,從內存寫磁盤,又是一陣等待,由於咱們的磁盤相對於CPU來講實在是太慢了。這個時候就是DMA出場的時候,在磁盤控制器收到來自CPU的讀命令以後,它將邏輯塊號翻譯成一個扇區地址,讀該扇區的內容,隨後將這些內容直接傳送到主存,不須要CPU的干涉。設備能夠本身執行讀或者寫總線事務而不須要CPU的過程,咱們稱爲直接內存訪問(Direct Memory Access,DMA)。這種數據傳統稱爲DMA傳送(DMA transfer)。


這個過程當中,CPU發送指令的時候,CPU出於內核態,在執行用戶程序的時候出於用戶態,CPU態頻繁的切換。每次讀寫都須要CPU的參與,可是你知道CPU太快了,出於等待的CPU會被分配到其餘線程,在切換回來,上下文切換。這就是這種IO慢的緣由。咱們可否藉助DMA技術呢,讓CPU只參與一次,磁盤控制器受到CPU的讀命令以後,磁盤控制器直接將邏輯塊號翻譯成一個扇區地址,讀該扇區的內容,隨後直接將內容傳送至主存。在CPU發送一次寫指令以後,磁盤控制器將主存的內容寫入磁盤中。能夠的。這也就是下面介紹的零拷貝。

long start = System.currentTimeMillis();
        FileChannel inChannel = FileChannel.open(Paths.get("D:基礎筆記PDF.zip"));
        // 輸出的時候要指明一下,channel的狀態。    
        FileChannel outChannel = FileChannel.open(Paths.get("D:基礎筆記PDF3.zip"), StandardOpenOption.WRITE,StandardOpenOption.CREATE_NEW,StandardOpenOption.READ);
        MappedByteBuffer inMappedByteBuffer = inChannel.map(FileChannel.MapMode.READ_ONLY, 0, inChannel.size());
        MappedByteBuffer outMappedyteBuffer = outChannel.map(FileChannel.MapMode.READ_WRITE, 0, inChannel.size());
        byte[] byteArray = new byte[inMappedByteBuffer.limit()];
        inMappedByteBuffer.get(byteArray);
        outMappedyteBuffer.put(byteArray);
        inChannel.close();
        outChannel.close();
        long end = System.currentTimeMillis();
        System.out.println(end - start);

基礎筆記PDF.zip大概是500多M,用流的話大概在6到7秒。用這種拷貝就是不到一秒。確實快。
第二種寫法:

long start = System.currentTimeMillis();
        FileChannel inChannel = FileChannel.open(Paths.get("D:基礎筆記PDF.zip"));
        FileChannel outChannel = FileChannel.open(Paths.get("D:基礎筆記PDF3.zip"), StandardOpenOption.WRITE,StandardOpenOption.CREATE_NEW,StandardOpenOption.READ);
        inChannel.transferTo(0,inChannel.size(),outChannel);
        // outChannel.transferFrom(inChannel,0,inChannel.size());
        long end = System.currentTimeMillis();
        System.out.println(end - start);

transferTo是將文件輸出到哪一個位置。
transferFrom是從哪一個位置讀,而後輸出到當前通道表明的位置。
MappedByteBuffer 是內存映射。操縱MappedByteBuffer對象會自動同步.
例子:

RandomAccessFile randomAccessFile = new RandomAccessFile("D:\\學習資料\\測試.txt","rw");
        FileChannel channel = randomAccessFile.getChannel();
        MappedByteBuffer map = channel.map(FileChannel.MapMode.READ_WRITE, 0, randomAccessFile.length());
        map.put(1,(byte)'q');
        channel.close();
相關文章
相關標籤/搜索