zoukankan      html  css  js  c++  java
  • NIO

    NIO:

    使用InputStream的read()方法从流中读取数据时,若数据源中没有数据,它会阻塞该线程,而OutputStream流,也是如此。因为它们都是阻塞式的输入与输出。不仅仅如此,传统的输入流,输出流都是通过字节的移动来处理的(即使不直接去处理字节流,但镀层的实现还是依赖于字节处理),也就是说,面向流的输入/输出,系统一次只能处理一个字节,因此面向流的输入/输出效率不高,为了提高效率,Java提供了一系列改进的输入/输出处理的新功能,这些新功能统称为New IO,简写为NIO。

    NIO使用了不同的方式来处理输入/输出,它采用内存映射文件的方式来处理输入/输出,NIO将文件或者文件的一段区域映射到了内存中,这样就可以想访问内存一样来访问文件了,通过这种方式来进行输入/输出比传统的输入/输出要快得多。

    在NIO中,有两个核心对象,一个是Channel (通道),另外一个则是Buffer(缓存)。

    Channel是对传统的输入/输出系统的模拟,在NIO系统中,所有的数据都需要通过Channel来进行传输(包括输出与输入),与传统的InputStream和OutputStream相比,它提供了一个map()方法,通过该map方法可以直接将“一块数据”映射到内存中。

    但程序是不能直接访问Channel中的数据,包括读取,写入都不行,Channel只与Buffer进行交互。也就是说,如果要从Channel中取得数据,就必须先用Buffer从Channel中取出一些数据,然后让程序从Buffer中取出这些数据;同理,若要对通道写入东西,也是先写入Buffer中,然后将Buffer写入Channel中。

    Buffer,从结构上来看,Buffer就像一个数组,它可以保存多个类型相同的数据。Buffer是一个抽象类,其最常用的子类是ByteBuffer.它可以在底层字节数组上进行get/set操作。除了ByteBuffer之外,对应于其他基本数据类型(boolean类型除外)都有相应的Buffer类:charBuffer,ShortBuffer,IntBuffer,LongBuffer,FloatBuffer,DoubleBuffer。

     以上相应的XxxBuffer都不能通过构造放来来直接创建,而是调用XxxBuffer.allocate(int capacity):来创建一个容量为capacity的XxxBuffer对象。一般来说,创建的ByteBuffer与CharBuffer的对象会比较多一些。

     Buffer中有三个重要的概念:capacity容量,position 位置,limit界限

      capacity 容量:指这个Buffer的可以存储多少数据的大小/长度

      postiton 位置:该参数类似于随机访问RandomAccessFile的记录指针,当从Buffer中取出/读出一个数据时,position会向后移动一位。初始化为0

      limit 界限: 一个不应该读出或者写入的缓冲区位置索引,即在Buffer容量中,position只能从0到limit参数范围的数据内容进行读取/写入,limit到capacity范围的数据内容时不能读取/写入的。初始化Buffer时,该参数默认值为capacity的值。

      

       如上图所示意的,该Buffer的容量有16个,limit指向了13,即position只能从0到12中取出数据,不能从13到15中取出数据。

     

     Buffer的主要作用就是从Channel中读取/写入数据,当对进行文件拷贝操作时,Buffer要从被拷贝文件中取出数据,然后向拷贝文件中写入Buffer。这里假设说我们定义了一个Buffer容量为1024的容量。当Buffer从被拷贝文件中取出数据,放入Buffer中,对应position的值为不停的加1,直到Buffer从被拷贝文件中取出了1024个数据,装了Buffer的容量,那么对应的position也是1024,这时我们从buffer中取出数据时,position由于已经是1024,最该Buffer最大的值时,程序是不能在从Buffer中读取数据的。

     因此需要将该Buffer的position的值设置为0,不仅如此,还需要设置从buffer读取的界限limit。比如说Buffer有1024的容量数据,我们只取出前512容量的数据。一般来说,当buffer有多少数据时,limit就为那个值。比如说Buffer的容量为1024,但数据只存到了512位,那么limit就是512。Buffer中提供了flip()方法,可以让Buffer从文件中取数据返回Buffer后,设置position的值为0 ,设置limit的值为Buffer存入多少数据的对应值。

     当程序从Buffer中取完数据后,并写入拷贝文件后,接下来Buffer有需要向被拷贝文件中取数据并写入Buffer中(假设说不是一次性拷贝完的情况),因为上述执行了flip()方法的原因,Buffer只能写入0-limit范围的数据,同时也因为position的位置为limit值,因此这个时候的Buffer是不能再写入任何数据的,为了让Buffer可以再次写入数据,需要将position的位置再次设置为0,并且limit的值再次设置为capacity的值。Buffer提供了clean方法,将position的值设置为0,将limit的值设置为capacity的值。

     下面写一个Buffer示范程序:

    public class NIO {
        
        private void buffereTest(){
            //创建Buffer
            CharBuffer buff = CharBuffer.allocate(8);
            System.out.println("capacity:"+buff.capacity());
            System.out.println("limit:"+buff.limit());
            System.out.println("position:"+buff.position());
            
            //放入元素
            buff.put("a");
            buff.put("b");
            buff.put("c");
            System.out.println("加入三个元素后,position = "+buff.position());
            //调用flip()方法
            buff.flip();
            System.out.println("执行flip()后,limit="+buff.limit()+",position="+buff.position());
            //取出第一个数据
            System.out.println("取出第一个元素:"+buff.get());
            System.out.println("取出一个元素后,position="+buff.position());
            //调用clear()方法
            buff.clear();
            System.out.println("执行clear后,limit="+buff.limit()+",position="+buff.position());
            System.out.println("取出第三个数据:"+buff.get(2));
            System.out.println("position:"+buff.position());
        }
        public static void main(String[] args) {
            // TODO Auto-generated method stub
            NIO nio = new NIO();
            nio.buffereTest();
        }
    }

     运行效果如下:

    capacity:8
    limit:8
    position:0
    加入三个元素后,position = 3
    执行flip()后,limit=3,position=0
    取出第一个元素:a
    取出一个元素后,position=1
    执行clear后,limit=8,position=0
    取出第三个数据:c
    position:0

     看到了Buffer如此的麻烦,估计会有人会认为直接设置byte[]或者char[]会更方便一些。但实际上者两者是不一样的。

    public class NIO {
        public void thanTest(){
            char[] chars = new char[10];
            CharBuffer charBuffer = CharBuffer.allocate(10);
            //存放数据
            for(int i=0;i<3;i++){
                chars[i]='a';
                charBuffer.put('a');
            }
            charBuffer.flip();
            for(int i=0;i<chars.length;i++){
                System.out.println("chars的第"+i+"值为:"+chars[i]);
            }
            for(int i=0;i<charBuffer.capacity();i++){
                System.out.println("charBuffer的第"+i+"值为:"+charBuffer.get(i));
            }
        }
        public static void main(String[] args) {
            // TODO Auto-generated method stub
            NIO nio = new NIO();
            nio.thanTest();
        }
    }

    运行效果如下:

    chars的第0值为:a
    chars的第1值为:a
    chars的第2值为:a
    chars的第3值为:
    chars的第4值为:
    chars的第5值为:
    chars的第6值为:
    chars的第7值为:
    chars的第8值为:
    chars的第9值为:
    charBuffer的第0值为:a
    charBuffer的第1值为:a
    charBuffer的第2值为:a
    Exception in thread "main" java.lang.IndexOutOfBoundsException
        at java.nio.Buffer.checkIndex(Unknown Source)
        at java.nio.HeapCharBuffer.get(Unknown Source)
        at File.NIO.thanTest(NIO.java:322)
        at File.NIO.main(NIO.java:329)
    View Code

     从上述代码我们可以得知,如读取文件内容不足char数组的长度时,剩余的位置char给予了空值(对某些文件来说,该空值代表的是空格的输入值),而charBuffer则可以避免这种情况,因为charBuffer写入数据时,先会调用flip()方法,让limit设置到buffer写入数据的位置,并且只能从CharBuffer的0~limit范围进行读与写的操作。

    Channel是一个抽象基类,按功能可以分成以下几大类:

      1. 用于文件的Channel: FileChannel

      2. 用于TCP网络的Channel: ServerSocketChannel,SocketChannel

      3. 用于UDP网络的Channel:DatagramChannel

      4. 用于支持线程间通信的Channel:Pipe.SinkChannel,PipeSourceChannel

      以上的Channel都不能通过构造方法来直接创建,而是调用InputStream/OutputStream的getChannel()方法来返回对应的Channel,根据不同的节点流返回的Channel也不一样,比如说FileInputStream/FileOutputStream返回的就是FileChannel。

    Channel中最常用的三类方法,map(),read(),write(),其中map()方法用于将Channel对应的部分或者全部数据映射成了ByteBuffer;而read()方法与write()方法都有一系列重载形式,这些方法用于从Bufer中读取数据或向Buffer中写入数据。

      关于map()方法中,有三个参数,第一个是FileChannel.MapMode参数,该参数执行映射时的模式,有只读,读写等模式,该FileChannel.MapMode下有对应常量,调用对应的常量作为参数便可;第二个,第三个参数用于控制将Channel的那些数据映射成ByteBuffer。

    下面通过NIO来实现拷贝文件:

    public class NIO {
        public void copyFile(String path ,String copyPath){
            File f1 = new File(path);
            File f2 = new File(copyPath);
            try{
                if(!f1.exists()){
                    System.out.println("拷贝文件不存在");
                }
                if(!f2.exists()){
                    f2.createNewFile();
                }
                if(f1.isFile() && f2.isFile()){
                    FileChannel inChannel = new FileInputStream(f1).getChannel();
                    FileChannel outChannel = new FileOutputStream(f2).getChannel();
                    
                    
                    //一次性取完,用于小文件的拷贝
    //                MappedByteBuffer buffer = inChannel.map(FileChannel.MapMode.READ_ONLY, 0, f1.length());
    //                System.out.println(buffer);
    //                outChannel.write(buffer);
    //                System.out.println(buffer);
                    
                    //分段取数据,用于大文件的拷贝
                    ByteBuffer byteBuff = ByteBuffer.allocate(1024);
                    while(inChannel.read(byteBuff) != -1){
                        //锁定空白区
                        byteBuff.flip();
                        outChannel.write(byteBuff);
                        //释放空白区
                        byteBuff.clear();
                    }
            }catch(Exception e){}    
        }
        
        public static void main(String[] args) {
            // TODO Auto-generated method stub
            NIO nio = new NIO();
            
            String charPath = "D:\data\file\book.txt";
            String charCopyPath = "D:\data\file\book_1.txt";
            nio.copyFile(charPath, charCopyPath);
        }
    }

    在上述代码中,有注释的适用于小文件的拷贝,另外一种是适用于大文件的拷贝

    NIO的随机访问文件

    在IO流中,有种可以任意访问文件位置并且可以在任意位置写入的RandomAccess流;在NIO中,也可以通过Buffer和Channel来实现。

    public class NIO {
        
        //下面代码实现了将文件内容进行复制,追加在该文件后面
        public void RandomFileChannelTest(String path){
            File file = new File(path);
            if(!file.exists()){
                System.out.println("要拷贝文件的路径不存在");
            }else{
                try{
                    RandomAccessFile raf = new RandomAccessFile(file,"rw");
                    FileChannel fileChannel = raf.getChannel();
                    ByteBuffer buffer =fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, file.length());
                    fileChannel.position(file.length()); //FileChannel.position(),改变记录指针值
                    fileChannel.write(buffer);
                }catch(Exception e){}
            }
        }
        
        public static void main(String[] args) {
            // TODO Auto-generated method stub
            NIO nio = new NIO();
            String path = "D:\data\file\book_1.txt";
            nio.RandomFileChannelTest(path);
        }
    }

    字符集与Charset:

    计算机底层是没有文本文件和图片之分的,它只是忠实地记录每个文件的二进制序列而已。对于文本文档而言,当需要保存时,程序/系统需要将文件的每个字符翻译成二进制序列;当需要读取时,程序/系统便会需要将二进制序列转换为一个个的字符。

    但由于各个国家的语言不同,各个国家都有自己的一套字符集,也就是说,当我们将中国的字符集翻译成了二进制序列,在用这个二进制序列翻译成了其他国家的字符集,那么便会出来乱码。

    在Java中,提供Charset类来处理字节序列和字符序列之间的转换关系。该类中,包含用于创建解码器和码编器的方法,还提供了获取Charset所支持的字符集方法。

    public class NIO {
        public void charseTest(){
            //获取Java支持的全部字符集
            SortedMap<String ,Charset> map = Charset.availableCharsets();
            for(String str : map.keySet()){
                System.out.println(str +"------>"+map.get(str));
            }
        }
        
        public static void main(String[] args) {
            nio.charseTest();
        }
    }

     对于中国而言,我们常用以下的字符集:

      GBK:简体中文字符集

      BIG5: 繁体中文字符集

      ISO-8859-1: ISO拉丁字母表NO.1,也叫做ISO-LATIN-1

      UTF-8 :8位UCS转换格式

      UTF-16BE:16位UCS转换格式

      UTF-16:16位UCS转换格式

      UTF-16LE:16位UCS转换格式

    Charset不能通过构造方法进行实例,而是通过Charset.forName(String 字符集)的方法来获取实例。获取实例后,便可以调用newDecoder()/newEncoder()方法获取CharsetDecoder/CharsetEncoder对象,分别代表着Charset的解码器与编码器。

    调用CharsetDecoder的decode()方法可以将ByteBuffer(字节序列)转换为CharBuffer(字符序列),调用CharsetEncode的encode()可以将CharBuffer(字符序列)转化为ByteBuffer(字节序列):

    public class NIO {
        public void charseTransform(){
            CharBuffer chars = CharBuffer.allocate(8);
            chars.put("你");
            chars.put("是");
            chars.put("谁");
            chars.flip();
            
            Charset charset = Charset.forName("utf-8");
            CharsetDecoder decoder = charset.newDecoder();
            CharsetEncoder encoder = charset.newEncoder();
            try{
                ByteBuffer bytes = encoder.encode(chars);
                System.out.println("""+decoder.decode(bytes)+""的编码如下:");
                for(int i=0;i<bytes.capacity();i++){
                    System.out.print(bytes.get(i)+"	");
                }            
            }catch(Exception e){}
        }
        
        public static void main(String[] args) {
            // TODO Auto-generated method stub
            NIO nio = new NIO();
            nio.charseTransform();
        }
    }

    运行效果如下:

    "你是谁"的编码如下:
    -28    -67    -96    -26    -104    -81    -24    -80    -127    

    文件锁:

    在操作系统中,如果多个运行程序需要并发修改同一个文件时,程序之间需要某种机制来进行通信,而使用文件锁可以有效地阻止多个进程并发修改同一个文件。

    在Java中,提供了FileLock来支持文件锁定的功能,在FileChannel中提供的lock()/tryLock()方法可以获得文件锁的FileLock对象,从而锁定文件。

      lock和tryLock的区别在于:当lock()试图锁定某个文件时,如果无法得到文件锁,程序将一直阻塞;而tryLock()是尝试锁定文件,它将直接返回而不是阻塞,如果获得了文件锁,则返回该文件锁,若没有获得,则返回null。

      lock()/lock(long position ,long size,boolean shared)方法:对文件进行加锁/对文件从position 开始,长度为size的内容加锁,该方法时阻塞式的

      tryLock/tryLock(long position ,long size,boolean shared)方法:对文件进行加锁/对文件从position 开始,长度为size的内容加锁,该方法时非阻塞式的

      上述方法有参数的情况下,当参数shared为true时,表明该锁时共享锁,可以运行多个进程来读取该文件,但组织其他进程来获得对该文件的排他锁;当参数shared为false时,表明该锁是一个排他锁。它将锁住对该文件的读写。

    处理完文件后,记得使用FileLock()的release()方法来释放文件锁。下面程序示范了使用FileLock锁定文件的示例:

    public class NIO {
        public void fileLock(){
            try{
                FileChannel fileChannel = new FileOutputStream("out.txt").getChannel();    
                FileLock fileLock = fileChannel.tryLock();
                System.out.println("对当前目录下out.txt文件添加文件锁");
                Thread.sleep(3600000); //持续一分钟
                fileLock.release();
                System.out.println("对当前目录下out.txt文件释放文件锁");
            }catch(Exception e){}
        }
        
        public static void main(String[] args) {
            nio.fileLock();
        }
    }

    运行上述代码后,我们对当前目录下得out.txt进行编辑,保存时,提示另一个程序正在使用此文件,进程无法访问,说明上述程序已经对该文件上锁了,其他进程在上述程序执行完毕之前是无法对该文件后进行操作得。

                                                              

    关于文件锁有几点需要指出:

     1. 在某些平台上,文件锁仅仅是建议性,并不是强制性的。着意味着即使一个程序不能获得文件锁,它也可以对该文件进行读写。

     2. 在某些平台上,不能同步地锁定一个文件并把它映射到内存中

     3. 文件锁是由Java虚拟机所持有的,如果两个Java程序使用同一个Java虚拟机运行,则它们不能对同一个文件进行加锁

     4. 在某些平台上关闭FileChannel时,会释放Java虚拟机在该文件上的所有锁,因此应该避免对同一个被锁定的文件打开多个FileChannel。

     

  • 相关阅读:
    [HDU2866] Special Prime (数论,公式)
    [骗分大法好] 信息学竞赛 骗分导论(论文搬运)
    flayway数据库管理
    RabbitMQ的基本概念与原理
    springboot+ideal实现远程调试
    盘点总结
    mysql查看进程命令
    Java字符串正则文本替换
    springboot代码级全局敏感信息加解密和脱敏方案
    使用PMD进行代码审查
  • 原文地址:https://www.cnblogs.com/hjlin/p/11436770.html
Copyright © 2011-2022 走看看