【NIO】学习系列(三)Channel

本文详细介绍了Java NIO Channel的概念、层次结构、主要接口及其特性,重点讲解了FileChannel的使用方法,包括读写操作、批量操作、锁定文件等。此外,还探讨了异步I/O、文件锁定、内存映射等高级特性。
摘要由CSDN通过智能技术生成

1.Channel介绍

通道就是数据传输的道路。

在丝绸之路上可以实现商品的交易。在NIO中可以实现在通道上传输原缓冲区和目的缓冲区要交互的数据。
数据交互图如下所示
在这里插入图片描述
注意点:
从缓冲区和通道的数据类型可以发现,缓冲区都是类,而通道都是接口,这时由于通道的功能实现是要依赖于操作系统的,Channel接口只定义了那些功能,而具体实现在不同的操作系统中是不一样的,因此,在JDK中,通道被设计成接口数据类型。

2.通道的接口层次类型

通道的类型是错综复杂的,在NIO中通道是一个接口

如下所示JDK文档中Channel有很多子接口,又有很多的实现类,从此信息来看,NIO技术中的通道功能非常强大。
在这里插入图片描述
通道是用于I/O操作的连接,更具体的讲,通道将代表数据到硬件设备、文件、网络套接字的连接。通道可处于打开或者关闭两种状态。当关闭了通道,再试图IO操作时就会导致ClosedChannelException异常被抛出,但可以通过调用通道的isOpen()判断通道是否打开,避免ClosedChannelException。一般情况下通道对于多线程的访问是安全的。

在JDK1.8版本中Channel接口具有11个子接口

  • AsynchronousChannel
  • AsycnronousByteChannel
  • ReadableByteChannel
  • ScatteringByteChannel
  • WritableByteChannel
  • GatheringByteChannel
  • ByteChannel
  • SeekableByteChannel
  • NetworkChannel
  • MulticastChannel
  • InterruptibleChannel
    每个通道都有自己独特的功能特性,下面逐一介绍

3.通道的特性

3.1AsyncharonousChannel接口介绍

AsyncharonousChannel接口主要作用是使用通道支持异步I/O操作。一步I/O操作有以下两种方式进行实现

(1)方法

Future operation();

operation是代表I/O操作的名称。使用Future可以用来检测I/O是否完成。

(2)回调

void operation(…A attachment,CompletionHandler<V,?super A> handler)

使用CompletionHandler回调的方式实现异步IO的优点是CompletionHandler对象可以被修复。当I/O操作成功或失败时,CompletionHandler对象中指定的方法会被调用

  • 当一个通道实现了可异步Asynchronously和可关闭closeable相关的接口时,若调用这个正在IO操作通道中的close方法
  • 取消通道此时通道会被置位一个错误的状态,这个状态可以阻止对通道的read()和write()。
  • 在调用cancel()方法以取消读写操作时,建议废弃I/O操作中的所有缓冲区,因为缓冲区的数据不是完整的,如果再次打开通道也要尽量避免访问这些缓冲区。

3.2 AsynchronousByteChannel接口介绍

此缓冲区接口的主要作用是使通道支持一步I/O操作,操作单位为字节

若上一个read()方法读取未完成,再次read()会抛出ReadPendingException。如果write()未完成再次调用write()会抛出WritePendingException。其他类型的I/O操作是否可以同时read()操作取决通道类型的实现。ByteBuffer类不是线程安全的,尽量保证在对其读写操作时,没有其他线程也来进行读写。

在这里插入图片描述

3.3 ReadableByteChannel接口介绍

ReadableByteChannel接口的主要作用是使通道允许对字节进行读操作。

注意:ReadableByteChannel接口只允许有一个读操作早执行。如果1个线程正在1个通道上执行1个read(),其他线程发起一个read()会被阻塞,知道第一个完成read。

ReadableByteChannel接口有以下连个特点:

  • 将通道当前位置中的字节序列读入1个ByteBuffer中
  • read(ByteBuffer)方法是同步的。

在这里插入图片描述

类的继承结构如下

在这里插入图片描述

上面三个接口的之间关系如下
在这里插入图片描述

3.4 ScatteringByteChannel接口的介绍

ScatteringByteChannel接口主要作用是可以从通道中读取字节到多个缓冲区中

ScatteringByteChannel接口的API结构如图所示

在这里插入图片描述

其类的继承结构如下所示

在这里插入图片描述

3.5 WriteableByteChannel接口的介绍

writableByteChannel接口的主要作用是使通道允许对字节进行写操作。

注意:WriteableByteChnanel只允许一个线程在执行写操作,如果其他线程也来写,就会被阻塞,知道第一个写完。

特点:

  • 将1个字节缓冲区的字节序列写入到当前位置
  • write(ByteBuffer)方法是同步的

在这里插入图片描述

其继承结构如下所示

在这里插入图片描述

3.6 GatheringByteChannel接口介绍

GatheringByteBuffer的主要作用是将多个缓冲区的数据写入到通道中。

GatheringByteChannel接口的API如下所示

在这里插入图片描述

3.7 Bytechannel接口介绍

ByteChannel的父接口有ReadableByteChannel和WritableByteChannel。Bytechannel接口没有添加新的方法。

ByteChannel是双向操作,可以读也可以写。

注意:ByteChannel自己没有任何的方法,都是继承父接口的方法

在这里插入图片描述

3.8 SeekableByteChannel接口的介绍

SeekableByteChannel接口的主要作用是在字节通道中维护position,以及允许position发生改变。

在这里插入图片描述

继承结构如下

在这里插入图片描述

3.9 NetWorkChannel接口介绍

netWorkChannel接口的主要作用是使通道与Socket尽心关联,使通道的数据可以在Socket上进行传输。

bind()方法,用于将Socket绑定本地地址

GetLocalAddress()返回Socket的SocketAddress对象,

