使用NIO提升性能


NIO是New I/O的簡稱,與舊式的基於流的I/O方法相對,從名字看,它表示新的一套Java I/O標准。

具有以下特性:

  傳統Java IO,它是阻塞的,低效的。那么Java NIO和傳統Java IO有什么不同?帶來了什么?

(1)面向塊的I/O

  傳統JavaIO是面向流的I/O。流I/O一次處理一個字節。NIO則是面向塊的I/O,每次操作都是以數據塊為單位。它們的差距就好象兩個人吃飯,一個人一粒一粒的吃,另一個人狼吞虎咽,快慢顯而易見。

  NIO中引入了緩沖區(Buffer)的概念,緩沖區作為傳輸數據的基本單位塊,所有對數據的操作都是基於將數據移進/移出緩沖區而來;讀數據的時候從緩沖區中取,寫的時候將數據填入緩沖區。盡管傳統JavaIO中也有相應的緩沖區過濾器流(BufferedInputStream等),但是移進/移出的操作是由程序員來包裝的,它本質是對數據結構化和積累達到處理時的方便,並不是一種提高I/O效率的措施。NIO的緩沖區則不然,對緩沖區的移進/移出操作是由底層操作系統來實現的。

  通常一次緩沖區操作是這樣的:某個進程需要進行I/O操作,它執行了一次讀(read)或者寫(write)的系統調用,向底層操作系統發出了請求,操作系統會按要求把數據緩沖區填滿或者排干。說起來簡單,其實很復雜。但至少我們知道了這事是由操作系統干的,比我們代碼級的實現要高效的多。

  除了效率上的差別外,緩沖區在數據分析和處理上也帶來的很大的便利和靈活性。

 (2)非阻塞的I/O + 就緒性選擇

  傳統JavaIO是基於阻塞I/O模型的:當發起一個I/O請求時,如果數據沒有准備好(read時無可讀數據,write時數據不可寫入),那么線程便會阻塞,直到數據准備好,導致線程大部分的時間都在阻塞。

  而非阻塞I/O則允許線程在有數據的時候處理數據,沒有數據的時候干點別的,提高了資源利用率。

  就緒性選擇通常是建立在非阻塞的基礎上,並且更進一步,它把檢查哪些I/O請求的數據准備好這個任務交給了底層操作系統,操作系統會去查看並返回結果集合,這樣我們只需要關心那些准備好進行操作的IO通道。關於就緒性選擇的過程會在后面詳述。

  NIO提供的Socket可以用非阻塞的方式工作,並且支持就緒性選擇,減少了資源消耗和CPU在線程間的切換,在管理線程效率上比傳統Socket高。

(3)文件鎖定和內存映射文件等操作系統特性

  NIO同時帶來了很多當今操作系統大都支持的特性。

  文件鎖定是多個進程協同工作的情況下,要協調進程間對共享數據的訪問必不可少的工具。

  內存映射利用虛擬內存技術提供對文件的高速緩存,使讀取磁盤文件就像從內存中讀取一樣高效,但是卻不會有內存泄漏的危險,因為在內存中不會存在文件的完整拷貝。

  此外還有一些其他的特性,后面再詳述。

 (4)為所有的原始類型提供(Buffer)緩存支持

 (5)使用Java.nio.charset.Charset作為字符集編碼解碼解決方案

 (6)增加通道(Channel)對象,作為新的原始I/O抽象

 (7)提供了基於Selector的異步網絡I/O。

