传统的io中,数据通过流传输;在nio中,数据放在缓冲区中进行管理,通过通道进行传输
1.通道接口层次
1.1相关接口介绍
根基接口Channel
public interface Channel extends Closeable {
//通道是否处于开启状态
public boolean isOpen();
//因为通道开启也需要关闭,所以实现了Closeable接口,所以这个方法懂的都懂
public void close() throws IOException;
}
定义读写操作的接口
public interface ReadableByteChannel extends Channel {
//将通道中的数据读取到给定的缓冲区中
public int read(ByteBuffer dst) throws IOException;
}
public interface WritableByteChannel extends Channel {
//将给定缓冲区中的数据写入到通道中
public int write(ByteBuffer src) throws IOException;
}
读写的整合
public interface ByteChannel extends ReadableByteChannel, WritableByteChannel{
}
ByteChannel下的衍生接口
//允许保留position和更改position的通道,以及对通道连接实体的相关操作
public interface SeekableByteChannel extends ByteChannel {
...
//获取当前的position
long position() throws IOException;
//修改当前的position
SeekableByteChannel position(long newPosition) throws IOException;
//返回此通道连接到的实体(比如文件)的当前大小
long size() throws IOException;
//将此通道连接到的实体截断(比如文件,截断之后,文件后面一半就没了)为给定大小
SeekableByteChannel truncate(long size) throws IOException;
}
响应中断的接口
public interface InterruptibleChannel extends Channel {
//当其他线程调用此方法时,在此通道上处于阻塞状态的线程会直接抛出 AsynchronousCloseException 异常
public void close() throws IOException;
}
//这是InterruptibleChannel的抽象实现,完成了一部分功能
public abstract class AbstractInterruptibleChannel implements Channel, InterruptibleChannel {
//加锁关闭操作用到
private final Object closeLock = new Object();
//当前Channel的开启状态
private volatile boolean open = true;
protected AbstractInterruptibleChannel() { }
//关闭操作实现
public final void close() throws IOException {
synchronized (closeLock) { //同时只能有一个线程进行此操作,加锁
if (!open) //如果已经关闭了,那么就不用继续了
return;
open = false; //开启状态变成false
implCloseChannel(); //开始关闭通道
}
}
//该方法由 close 方法调用,以执行关闭通道的具体操作,仅当通道尚未关闭时才调用此方法,不会多次调用。
protected abstract void implCloseChannel() throws IOException;
public final boolean isOpen() {
return open;
}
//开始阻塞(有可能一直阻塞下去)操作之前,需要调用此方法进行标记,
protected final void begin() {
...
}
//阻塞操作结束之后,也需要需要调用此方法,为了防止异常情况导致此方法没有被调用,建议放在finally中
protected final void end(boolean completed)
...
}
...
}
1.2 使用通道读取数据
public static void main(String[] args) throws IOException {
//缓冲区创建好,一会就靠它来传输数据
ByteBuffer buffer = ByteBuffer.allocate(10);
//将System.in作为输入源,一会Channel就可以从这里读取数据,然后通过缓冲区装载一次性传递数据
ReadableByteChannel readChannel = Channels.newChannel(System.in);
while (true) {
//将通道中的数据写到缓冲区中,缓冲区最多一次装10个
readChannel.read(buffer);
//写入操作结束之后,需要进行翻转,以便接下来的读取操作
buffer.flip();
//最后转换成String打印出来康康
System.out.println("读取到一批数据:"+new String(buffer.array(), 0, buffer.remaining()));
//回到最开始的状态
buffer.clear();
}
}
注:Channel不像流那样是单向的,它就像它的名字一样,一个通道可以从一端走到另一端,也可以从另一端走到这一端
2.文件传输
相比传统的文件输入输出,需要至少两个流,一个完成读操作一个完成写操作,但是channel通过buffer就可以轻易实现读写转换,从而只需要一个类来完成
public static void main(String[] args) throws IOException {
/*
通过RandomAccessFile进行创建,注意后面的mode有几种:
r 以只读的方式使用
rw 读操作和写操作都可以
rws 每当进行写操作,同步的刷新到磁盘,刷新内容和元数据
rwd 每当进行写操作,同步的刷新到磁盘,刷新内容
*/
try(RandomAccessFile f = new RandomAccessFile("test.txt", "rw"); //这里设定为支持读写,这样创建的通道才能具有这些功能
FileChannel channel = f.getChannel()){ //通过RandomAccessFile创建一个通道
channel.write(ByteBuffer.wrap("伞兵二号马飞飞准备就绪!".getBytes()));
System.out.println("写操作完成之后文件访问位置:"+channel.position()); //注意读取也是从现在的位置开始
channel.position(0); //需要将位置变回到最前面,这样下面才能从文件的最开始进行读取
ByteBuffer buffer = ByteBuffer.allocate(128);
channel.read(buffer);
buffer.flip();
System.out.println(new String(buffer.array(), 0, buffer.remaining()));
}
}
注:通过FileInputStream和FileOutputStream获取的channel也只能完成一种操作
对文件进行截断
public static void main(String[] args) throws IOException {
try(RandomAccessFile f = new RandomAccessFile("test.txt", "rw");
FileChannel channel = f.getChannel()){
//截断文件,只留前20个字节
channel.truncate(20);
ByteBuffer buffer = ByteBuffer.allocate(128);
channel.read(buffer);
buffer.flip();
System.out.println(new String(buffer.array(), 0, buffer.remaining()));
}
}
文件复制
public static void main(String[] args) throws IOException {
try(FileOutputStream out = new FileOutputStream("test2.txt");
FileInputStream in = new FileInputStream("test.txt")){
FileChannel inChannel = in.getChannel(); //获取到test文件的通道
inChannel.transferTo(0, inChannel.size(), out.getChannel()); //直接将test文件通道中的数据转到test2文件的通道中
// 或者反向可以使用transferFrom
}
}
文件编辑
此处使用的就是DirectByteBuffer直接缓冲区,效率还是很高的。
//注意一定要是可写的,不然无法进行修改操作
try(RandomAccessFile f = new RandomAccessFile("test.txt", "rw");
FileChannel channel = f.getChannel()){
//通过map方法映射文件的某一段内容,创建MappedByteBuffer对象
//比如这里就是从第四个字节开始,映射10字节内容到内存中
//注意这里需要使用MapMode.READ_WRITE模式,其他模式无法保存数据到文件
MappedByteBuffer buffer = channel.map(FileChannel.MapMode.READ_WRITE, 4, 10);
//我们可以直接对在内存中的数据进行编辑,也就是编辑Buffer中的内容
//注意这里写入也是从pos位置开始的,默认是从0开始,相对于文件就是从第四个字节开始写
//注意我们只映射了10个字节,也就是写的内容不能超出10字节了
buffer.put("yyds".getBytes());
//编辑完成后,通过force方法将数据写回文件的映射区域
buffer.force();
}
3.文件锁
-
我们可以创建一个跨进程文件锁来防止多个进程之间的文件争抢操作(注意这里是进程,不是线程)FileLock是文件锁,它能保证同一时间只有一个进程(程序)能够修改它,或者都只可以读,这样就解决了多进程间的同步文件,保证了安全性。但是需要注意的是,它进程级别的,不是线程级别的,他可以解决多个进程并发访问同一个文件的问题,但是它不适用于控制同一个进程中多个线程对一个文件的访问。
-
加锁操作
-
public static void main(String[] args) throws IOException, InterruptedException { //创建RandomAccessFile对象,并拿到Channel RandomAccessFile f = new RandomAccessFile("test.txt", "rw"); FileChannel channel = f.getChannel(); System.out.println(new Date() + " 正在尝试获取文件锁..."); //接着我们直接使用lock方法进行加锁操作(如果其他进程已经加锁,那么会一直阻塞在这里) //加锁操作支持对文件的某一段进行加锁,比如这里就是从0开始后的6个字节加锁,false代表这是一把独占锁 true代表是共享锁 //范围锁甚至可以提前加到一个还未写入的位置上 FileLock lock = channel.lock(0, 6, false); System.out.println(new Date() + " 已获取到文件锁!"); Thread.sleep(5000); //假设要处理5秒钟 System.out.println(new Date() + " 操作完毕,释放文件锁!"); //操作完成之后使用release方法进行锁释放 lock.release(); }
-
-
有关共享锁和独占锁:
- 进程对文件加独占锁后,当前进程对文件可读可写,独占此文件,其它进程是不能读该文件进行读写操作的。
- 进程对文件加共享锁后,进程可以对文件进行读操作,但是无法进行写操作,共享锁可以被多个进程添加,但是只要存在共享锁,就不能添加独占锁。
-
非阻塞的方式加锁
tryLock()
,如果失败则返回null