在这里插入图片描述

结构如下所示

在这里插入图片描述

3.10 MulticastChannel接口介绍

MultiCastChannel接口的主要作用是使通道支持IP多播。IP多播就是将多个主机地址打包,形成一个group。然后报文向这个组进行发送,也就相当于同时向多个主机传输数据。

在这里插入图片描述

类继承结构如下

在这里插入图片描述

3.11 InterruptibleChannel接口的介绍

此接口可以是通道以异步的方式进行关闭与终端。

打断阻塞的两种方法

1.当通道实现了asynchronously和closeable特性:如果一个线程在通道上阻塞了。那么其他线程调用该通道的close(),这个线程会收到AsynchronousCloseException

2.当通道不仅实现了Asynchronously和closeable特性的同事还实现了interrupitable:当出现阻塞时,其他线程调用该线程的interrupt方法,通道将会被关闭,阻塞线程接收到ClosedByInterruptException异常,这个线程的状态一直是中断状态。

在这里插入图片描述

继承结构如下

在这里插入图片描述
至此介绍完了接口,下面介绍实现类及其方法

4.AbstractInterruptibleChannel类的介绍

核心接口的实现类如下

  • AbstractInterruptibleChannel
  • AbstractSelectableChannel
  • AsychronousFileChannel
  • AsynchronousServerSocketChannel
  • AsyncronousSocketChannel
  • DtagramChannel
  • Filechannel
  • pipe.SinkChannel
  • Pipe.SinkChannel
  • Pipe.SourceChannel
  • SelectableChannel
  • ServerSocketChannel
  • SocketChannel

下面将首先介绍FileChannel类,而FileChannel的父类正是AbtsrtactInterrupitableChannel类。

5.FileChannel类的使用

FileChannel的作用是读取、写入、映射和操作文件的通道。该通道永远是阻塞的操作。

FileChannel类在内部维护当前文件的Position可对其进行查询和修改。

FileCchannel内部维护了一个字节序列,当写入的字节超过文件的当前大小时就会增加文件大小。截取该文件时,减少文件大小。

特定的文件操作如下

  • 以不影响通道当前位置的方式,对文件中绝对位置的字节进行读取或写入
  • 将文件中的某个区域直接映射到内存中,相对于较大的文件,这通常比调用普通read()或write()方法更高效
  • 强制对底层存储设备进行文件的更新,确保在系统崩溃时不会丢失数据
  • 以一种可被很多操作系统优化为直接向文件系统缓存发送或从中读取的告诉传输方法,将字节从文件传输到某个其他通道中,反之亦然。
  • 可锁定某个文件区域,以组织其他程序对其进行访问

5.1 写操作与位置的使用

int write(ByteBuffer src)方法的作用是将remainning字节序列从给定的缓冲区写入此通道的当前位置,此方法的行为与WritableByteChannel接口所指定的行为完全相同:一个时刻只能有一个线程在写入。

int write(ByteBuffer src)方法的使用方式如下所示

@Test
public void test1(){
    FileOutputStream fos  = null;
    FileChannel fileChannel = null;
    try {
        fos = new FileOutputStream("D:\\xiaoDproject\\nio_project\\src\\main\\resources\\a.txt");
        fileChannel = fos.getChannel();
        ByteBuffer byteBuffer = ByteBuffer.wrap("abcd".getBytes());
        System.out.println("chanel position -> "+fileChannel.position());
        System.out.println("write 返回值 -> "+fileChannel.write(byteBuffer));
        System.out.println("chanel position -> "+fileChannel.position());
        fileChannel.position(2);//下次写入时从Position为2开始写入
        byteBuffer.rewind();//重置position为0再次进行写入
        System.out.println("write 返回值 -> "+fileChannel.write(byteBuffer));
        System.out.println("chanel position -> "+fileChannel.position());
    }catch (IOException e){
        e.printStackTrace();
    }finally {
        try {
            if(fileChannel != null){
                fileChannel.close();
            }
            if(fos != null){
                fos.close();
            }
        }catch (IOException e){
            e.printStackTrace();
        }

    }
}

验证 int write(Bytebuffer src)写入remainning

@Test
public void test2(){
    FileOutputStream fos  = null;
    FileChannel fileChannel = null;
    try {
        ByteBuffer buffer = ByteBuffer.wrap("abcdef".getBytes());
        buffer.position(1);//bcdef
        buffer.limit(3);//bc
        fos = new FileOutputStream("D:\\xiaoDproject\\nio_project\\src\\main\\resources\\a.txt");
        fileChannel = fos.getChannel();
        System.out.println("写入 ->"+fileChannel.write(buffer));
        System.out.println("buffer position->"+buffer.position());
    }catch (Exception e){
        e.printStackTrace();
    }finally {
        try {
            if(fileChannel != null){
                fileChannel.close();
            }
            if(fos != null){
                fos.close();
            }
        }catch (IOException e){
            e.printStackTrace();
        }

    }
}

write(ByteBuffer src)验证方法是同步的

打印的结果没有出现abcdef和我说中国人交叉的情况,所以是同步的,一个时刻只能有一个进行写入。

注意:不要使用junit进行测试,亲测出现问题,而且9个线程必须都使用同一个公共的fos和Filechannel

abcdef
我是中国人
abcdef
我是中国人
abcdef
我是中国人
abcdef
我是中国人
public class MyApplication {
    private static FileOutputStream fos  = null;
    private static FileChannel fileChannel = null;
    private static CountDownLatch countDownLatch = new CountDownLatch(9);
    public static void main(String[] args) throws InterruptedException, IOException {
        fos = new FileOutputStream("D:\\xiaoDproject\\nio_project\\src\\main\\resources\\a.txt");
        fileChannel = fos.getChannel();
        IntStream.rangeClosed(1,9).forEach( i -> {
            new Thread(()->{
                try {
                    ByteBuffer buffer  = null;
                    if(i%2==0){
                        buffer = ByteBuffer.wrap("abcdef\r\n".getBytes());
                    }else{
                        buffer = ByteBuffer.wrap("我是中国人\r\n".getBytes());
                    }
                    fileChannel.write(buffer);
                }catch (Exception e){
                    e.printStackTrace();
                }finally {
                    countDownLatch.countDown();
                }
            }).start();
        });
        countDownLatch.await();
        fileChannel.close();
        fos.close();
    }
}

5.2读取操作int read(ByteBuffer dst)

此方法的特点

  • 方法是同步的
  • 读取一个目标缓冲区remainning大小的字节到目标缓冲区中

返回值

  • 负数:表示通道数据被读取完了
  • 正数:代表读取到目标缓冲区的字节个数
  • 0 :表示通道没有读取任何数据,一般是因为目标缓冲区的remainning为0了
@Test
public void test3(){
    FileInputStream fos = null;
    FileChannel fileChannel = null;
    try {
        fos = new FileInputStream(new File("D:\\xiaoDproject\\nio_project\\src\\main\\resources\\a.txt"));
        fileChannel = fos.getChannel();
        ByteBuffer byteBuffer = ByteBuffer.allocate(5);//5字节大小
        int res = fileChannel.read(byteBuffer);
        System.out.println("read -> "+res);
        System.out.println("buffer position ->"+byteBuffer.position());
        res = fileChannel.read(byteBuffer);
        System.out.println("read2 -> "+res);
        //读取为0,因为remainning为0,所以只需要重置缓冲区
        byteBuffer.clear();
        res = fileChannel.read(byteBuffer);
        System.out.println("read3 -> "+res);
    }catch (IOException e){
        e.printStackTrace();
    }finally {
        try {
            if(fileChannel != null){
                fileChannel.close();
            }
            if(fos != null){
                fos.close();
            }
        }catch (IOException e){
            e.printStackTrace();
        }
    }
}

5.3 批量写操作long write(BuyeBuffer[] srcs)

将每个缓冲区的remainning字节序列写入到通道的当前位置

其writes(srcs)与write(srcs,0,srcs.length)的用法完全相同

  • 方法是同步的
  • 将1个byteBuffer缓冲区的remainning字节序列写入到通道的当前位置

此外还具有第三个特性

  • 将多个ByteBuffer缓冲区中的remainning剩余字节序列写入通道的当前位置

测试代码如下

a.txt的内容如下,写入时会从当前缓冲区的position开始。

bcd1234***
@Test
public void test4(){
    FileOutputStream fos = null;
    FileChannel fileChannel = null;
    try {
        fos = new FileOutputStream("D:\\xiaoDproject\\nio_project\\src\\main\\resources\\a.txt");
        fileChannel = fos.getChannel();
        ByteBuffer buffer1 = ByteBuffer.wrap("abcd".getBytes());
        buffer1.position(1);
        ByteBuffer buffer2 = ByteBuffer.wrap("1234".getBytes());
        ByteBuffer buffer3 = ByteBuffer.wrap("***".getBytes());
        ByteBuffer[] buffers = new ByteBuffer[]{buffer1,buffer2,buffer3};
        fileChannel.write(buffers);
    }catch (IOException e){

    }finally {
        try {
            if(fileChannel != null){
                fileChannel.close();
            }
            if(fos != null){
                fos.close();
            }
        }catch (IOException e){
            e.printStackTrace();
        }
    }
}

5.4 批量读取操作 long read(ByteBuffer[] dsts)

该方法是将字节序列从通道读取给定缓冲区数组中第0个缓冲区的当前位置。

特点

  • 方法是同步的
  • 将当前位置的字节序列读取到第0个ByteBuffer缓冲区的remainning空间中

此方法是从通道的当前位置开始将第一个缓冲区的remainning空间先占满,再占第二个

@Test
public void test5(){
    FileInputStream fis = null;
    FileChannel fileChannel = null;
    try {
        fis = new FileInputStream("D:\\xiaoDproject\\nio_project\\src\\main\\resources\\a.txt");
        fileChannel = fis.getChannel();
        ByteBuffer buffer = ByteBuffer.allocate(2);
        ByteBuffer buffer1 = ByteBuffer.allocate(2);
        ByteBuffer[] byteBuffers = new ByteBuffer[]{buffer,buffer1};
        long res = fileChannel.read(byteBuffers);//读取了四个
        System.out.println("读取的字节个数 -> "+res);
        //重置缓冲区
        buffer.clear();
        buffer1.clear();
        //a.txt总共就五个字节之前读取了4个还剩一个正在读取
        res = fileChannel.read(byteBuffers);
        System.out.println("读取的字节个数 -> "+res);
        buffer.clear();
        buffer1.clear();
        //上一次已经读取完了,这次肯定返回-1啦
        res = fileChannel.read(byteBuffers);
        System.out.println(res);
    }catch (Exception e){

    }finally {
        try {
            if(fileChannel != null){
                fileChannel.close();
            }
            if(fis != null){
                fis.close();
            }
        }catch (IOException e){
            e.printStackTrace();
        }
    }
}

5.5 部分批量写操作long write(ByteBuffer[] srcs,int offset,int length)

方法的作用是以指定的缓冲区组的下标开始,向后使用legth个字节缓冲区,再讲每个缓冲区的remainning剩余字节序列写入到此通道当前位置

参数说明:

offset:起始缓冲区的下标,不能小于零且不能大于srcs.length

length:要访问的最大缓冲区个数;必须为非负数并且不能大于srcs.length-offset

特点

  • 将缓冲区的remainng字节序列写入到通道的当前位置
  • 方法是同步的

测试代码如下

fileChannel.write(bufferArray,1,1);//表示将数组下标为1,取出1个元素将其remainning写入到通道中

a.txt的内容输出为qwer

@Test
public void test6(){
    FileOutputStream fos = null;
    FileChannel fileChannel = null;
    try {
        fos = new FileOutputStream("D:\\xiaoDproject\\nio_project\\src\\main\\resources\\a.txt");
        fileChannel = fos.getChannel();
        ByteBuffer byteBuffer1 = ByteBuffer.wrap("abcd".getBytes());
        ByteBuffer byteBuffer2 = ByteBuffer.wrap("qwer".getBytes());
        ByteBuffer byteBuffer3 = ByteBuffer.wrap("1234".getBytes());
        ByteBuffer[] bufferArray = new ByteBuffer[]{byteBuffer1,byteBuffer2,byteBuffer3};
        fileChannel.write(bufferArray,1,1);

    }catch (Exception e){
        e.printStackTrace();
    }finally{
        try {
            if(fileChannel !=null){
                fileChannel.close();
            }
            if(fos != null){
                fos.close();
            }
        }catch (IOException e){
            e.printStackTrace();
        }

    }
}

5.6 部分批量读取

long read(ByteBuffer[]dsts,int offset,int length);

作用:将通道中当前位置的字节序列读入以下标为offset开始的remainng剩余空间,并且连续写入到legtn个byteBuffer缓冲区

测试代码如下

fileChannel.read(bufferArray,1,2);//从下标1开始读入两个缓冲区
@Test
public void test7(){
    FileInputStream fis = null;
    FileChannel fileChannel = null;
    try {
        fis = new FileInputStream("D:\\xiaoDproject\\nio_project\\src\\main\\resources\\a.txt");
        fileChannel = fis.getChannel();
        ByteBuffer byteBuffer1 = ByteBuffer.allocate(2);
        ByteBuffer byteBuffer2 = ByteBuffer.allocate(2);
        ByteBuffer byteBuffer3 = ByteBuffer.allocate(2);
        ByteBuffer[] bufferArray = new ByteBuffer[]{byteBuffer1,byteBuffer2,byteBuffer3};
        fileChannel.read(bufferArray,1,2);//从下标开始读入两个缓冲区
        //所以byteBuffer2该是ab,byteBuffer3该是cd
        System.out.println("通道的当前位置 -> "+fileChannel.position());
        byteBuffer2.rewind();
        byteBuffer3.rewind();
        while(byteBuffer2.hasRemaining()){
            System.out.println((char)byteBuffer2.get());
        }
        while(byteBuffer3.hasRemaining()){
            System.out.println((char)byteBuffer3.get());
        }
    }catch (IOException e){
        e.printStackTrace();
    }finally {
        try {
            if(fileChannel != null){
                fileChannel.close();
            }
            if(fis != null){
                fis.close();
            }
        }catch (IOException e){
        }
    }
}

5.7 向通道的指定position位置写入数据

write (ByteBuffer src long position);

从通道的position位置开始写入数据

注意

  • 此方法不会改变通道的position
  • 假设从2开始那么前面的会用空格替代

测试代码如下–此时a.txt的内容为

 abcde//前面有个空格哦
@Test
public void test8(){
    try(FileOutputStream fos = new FileOutputStream("D:\\xiaoDproject\\nio_project\\src\\main\\resources\\a.txt");
        FileChannel fileChannel = fos.getChannel()) {
        ByteBuffer byteBuffer = ByteBuffer.wrap("abcde".getBytes());
        System.out.println(fileChannel.position());
        fileChannel.write(byteBuffer,1);
        System.out.println(fileChannel.position());
    }catch (IOException e){
        e.printStackTrace();
    }
}

5.8 读取通道指定位置的数据

read(ByteBuffer dst,long position)

从通道指定的位置读取数据

注意

  • 此方法不修改当前通道的position
  • 此方法是同步的

测试程序如下

@Test
public void test9(){
    try(FileInputStream fis = new FileInputStream("D:\\xiaoDproject\\nio_project\\src\\main\\resources\\a.txt");
        FileChannel fileChannel = fis.getChannel()) {
        ByteBuffer byteBuffer = ByteBuffer.allocate(5);
        //当前a.txt为abcdefg7位数,从下标为1开始,最多只能读取五个因为bytebuffer的remainning = 5
        int read = fileChannel.read(byteBuffer, 1);
        System.out.println("读取个数 -> "+read);
        System.out.println("buffer position ->"+byteBuffer.position());
        byteBuffer.rewind();
        while(byteBuffer.hasRemaining()){
            System.out.println((char)byteBuffer.get());
        }
    }catch (IOException e){
        e.printStackTrace();
    }
}

5.9 设置位置与获得大小

position(long newPosition)设置通道的当前位置

long size()方法返回通道关联的文件的当前大小(字节单位)

注意

就算position设置大于文件当前的大小也是可以的,当从当前位置读取时会返回-1,直接到达文件末尾。当从当前位置写入时会尝试扩大文件大小。在以前文件末尾和新当前位置之间的字节是没有指定的。

设置position后读取测试

@Test
public void test10(){
    try(FileInputStream fis = new FileInputStream("D:\\xiaoDproject\\nio_project\\src\\main\\resources\\a.txt");
        FileChannel readChannel = fis.getChannel())
    {
        ByteBuffer byteBuffer = ByteBuffer.allocate(3);
        //从下标为2开始
        readChannel.position(2);
        readChannel.read(byteBuffer);
        byteBuffer.rewind();
        while(byteBuffer.hasRemaining()){
            System.out.println((char)byteBuffer.get());
        }
    }catch (IOException e){
        e.printStackTrace();
    }
}

设置position大于文件的大小然后写入,在之前的文件末尾和position起始位置之间会有空格,因为未指定字节

@Test
public void test10(){
    try(FileInputStream fis = new FileInputStream("D:\\xiaoDproject\\nio_project\\src\\main\\resources\\a.txt");
        FileChannel readChannel = fis.getChannel())
    {
        ByteBuffer byteBuffer = ByteBuffer.allocate(3);
        //从下标为2开始
        readChannel.position(2);
        readChannel.read(byteBuffer);
        byteBuffer.rewind();
        while(byteBuffer.hasRemaining()){
            System.out.println((char)byteBuffer.get());
        }
    }catch (IOException e){
        e.printStackTrace();
    }
}

5.10 截断缓冲区

truncate(long size)方法的作用时将此通道的文件截取给定大小。如果给定大小小于文件大小,截取,舍弃后面所有字节。如果给定大小大于文件大小,则不会修改文件。

如下所示:当前a.txt的内容为abcde,截取为3个时只剩下abc,所以最后输出abc

@Test
public void test12(){
    try(FileOutputStream fos = new FileOutputStream("D:\\xiaoDproject\\nio_project\\src\\main\\resources\\a.txt");
        FileChannel fileChannel = fos.getChannel()) {
        ByteBuffer byteBuffer = ByteBuffer.wrap("abcdef".getBytes());
        fileChannel.write(byteBuffer);
        System.out.println(fileChannel.size());
        //截取大小
        fileChannel.truncate(3);
        System.out.println(fileChannel.size());
    }catch (IOException e){
        e.printStackTrace();
    }
}

5.11 将数据传输到其他可写入字节通道

long transferTo(position,count,WritableByteChannel dest)方法是将字节从此通道的文件传输到给定的可写入字节通道

试图读取从position开始读取count个字节,并将其写入到指定的通道的当前位置

  • 此方法不修改此通道的位置
  • 与简单的循环语句相比,这种方式高效的多

如下所示:现有a.txt:abcdefg,b.txt:123456789

如下所示传输,结果b.txt为123cde789

@Test
public void test13(){
    try (RandomAccessFile fileA = new RandomAccessFile(new File("D:\\xiaoDproject\\nio_project\\src\\main\\resources\\a.txt"),"rw");
         RandomAccessFile fileB = new RandomAccessFile(new File("D:\\xiaoDproject\\nio_project\\src\\main\\resources\\b.txt"),"rw");
         FileChannel channelA = fileA.getChannel();
         FileChannel channelB = fileB.getChannel()){
        channelB.position(3);
        //a.txt为abcdefg从下标为2开始传输3个及cde传输到channelB的下标为3开始
        channelA.transferTo(2,3,channelB);
    }catch (IOException e){
        e.printStackTrace();
    }
}

几种情况的注意点

1.当position大于文件的大小,此时不会传输任何字节

2.假设现在count的个数大于siz-position也就是现有个数,那么只会传输现有个数

5.12 将字节从给定可读取字节通道传输到此通道文件中

long transferFrom(ReadableByteChannel src,Position,count)

将目标通道从posotion开始传输count个字节到当前通道的文件中

此方法不修改此通道的位置

测试程序如下

a.txt => abcdefg

b.txt => 123456789

结果 a.txt => abc12fg

分析

使用ChannelA.transferFrom是将B的内容从B的position开始赋值到ChannelA中,赋值到postion = 3的位置赋值2个,也就是将12复制到ChannelA中(替换了原有的字节)。

@Test
public void test19(){
    RandomAccessFile rafA = null;
    RandomAccessFile rafB = null;
    try {
        rafA = new RandomAccessFile("D:\\xiaoDproject\\nio_project\\src\\main\\resources\\a.txt","rw");
        rafB = new RandomAccessFile("D:\\xiaoDproject\\nio_project\\src\\main\\resources\\b.txt","rw");

        FileChannel fileChannelA = rafA.getChannel();

        FileChannel fileChannelB = rafB.getChannel();

        long length = fileChannelA.transferFrom(fileChannelB, 3, 2);
        System.out.println("length =>"+length);

    }catch (IOException e){
        e.printStackTrace();
    }finally {
        try {
            if(rafB != null)
                rafB.close();
            if(rafB != null)
                rafB.close();
        }catch (IOException e){
            e.printStackTrace();
        }
    }
}

5.13 执行锁定操作

FileLock lock(long position,long size,boolean shared)方法是获取此通道的文件给定区域的锁定。

在调用此方法时另一个线程关闭了此通道,就会抛出AsynchronousCloseException异常

可以锁定文件的固定位置。无参方法lock(),锁定的上限是Long.MAX_VALUE。

文件分为独占的和共享的,共享锁定可以组织其他恒旭获取独占锁定,但是允许共享锁定。

注意:某些操作系统不支持共享锁定,在这种情况下,自动将对共享锁定转为独占锁定。可以使用isShared()方法来测试新获取的锁定是共享的还是独占的。

  • 共享锁自己和他人不能写
  • 共享锁自己和他人可以读取
  • 独占锁自己可读可写
  • 独占锁他人不可读不可写
  • lock的position指从哪里上锁size指锁的位置是多少
  • 提前锁定:当前文件大小小于position但是当文件扩大到一定程度postion小于文件大小,此时位置就会上锁
  • 加了共享锁其他人不能加独占锁,但是可以加共享锁
  • 加了独占锁不能加任何锁了

文件锁定是以整个虚拟机来保持的,不适用用于同一虚拟机多线程对文件访问

测试程序如下:

需要创建两个文件,以两个进程的方式运行

测试结果分析

由于第一个已经创建了独占锁,其他再次上独占锁肯定是无法上独占锁的,因此会阻塞

@Test
public void test20(){
    try(RandomAccessFile raf = new RandomAccessFile("D:\\xiaoDproject\\nio_project\\src\\main\\resources\\a.txt","rw");
        FileChannel fileChannel = raf.getChannel()) {
        //锁定 1 - 2
        System.out.println("A - > start lock");
        fileChannel.lock(1,2,false);
        System.out.println("A - > end lock");
        Thread.sleep(Integer.MAX_VALUE);
    }catch (Exception e){
        e.printStackTrace();
    }
}
@Test
public void test21(){
    try(RandomAccessFile raf = new RandomAccessFile("D:\\xiaoDproject\\nio_project\\src\\main\\resources\\a.txt","rw");
        FileChannel fileChannel = raf.getChannel()) {
        //锁定 1 - 2
        System.out.println("B - > start lock");
        fileChannel.lock(1,2,false);
        System.out.println("B - > end lock");
        Thread.sleep(Integer.MAX_VALUE);
    }catch (Exception e){
        e.printStackTrace();
    }
}

验证AsynchronousCloseException

在执行了lock方法时,然后进行close就会出现这个异常

public static void main(String[] args) throws InterruptedException, IOException {
    FileOutputStream fos = new FileOutputStream("D:\\xiaoDproject\\nio_project\\src\\main\\resources\\a.txt");
    FileChannel channel = fos.getChannel();
    Thread a = new Thread(() -> {
        try {
            channel.lock(1,2,false);
        } catch (IOException e) {
            e.printStackTrace();
        }
    });
    Thread b = new Thread(() -> {
        try {
            
            channel.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    });
    a.start();
    TimeUnit.MILLISECONDS.sleep(1);
    b.start();
    TimeUnit.SECONDS.sleep(1);
    //使用线程2关闭已锁定的channel
    fos.close();
    channel.close();
}

当线程在获得锁时发现自己被中断会抛出FileLOckInterruptException

如下所示:有两个程序在进行执行,A程序上来就加了独占锁,B程序加不了独占锁,B程序将自己中断了,此时没有获得锁,不会有变化,当获得锁时立即中断,也就是在A程序释放锁后B获得锁然后捕获到异常。

public class MyDemo1 {
    public static void main(String[] args) {
        try (RandomAccessFile raf = new RandomAccessFile("D:\\xiaoDproject\\nio_project\\src\\main\\resources\\a.txt","rw");
             FileChannel fileChannel = raf.getChannel()){
            System.out.println("A is started");
            FileLock lock = fileChannel.lock(1, 1, false);
            System.out.println("A is get Lock");
            TimeUnit.SECONDS.sleep(10);
        }catch (IOException | InterruptedException e){
            e.printStackTrace();
        }finally {
            System.out.println("A is done");
        }
    }
}
public class MyDemo2 {
    public static void main(String[] args) throws InterruptedException {
        Thread t= new Thread(()->{
            try (RandomAccessFile raf = new RandomAccessFile("D:\\xiaoDproject\\nio_project\\src\\main\\resources\\a.txt","rw");
                 FileChannel fileChannel = raf.getChannel()){
                System.out.println("B is started");
                FileLock lock = fileChannel.lock(1, 1, false);
                System.out.println("B is get Lock");
                TimeUnit.SECONDS.sleep(10);
                System.out.println("B is finished");
            }catch (Exception e){
                e.printStackTrace();
            }
        });
        t.start();
        TimeUnit.SECONDS.sleep(2);
        t.interrupt();
    }
}

验证共享锁不能写

如下所示,Demo1给文件的1,2位置加入了共享锁,

其他人不能在这个位置写入,如果写入就会报错

public class MyDemo1 {
    //对文件a.txt从1,2位置加入共享锁
    public static void main(String[] args) {
        try(RandomAccessFile raf = new RandomAccessFile("D:\\xiaoDproject\\nio_project\\src\\main\\resources\\a.txt","rw");
            FileChannel fileChannel = raf.getChannel()){
            fileChannel.lock(1,2,true);
            TimeUnit.SECONDS.sleep(20L);
        }catch (IOException | InterruptedException e){
            e.printStackTrace();
        }
    }
}
public class MyDemo2 {
    //在别人加的共享锁的基础上写入
    public static void main(String[] args) {
        try (RandomAccessFile raf = new RandomAccessFile("D:\\xiaoDproject\\nio_project\\src\\main\\resources\\a.txt","rw");
             FileChannel fileChannel = raf.getChannel()){
            fileChannel.position(1);
            //从加入共享锁的位置读取
            ByteBuffer byteBuffer = ByteBuffer.wrap("abc".getBytes());
            fileChannel.write(byteBuffer);

        }catch (IOException e){
            e.printStackTrace();
        }
    }
}

5.14 FileLock lock()方法的使用

注意:无参的lock是对文件整体的独占锁定。调用lock与lock(0,Long.MAX_VALUE,false)是一致的,因为内部也是这样做的。

代码演示

5.15 尝试锁定给定区域(tryLock)

FileLock tryLock(long position,long size,boolean shared);//尝试获取此通道的文件给定区域的锁定

此方法不会阻塞当前线程。无论是否成功总是立即返回。如果由于另一个程序保持着锁导致无法获取锁定则返回Null,其他任何情况都会抛出异常。

测试程序如下

测试加独占锁后第二个程序加入共享锁

结果FileLock为null,如果demo1加入的是共享锁,那么demo2是可以获得锁的,因为共享锁可以叠加

public class MyDemo1 {
    //对文件a.txt从2,3位置加入共享锁
    public static void main(String[] args) {
        try(RandomAccessFile raf = new RandomAccessFile("D:\\xiaoDproject\\nio_project\\src\\main\\resources\\a.txt","rw");
            FileChannel fileChannel = raf.getChannel()){
            System.out.println("A 加上独占锁");
            FileLock fileLock = fileChannel.tryLock(1, 2, false);
            System.out.println("A 加入独占锁成功 -> "+fileLock);
            TimeUnit.SECONDS.sleep(20L);
        }catch (IOException | InterruptedException e){
            e.printStackTrace();
        }
    }
}
public class MyDemo2 {
    public static void main(String[] args) {
        try (RandomAccessFile raf = new RandomAccessFile("D:\\xiaoDproject\\nio_project\\src\\main\\resources\\a.txt","rw");
             FileChannel fileChannel = raf.getChannel()){
            //尝试获得共享锁
            System.out.println("B -> start");
            FileLock fileLock = fileChannel.tryLock(1, 2, true);
            System.out.println("B 获得共享锁 -> "+fileLock);
        }catch (IOException e){
            e.printStackTrace();
        }
    }
}

5.16 无参的tryLock();

其和tryLock(position,size,shared)是一致的,只有在其中传递的是默认自,如下所示

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-p1QAdjER-1602674187422)(images/trylock.png)]

5.17 FileLock类

表示文件区域锁定的标记。每次通过FileChannel类的lock()或tryLock()方法获取锁定时就会获得这个对象。

通过release()方法可以释放锁定。

调用isValid()的方法测试锁定的有效性。

单个虚拟机在某个特定文件上所保持锁定是不重叠的。要测试某个候选锁定范围是否与现有锁定重叠,可使用overlaps()方法。

注意当关闭通道时其所在的锁定也会关闭,不管锁定是当前的通道还是其他通道,都会进行关闭。

channel()方法发挥当前锁所属的FileChnnel文件通道对象,在新版本的JDK中该方法已经被public channel acqueredBy()方法所替代。

5.18 强制将所有对通道的更新写入包含文件的存储设备

void force(boolean metaData)

如果此通道的文件驻留在本地存储设备上,则此方法返回时刻保证:在此通道创建后或在最后一次调用此方法或,对改文件进行的所有更改都已经写入设备中。这对于系统崩溃时不会丢失重要的信息。如果文件不再本地上就无法保证了

总之

我们知道,操作系统为了减少IO,是先将数据放入内核缓存中,特定的时间进行更新到文件中。如果出现宕机就会出现缓存数据丢失。force(boolean)就是强制进行IO,在某种程度上如果发生宕机可以减少文件的损失。

当参数为true时强制更新文件内容和元数据,不为true时只强制更新文件

测试代码如下

第一个程序执行的时间是66

第二个是3289

所以force对于性能的损耗是很大的,谨慎使用

@Test
public void test22(){
    try(FileOutputStream fos = new FileOutputStream("D:\\xiaoDproject\\nio_project\\src\\main\\resources\\a.txt");
        FileChannel fileChannel = fos.getChannel()) {
        long beginTime = System.currentTimeMillis();
        for(int i = 0;i<5000;i++){
            fileChannel.write(ByteBuffer.wrap("1233".getBytes()));
        }
        long endTime = System.currentTimeMillis();
        System.out.println("spend time -> "+(endTime - beginTime));
    }catch (IOException e){
        e.printStackTrace();
    }
}
@Test
public void test23(){
    try(FileOutputStream fos = new FileOutputStream("D:\\xiaoDproject\\nio_project\\src\\main\\resources\\a.txt");
        FileChannel fileChannel = fos.getChannel()) {
        long beginTime = System.currentTimeMillis();
        for(int i = 0;i<5000;i++){
            fileChannel.write(ByteBuffer.wrap("1233".getBytes()));
            fileChannel.force(false);
        }
        long endTime = System.currentTimeMillis();
        System.out.println("spend time -> "+(endTime - beginTime));
    }catch (IOException e){
        e.printStackTrace();
    }
}

5.19 将文件通道区域直接映射到内存

MappedByteBuffer map(FileChannel.MapMode mode,long position,long zise)

对于大文件MappedByteBuffer 的速度很快,但是小文件不建议使用

其中MapMode有三种模式

  • 只读(MapMode.READ_ONLY)
  • 读取/写入(READ_WRITE):对得到的缓冲区的更改最终将传播到文件,注意:该更改对其他程序不一定可见
  • 专用(PRIVATE):对得到的缓冲区的更改不会传播到文件,并且对映射到同一文件是不可见的,相反会创建缓冲区已修改部分的专用副本。

MappedByteBuffer三个特有的方法

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-T6MLTUrw-1602674187424)(images/特有.png)]

测试代码如下所示

@Test
public void test24(){
    try (RandomAccessFile raf = new RandomAccessFile("D:\\xiaoDproject\\nio_project\\src\\main\\resources\\a.txt","rw");
         FileChannel fileChannel = raf.getChannel()){
        //cdefg
        MappedByteBuffer buffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 2, 5);
        System.out.println("position -> "+buffer.position());
        while(buffer.hasRemaining()){
            System.out.println((char)buffer.get());
        }
        System.out.println("position -> "+buffer.position());
    }catch (IOException e){
        e.printStackTrace();
    }
}

测试使用put就可以写入数据

如果发生写入数据没改变,请不要再编辑器中打开文件,应该在磁盘中打开

public static void main(String[] args) throws IOException {
    File file = new File("D:\\xiaoDproject\\nio_project\\src\\main\\resources\\a.txt");
    RandomAccessFile raf = new RandomAccessFile(file,"rw");
    FileChannel fileChannel = raf.getChannel();
    MappedByteBuffer buffer = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, 5);
    while(buffer.hasRemaining()){
        System.out.println((char)buffer.get());
    }
    buffer.position(0);
    buffer.put((byte)'d');
    buffer.put((byte)'0');
    buffer.put((byte)'n');
    buffer.put((byte)'g');
    fileChannel.close();
    raf.close();
}

私有模式PRIVATE

只针对当前的MappedByteBuffer可视,并不会更新底层文件

force()方法的作用

public final MappedByteBuffer force()方法对的作用是将此缓冲区所做的内容强制写入包含映射文件的存储设备中。如果文件不在本地设备上,则无法做出这样的保证。但是调用force()会导致性能下降

测试代码如下所示

