关于FileChannel的解释及用途网络上的资料已经解释的很清楚了, 总的概括来说 FileChannel 是 用于读、写、映射、维护一个文件的通道, 这里不再赘述
起因▼
FileChannel中提供了两个方法 transferFrom(ReadableByteChannel src, long position, long count) 和 transferTo(long position, long count, WritableByteChannel target)用于两个通道间的数据传输, 通常使用单线程进行传输的时候这两个方法不会出现什么问题, 但是 当我使用多线程方式 进行文件的复制的时候, transferFrom 方法最后传输的总是 count的长度(count是每个线程平均分配处理的长度). 这个问题困扰我两天,并且各处资料都没有详细的介绍关于这两个方法在多线程情况下的问题, 在此做个记录, 也希望后来者乘凉.
问题复现▼
以下代码测试两个方法使用多线程 copy文件
package com.example.day1nio;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.channels.FileChannel;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class BlogTest {
public static void main(String[] args) {
String url = "D:\\Study\\test.rar";
String copyUrl = "D:\\Study\\copy.rar";
try{
File testFile = new File(url);
long size = testFile.length();
//定义每个线程处理的文件大小
long subFileCount = 20 * 1024 * 1024;// 20M
//定义线程池 控制同时处理的线程数 可以避免磁盘占用过高
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(50);
//计算需要多少线程处理
long threadCount = size % subFileCount == 0 ? size/subFileCount : size/subFileCount +1;
System.out.println("需要线程数:"+threadCount);
CountDownLatch cdl = new CountDownLatch((int) threadCount);
for(long i=0,position=0; i<threadCount; i++,position=i*subFileCount){
if(size - position < subFileCount){
subFileCount = size - position;
}
fixedThreadPool.execute(new FileThread(url, copyUrl, position, subFileCount, cdl));
}
cdl.await();
System.out.println(cdl.getCount());
System.out.println("文件复制完毕");
fixedThreadPool.shutdown();
} catch (Exception e){
e.printStackTrace();
}
}
private static class FileThread extends Thread {
private String fileUrl;
private String copyUrl;
private long position;
private long count;
private CountDownLatch cdl;
public FileThread(String fileUrl, String copyUrl, long position, long count, CountDownLatch cdl) {
this.fileUrl = fileUrl;
this.copyUrl = copyUrl;
this.position = position;
this.count = count;
this.cdl = cdl;
}
@Override
public void run() {
RandomAccessFile fileRaf = null;
RandomAccessFile copyRaf = null;
FileChannel fileChannel = null;
FileChannel copyChannel = null;
try {
fileRaf = new RandomAccessFile(new File(fileUrl), "rw");
copyRaf = new RandomAccessFile(new File(copyUrl), "rw");
fileChannel = fileRaf.getChannel();
copyChannel = copyRaf.getChannel();
copyChannel.position(position);
fileChannel.position(position);
//使用两个方法进行测试
// fileChannel.transferTo(position, count, copyChannel);
copyChannel.transferFrom(fileChannel, position, count);
} catch (Exception e) {
e.printStackTrace();
} finally {
cdl.countDown();
closeStream(copyChannel, fileChannel, copyRaf, fileRaf);
}
}
}
//关闭流
public static void closeStream(Closeable...closeables){
for (Closeable closeable : closeables) {
if (closeable != null){
try {
closeable.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
使用transferFrom copy的文件▼
使用transferTo copy的文件▼
上面两张图片可以看出, 使用transferFrom copy的文件总是定义的每个线程分配的处理大小 20M, transferTo copy的文件是大小正常且没有损坏, 具体原因往下看
分析▼
从方法名来看 transferTo 是将当前通道数据写到另一个通道, transferFrom 是从另一个通道拿数据到当前通道, 通过这个思路思考一下, 是什么原因呢?
transferTo 是将当前通道数据写到另一个通道, 对象是当前通道, 所以我们不用考虑另一个通道的什么, 我就把文件数据写给你就行了, 而 transferFrom 是从另一个通道拿数据到当前通道, 在复制文件的情况下, 新建立的当前通道是空白的, 我们只知道要写到哪个文件,但是不知道文件的大小, 所以默认每一次就从0开始写入, 从而导致文件最后的大小是线程分配大小. 所以,在建立文件的同时,我们就给它指定一个大小, 然后每一次从指定的位置开始写入, 那么就可以完整的复制一个文件了
修改上面代码中的 静态内部线程类▼
与以上代码相比,只增加了一行代码
//指定文件大小
copyRaf.setLength(fileRaf.length());
private static class FileThread extends Thread {
private String fileUrl;
private String copyUrl;
private long position;
private long count;
private CountDownLatch cdl;
public FileThread(String fileUrl, String copyUrl, long position, long count, CountDownLatch cdl) {
this.fileUrl = fileUrl;
this.copyUrl = copyUrl;
this.position = position;
this.count = count;
this.cdl = cdl;
}
@Override
public void run() {
RandomAccessFile fileRaf = null;
RandomAccessFile copyRaf = null;
FileChannel fileChannel = null;
FileChannel copyChannel = null;
try {
fileRaf = new RandomAccessFile(new File(fileUrl), "rw");
copyRaf = new RandomAccessFile(new File(copyUrl), "rw");
//指定文件大小
copyRaf.setLength(fileRaf.length());
fileChannel = fileRaf.getChannel();
copyChannel = copyRaf.getChannel();
copyChannel.position(position);
fileChannel.position(position);
//使用两个方法进行测试
fileChannel.transferTo(position, count, copyChannel);
// copyChannel.transferFrom(fileChannel, position, count);
} catch (Exception e) {
e.printStackTrace();
} finally {
cdl.countDown();
closeStream(copyChannel, fileChannel, copyRaf, fileRaf);
}
}
}
欢迎关注我的微信公众号 抓几个娃 聊点技术,聊点生活
以上