突然想写一个多线程下载的小案例。但是并不知道怎么做,所以想先写一个多线程复制文件的小案例。
搜遍全网,都没有找到一个类似的。不过还是找到一篇有参考价值的博文:jAVA基础 提高文件复制性能之多线程复制文件
以及这篇java多线程复制文件,RandomAccessFile类
想起每次面试都会被问起:怎么实现多线程下载?很慌。每次回答都是支支吾吾的。毕竟,我不能直接说,我都是用开源框架直接实现的,并没有去看过实现源码~ 一种被支配的恐惧,一直笼罩在我内心…
好了,不多说了,贴一下我实现的代码:
package com.cat.multi.copy;
import java.io.File;
import java.io.IOException;
/**
* Created by cat on 2018/1/20.
* 多线程拷贝
*/
public class Main {
public static void main(String[] args) throws IOException, InterruptedException {
String path = Class.class.getClass().getResource("/").getPath();
String rootPath = new File(path).getParentFile().getParentFile().getParentFile().getPath();
File sf = new File(rootPath, "raw/src/memo_methine.pdf"); // src
File df = new File(rootPath, "raw/dest/memo_methine_copied.pdf"); // dest
if (df.isFile()) {
df.delete();
}
long srcsize = sf.length();
System.out.println("srcSize==" + srcsize);
// long pos = 0;
// long end = srcsize / 2 / 1024 * 1024;
long pos1 = 0;
long end1= srcsize / 2 / 1024 * 1024;
Copy copy1 = new Copy(sf.getAbsolutePath(), df.getAbsolutePath(), pos1, end1);
long pos2 = srcsize / 2 / 1024 * 1024;
long end2 = srcsize;
Copy copy2 = new Copy(sf.getAbsolutePath(), df.getAbsolutePath(), pos2, end2);
new Thread(copy2).start();
// Thread.sleep(10);
new Thread(copy1).start();
}
}
可以看到,调用还是比较简单的,不过要传4个参数srcPath,destPath,pos,end
。为啥要4个参数,这里简要说明一下:
srcPath
和destPath
就不说了,就是源文件路径,以及复制后的目标路径
pos
是啥?是RandomAccessFile#seek(pos)
方法需要的参数。表示偏移位置,从pos 开始去读写数据。
end
是啥?既然是多线程复制,也就是表示如果一个文件是3M
的,然后开3个线程去复制,那么,每个线程都去复制1M
的大小就可以了。所以,end
是和pos
组合使用的,表示,当前线程复制的文件的起点位置和终点位置。(可以把文件理解成一个数据流,流就用节点和长度…)
然后是 Copy.java
的代码:
package com.cat.multi.copy;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
/**
*
*
* Created by cat on 2018/1/20.
* 小应用:多线程拷贝数据
* 分析一下:多线程拷贝是否需要考虑线程安全?
* 1. 是否是多线程运行环境? ---> 是
* 2. 是否需要访问共享数据? ---> 否
* * 为什么说没有访问共享数据,因为调用的时候是为每个线程分别创建了一个Copy 对象
* 3. 操作共享数据是否是多条语句?--> 是
*
*
*/
public class Copy implements Runnable {
private final int bufferSize = 1024;
private final String srcPath;
private final String dest;
private long pos;
private long end;
/**
* @param dest 全路径 --> /data/storage/camera/dog.jpg
* @param srcPath 全路径
*/
public Copy(String srcPath, String dest, long pos, long end) {
this.srcPath = srcPath;
this.dest = dest;
this.pos = pos;
this.end = end;
}
/**
* 传统的读写文件的方式 --> 可用,但是不能实现分段读写文件。
*
* @throws IOException io
*/
private void copy() throws IOException {
RandomAccessFile bin = new RandomAccessFile(srcPath, "r");
RandomAccessFile bout = new RandomAccessFile(dest, "rw");
// BufferedInputStream bin = new BufferedInputStream(new FileInputStream(src));
// BufferedOutputStream bout = new BufferedOutputStream(new FileOutputStream(df));
byte[] buffer = new byte[1024];
int read;
while ((read = bin.read(buffer)) != -1) {
bout.write(buffer, 0, read);
}
bout.close();
bin.close();
}
/**
* need copy 2
*
* @throws IOException
*/
public void copy1() throws IOException, InterruptedException {
if (this.pos == this.end) {
System.out.println(" 文件已经复制结束了....");
return;
}
Thread.sleep(100);
RandomAccessFile bin = new RandomAccessFile(srcPath, "r");
RandomAccessFile bout = new RandomAccessFile(dest, "rw");
byte[] buffer = new byte[bufferSize];
int read;
long total = 0;
bin.seek(pos);
bout.seek(pos);
while ((read = bin.read(buffer)) != -1) {
bout.write(buffer, 0, read);
total += read;
if (total + pos == this.end) {
System.out.println("我的任务完成了. 当前完成的数据为:" + total);
System.out.println("c1. from:" + pos + " , end=" + end + " , total=" + new File(srcPath).length());
this.pos = this.end;
break;
} else if (total > this.end) {
throw new RuntimeException("end 计算出错了,需要修改程序,否则数据复制会出错...");
}
}
bout.close();
bin.close();
}
@Override
public void run() {
while (true) {
if (this.pos == this.end) {
System.out.println(" 文件已经复制结束了....");
break;
}
try {
copy1();
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
}
}
}
这里重点是Copy#copy1()
这个方法,通过改变pos
实现了对文件的分段复制。
看如下代码片段:
byte[] buffer = new byte[bufferSize];
int read;
long total = 0;
bin.seek(pos); // 关键点在这里
bout.seek(pos); // in 和 out 都需要 seek()
while ((read = bin.read(buffer)) != -1) {
bout.write(buffer, 0, read);
total += read;
if (total + pos == this.end) {
System.out.println("我的任务完成了. 当前完成的数据为:" + total);
System.out.println("c1. from:" + pos + " , end=" + end + " , total=" + new File(srcPath).length());
this.pos = this.end;
break;
} else if (total > this.end) {
throw new RuntimeException("end 计算出错了,需要修改程序,否则数据复制会出错...");
}
}
分段复制的大体思路就是:先计算好每一段需要复制的文件的起点和终点,对应的参数就是pos , end
。然后再复制的时候,按照这个参数去复制就可以了。
pos
这里。bin.seek(pos);bout.seek(pos);
if (total + pos == this.end){...break;}
最后,说明一下,这个多线程复制文件的程序,是不用考虑线程安全的问题的。因为不涉及多个线程对共享数据的写操作。虽然,是多线程共同写目标文件了,但是分工明确,每个线程只写自己的pos,end
的数据。