@Test
public void test28(){
    try (RandomAccessFile raf = new RandomAccessFile("D:\\xiaoDproject\\nio_project\\src\\main\\resources\\b.txt","rw");
         FileChannel fileChannel = raf.getChannel()){
        MappedByteBuffer byteBuffer = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, 10);
        long start = System.currentTimeMillis();
        for(int i = 1;i< 10;i++){
            byteBuffer.put((byte)'3');
            byteBuffer.force();
        }
        long end = System.currentTimeMillis();
        System.out.println("spend time -> "+(end-start));
    }catch (IOException e){
        e.printStackTrace();
    }
}

MappedByteBuffer load()方法

方法的作用是将此缓冲区的内容加载到物理内存中。此方法最大限度的确保他返回此缓冲区内容与物理内存中,调用此方法可能会导致页面错误,并导致发生I/O操作。

boolean isLoad()方法的作用是判断此缓冲区的内容位于物理内存中

如果为true意味着此缓冲区中所有数据极有可能都位于物理内存中,因此是可访问的,不会出现虚拟也错误,无需任何IO操作。返回值为false不一定意味着缓冲区的内容不位于物理内存中,返回值是一个提示,而不是保证。

本测试要在linux系统进行,因为在windows系统中调用isLoaded()方法永远返回false。

5.20 打开一个文件

FileChannel open(Path path,OpenOption…options)方法的作用是打开一个文件,以便对这个文件进行后期处理

参数:

  • Path是一个接口类型,表示文件的路径
  • OpenOption代表以什么样的方式打开或创建一个文件。OpenOption也是一个接口

OpenOption接口的实现类通常由StandardOpenOption枚举进行代替。其内部有若干常量,下面介绍这些常量的使用。

1.枚举常量CREATE和WRITE的使用

CREATE:创建一个新的文件(如果他不存在)

WRITE:打开文件进行写入访问,前提改文件必须存在

注意:使用CREATE不能单独的创建文件,还必须使用WRITE才行

测试代码如下

如果只加了CREATE就不行,必须有WRITE

@Test
public void test29(){
    try {
        File file = new File("D:\\xiaoDproject\\nio_project\\src\\main\\resources\\a.txt");
        Path path = file.toPath();
        FileChannel fileChannel = FileChannel.open(path, StandardOpenOption.CREATE, StandardOpenOption.WRITE);
        fileChannel.close();
    }catch (IOException e){
        e.printStackTrace();
    }
}

2.枚举常量APPEND的使用

注意:不需要加上WRITE了,因为APPEND默认就是可以写入的

如果打开文件以写入访问,则字节将写入文件末尾而不是开始处

@Test
public void test30(){
    try {
        FileChannel fileChannel = FileChannel.open(new File("D:\\xiaoDproject\\nio_project\\src\\main\\resources\\a.txt").toPath()
                                                   , StandardOpenOption.APPEND);
        fileChannel.write(ByteBuffer.wrap("abcde".getBytes()));
        fileChannel.close();
    }catch (IOException e){
        e.printStackTrace();
    }
}

3.枚举常量TRUNCATE_EXISTING的使用

如果文件存在并且为写入方式打开,则其长度被截断为0.如果只读,则此项无效.

测试程序如下

结果a.txt内容被置空了

@Test
public void test31(){
    try {
        FileChannel fileChannel = FileChannel.open(new File("D:\\xiaoDproject\\nio_project\\src\\main\\resources\\a.txt").toPath(),                                             StandardOpenOption.WRITE,StandardOpenOption.TRUNCATE_EXISTING);
        fileChannel.close();
    }catch (IOException e){
        e.printStackTrace();
    }
}

4.枚举常量READ使用

@Test
public void test32(){
    try {
        File file = new File("D:\\xiaoDproject\\nio_project\\src\\main\\resources\\a.txt");
        FileChannel fileChannel = FileChannel.open(file.toPath(),
                                                   StandardOpenOption.READ);
        byte[] bytes = new byte[(int)file.length()];
        ByteBuffer byteBuffer = ByteBuffer.wrap(bytes);
        fileChannel.read(byteBuffer);
        byteBuffer.flip();
        while(byteBuffer.hasRemaining()){
            System.out.println((char)byteBuffer.get());
        }
        fileChannel.close();
    }catch (IOException e){
        e.printStackTrace();
    }
}

5.枚举常量CREATE_NEW

创建一个新文件,如果文件已存在就出现异常

6.DELETE_ON_CLOSE

关闭时删除,实现最大努力的在关闭通话时删除改文件。如果未调用close()方法,则在虚拟机终止时尝试删除改文件,使用此选项是要非常小心。

7.枚举常量SPARSE的使用

作用:与CREATE_NEW选项一起使用时,此选项提供了提示,表名新文件将是稀疏的,当文件系统不支持创建稀疏文件时,将忽略该选项。

假设现在创建12G的文件,但是文件中只存储了一个字节,其实可以使用SPARSE来创建稀疏文件,当内容扩充是在进行扩大。

8.枚举常量SYNC

要求对文件内容或元数据的每次更新都同步写入底层存储设备。如果这样做程序运行的效率就会很低。

9.枚举DSYNC

枚举常量DSYNC的作用:要求对文件内容的每次更新都同步写入底层设备。和SYNC的区别:SYNC更新内容和袁术,而DSYNC只更新内容

5.21 判断通道是否打开

public final booelan isOpen()

程序如下所示,结果打印

true
false
@Test
public void test33(){
    try {
        RandomAccessFile raf = new RandomAccessFile("D:\\xiaoDproject\\nio_project\\src\\main\\resources\\a.txt","rw");
        FileChannel channel = raf.getChannel();
        System.out.println("open status -> "+channel.isOpen());
        channel.close();
        System.out.println("open status -> "+channel.isOpen());
    }catch (IOException e){
        e.printStackTrace();
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值