為什么要使用NIO?

  對於文件I/O, 在我看來使用IO和NIO是區別不大的,Java1.4開始原始IO也根據NIO重新實現過了,提供了對於NIO特性的支持。即使是流,也會比以前更加高效。企業級應用軟件中涉及I/O的部分多半是讀寫文件的功能性需求,很少有在並發上的要求,那么JavaIO包已經很勝任了。

  對於網絡I/O,傳統的阻塞式I/O,一個線程對應一個連接,采用線程池的模式在大部分場景下簡單高效。當連接數茫茫多時,並且數據的移動非常頻繁,NIO無疑是更好的選擇。

  NIO標榜的是高速、可伸縮的I/O,因為它更親近操作系統。當需求很平凡,沒有太高的效率要求的時候,你看不出它的好,反而覺得NIO代碼實現復雜,不易理解。選擇與否全看使用的場景,這點就看使用者的權衡了。

 NIO的Buffer類族和Channel

  在NIO中和Buffer配合使用的還有Channel。Channel是一個雙向通道,即可讀,也可寫。有點兒類似Stream,但是Stream是單向的。應用程序中不能直接對Channel進行讀寫操作,而必須通過Buffer來進行。比如,在讀一個Channel的時候,需要先將數據讀入到相對應的Buffer,然后在Buffer中進行讀取。

 文件復制示例如下:

    public static void nioCopyFile(String resource, String destination){
        try {
            FileInputStream fis = new FileInputStream(resource);
            FileOutputStream fos = new FileOutputStream(destination);
            FileChannel readChannel = fis.getChannel();
            FileChannel writeChannel = fos.getChannel();
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            int len;
            while((len=readChannel.read(buffer))!=-1){
                buffer.flip();
                writeChannel.write(buffer);
                buffer.clear();
            }
            readChannel.close();
            writeChannel.close();
        } catch (FileNotFoundException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }

 文件映射到內存

  NIO提供了一種將文件映射到內存的方法進行I/O操作,它可以比常規的基於流的I/O快很多。這個操作主要由FileChannel.map()方法實現,比如:

MappredByteBuffer mbb = fc.map(FileChannel.MapMode.READ_WRITE, 0, 1024);

  以上代碼將文件的前1024個字節映射到內存中。map()方法返回一個MappredByteBuffer,它是ByteBuffer的子類。因此,可以像使用ByteBuffer那樣使用它。

 

    public static void nioCopyFile(String resource, String destination){
        try {

            RandomAccessFile fis = new RandomAccessFile(resource, "rw");
            RandomAccessFile fos = new RandomAccessFile(destination, "rw");
            FileChannel readChannel = fis.getChannel();            
            FileChannel writeChannel = fos.getChannel();
            MappedByteBuffer mbb = readChannel.map(FileChannel.MapMode.READ_WRITE, 0, readChannel.size());
            writeChannel.write(mbb);
            readChannel.close();
            writeChannel.close();
        } catch (FileNotFoundException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }

或者如下寫法:

    public static void nioCopyFile(String resource, String destination){
        try {
            File res = new File(resource);
            File dest = new File(destination);
            if(!dest.exists())
                dest.createNewFile();
            FileInputStream fis = new FileInputStream(res);
            FileOutputStream fos = new FileOutputStream(dest);
            FileChannel readChannel = fis.getChannel();            
            FileChannel writeChannel = fos.getChannel();
            MappedByteBuffer mbb = readChannel.map(FileChannel.MapMode.READ_ONLY, 0, readChannel.size());
            writeChannel.write(mbb);
            readChannel.close();
            writeChannel.close();
        } catch (FileNotFoundException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }      

或者如下寫法:

    public static void nioCopyFile(String resource, String destination){
        try {
            FileInputStream fis = new FileInputStream(resource);
            FileOutputStream fos = new FileOutputStream(destination);
            FileChannel readChannel = fis.getChannel();            
            FileChannel writeChannel = fos.getChannel();
            MappedByteBuffer mbb = readChannel.map(FileChannel.MapMode.READ_ONLY, 0, readChannel.size());
            writeChannel.write(mbb);
            readChannel.close();
            writeChannel.close();
        } catch (FileNotFoundException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }      

處理結構化數據

  NIO還提供了處理結構化數據的方法,稱之為散射(Scattering)和聚集(Gathering)。散射指將數據讀入一組Buffer中,而不僅僅是一個。聚集與之相反,指將數據寫入一組Buffer中。

  假設有文本文件,格式為“書名作者”,現通過聚集寫操作創建該文件和散射讀文件:

    public static void readAndWrite(){
        try {
            //聚集寫操作
            ByteBuffer bookBuf = ByteBuffer.wrap("java性能優化技巧".getBytes("utf-8"));
            ByteBuffer autBuf = ByteBuffer.wrap("葛一鳴".getBytes("utf-8"));
            int booklen = bookBuf.limit();  //記錄書名長度
            int authlen = autBuf.limit();    //記錄作者長度
            ByteBuffer[] bufs = new ByteBuffer[]{bookBuf, autBuf};
            File file = new File("D:\\book.txt");
            if(!file.exists())
                file.createNewFile();  //文件不存在則創建文件
            FileOutputStream fos = new FileOutputStream(file);
            FileChannel fc = fos.getChannel();
            fc.write(bufs);
            fos.close();
            fc.close();
            //散射讀操作
            ByteBuffer b1 = ByteBuffer.allocate(booklen);
            ByteBuffer b2 = ByteBuffer.allocate(authlen);
            ByteBuffer[] buffs = new ByteBuffer[]{b1,b2};
            FileInputStream fin = new FileInputStream(file);
            FileChannel fic = fin.getChannel();
            fic.read(buffs);
            fin.close();
            fic.close();
            String bookName = new String(b1.array(), "utf-8");
            String authName = new String(b2.array(), "utf-8");
            System.out.println(bookName+" "+authName);;
        } catch (UnsupportedEncodingException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }

 直接內存訪問

  NIO的Buffer還提供了一個可以直接訪問系統物理內存的類——DirectBuffer。DirectBuffer繼承自ByteBuffer,但和普通的ByteBuffer不同。普通的ByteBuffer仍然在JVM堆上分配空間,其最大內存,受到最大堆的限制。而DirectBuffer直接分配在物理內存中,並不占用堆空間。使用DirectBuffer是一個更接近系統底層的方法,所以,它的速度比普通的ByteBuffer更快。

申請DirectBuffer的方法如下:

ByteBuffer b = ByteBuffer.allocateDirect(500);

雖然訪問速度上有優勢,但是創建和銷毀DirectBuffer的花費卻遠比ByteBuffer高。因此在需要頻繁創建Buffer的場合,不宜使用DirectBuffer,但是如果能將DirectBuffer進行復用,那么,在讀寫頻繁的情況下,它完全可以大幅改善系統性能。

  將DirectBuffer應用於真實系統中,不可避免地還需要對DirectBuffer進行監控。下面是一段可用於DirectBuffer監控的代碼,增強DirectBuffer的可用性:

 

    //這段代碼用於監控DirectBuffer的使用情況
    public void monDirectBuffer() throws ClassNotFoundException, NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException{
        Class c = Class.forName("java.nio.Bits");  //通過反射取得私有數據
        Field maxMemory = c.getDeclaredField("maxMemory");
        maxMemory.setAccessible(true);
        Field reservedMemory = c.getDeclaredField("reservedMemory");
        reservedMemory.setAccessible(true);
        synchronized(c){
            Long maxMemoryValue = (Long) maxMemory.get(null); //總大小
            Long reservedMemoryValue = (Long) reservedMemory.get(null);  //剩余大小
            System.out.println("maxMemoryValue:"+maxMemoryValue);
            System.out.println("reservedMemoryValue:"+reservedMemoryValue);
        }
    }

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM