据说大连某211高校的信息学院的李教授非常好这口,他带的每个操作系统本科班,每个学期都必须完成这个程序,不过网上关于这方面的资料甚少,就只有一份C语言版。
然而,那份被历届学生已经抄烂,改实验结果把李教授忽悠了N年的C语言版,所使用的类、所开的线程与进程也不甚合理,把一个本来非常简单的程序搞得十分冗长。明明这个线程并发拷贝程序只涉及到线程的互斥的方面,与线程的同步半点关系,这个线程并发拷贝程序的互斥,也就是每个线程操作的文件只能是一个,保证不出现一个文件被多个文件操作的情况即可,做完拷贝之后根本就无须做其它行为,根本就无须与其它程序进行通信。那份C语言版,居然还要用管道实现,让诸位不明真相的学子,更加分不清是做什么的。管道在之前的《【Java】线程管道通讯》(点击打开链接)我已经说得很详细,这里不在赘述。
同时,李教授此题也并不合理,即使没有实现线程的互斥,由于是拷贝程序,根本就不会出现原文件夹的文件没有的情况,即使线程互斥没有实现,此程序照样能跑,只不过后来拷贝过来的文本覆盖掉前面拷贝过来的文件而已。要是给我来出题,应该把此题改成“线程并发剪切程序”,那样才能让学生真正明白线程互斥的重要性。
但是,有一点还是要表扬李教授的,这道题目绝对是他自己独创,网上找不到任何资料,要是拉去查重绝对过关,可惜每个学期李教授总是让学生自己解决问题,从来不公布参考答案让学生学习,这一点真的非常不好。
为了让以后的诸位学子不再深受其害,解决各种找不到资料,没有人问的痛苦。我写作了本文,给各位后来者用自己比较擅长的Java来好好地解释下什么是线程并发拷贝程序。
一、基本目标
我们要做出这样的一个程序:
首先在f盘有两个文件夹a与b,f:\a里面放着一堆文件,里面还有一个文件夹“新建文件夹”,而且这个文件夹里面也是一堆文件。
完成的功能与《【Java】利用文件输入输出流完成把一个文件夹内的所有文件拷贝的另一的文件夹的操作》(点击打开链接)类似,
程序运行之后,f:\a里面的所有文件都会拷贝到f:\b中,而且f:\b中的所有文件都会加上的rename_的前缀,据说李教授非常喜欢让学生在拷贝的文件里面加上自己的学号,以保持每个人的原创性,对此,我只能表示呵呵了,你弄懂整个程序之后,我再每个文件前面加上“李教授abc”都可以^_^
不过,此线程并发拷贝程序与《【Java】利用文件输入输出流完成把一个文件夹内的所有文件拷贝的另一的文件夹的操作》(点击打开链接)中不同的是,大家请注意eclipse里面的运行结果,每个文件的复制是由不同的线程完成的。
这样对比于《【Java】利用文件输入输出流完成把一个文件夹内的所有文件拷贝的另一的文件夹的操作》(点击打开链接)的单线程,如果要拷贝上千个的大文件,这个程序显然是更加优秀。
本文为了让大家看清实验结果,故意让每次线程拷贝之后停留1秒。
二、基本思想
据说,李教授除了让学生完成程序,还必须完成流程图,以保证每个学生真的是想懂……结果一大堆完全相同的流程图出现了……
其实,本程序每个线程的流程图是这样的:
至于关于拷贝文件的流程,可以参考我之前的《【Java】利用文件输入输出流完成把一个文件夹内的所有文件拷贝的另一的文件夹的操作》(点击打开链接),线程并发、互斥与同步的流程,可以参考我之前的《【Java】线程并发、互斥与同步》(点击打开链接),甚至你还搞不清楚什么是Java输入输出流,可以参考我之前的《【Java】打印流与缓冲区读者完成输入与输出到文件操作》(点击打开链接),对了,本文还用到了临界区与临界资源的概念,也不懂的可以参考我之前的《【Java】利用synchronized(this)完成线程的临界区》(点击打开链接),新手向,有教无类,诲人不倦,保证只要会写Java绝对看得懂。
三、制作过程
1、首先与《【Java】线程并发、互斥与同步》(点击打开链接)一样要在主函数ThreadFileCopy定义一个进程FileCopy,然后在里面开四条进程:
public class ThreadFileCopy {
public static void main(String[] args) throws Exception {
FileCopy filecopy = new FileCopy("f:/a", "f:/b");
new Thread(filecopy, "线程1").start();
new Thread(filecopy, "线程2").start();
new Thread(filecopy, "线程3").start();
new Thread(filecopy, "线程4").start();
}
}
进程类FileCopy有带参数的构造函数,需要你传递拷贝原路径与拷贝新路径进去,这里与《【Java】利用文件输入输出流完成把一个文件夹内的所有文件拷贝的另一的文件夹的操作》(点击打开链接)的一样。
其实整个程序就是《【Java】利用synchronized(this)完成线程的临界区》(点击打开链接)与《【Java】利用文件输入输出流完成把一个文件夹内的所有文件拷贝的另一的文件夹的操作》(点击打开链接)两个程序合并,《【Java】利用synchronized(this)完成线程的临界区》(点击打开链接)是为了线程并发,《【Java】利用文件输入输出流完成把一个文件夹内的所有文件拷贝的另一的文件夹的操作》(点击打开链接)是为拷贝,所以有了如下的爱情结晶:
class FileCopy implements Runnable {
//首先是构造函数、成员声明部分
private String oldPath;
private String newPath;
//这个i是游标,记录到拷贝的第几个个文件的游标,拷贝完毕这个游标是向下移
//同时这个i也是所谓的“临界资源”
private int i = 0;
public FileCopy(String oldPath, String newPath) {
this.oldPath = oldPath;
this.newPath = newPath;
}
public void run() {
try {
(new File(newPath)).mkdirs();
File filelist = new File(oldPath);
String[] file = filelist.list();
File temp = null;
//file.length取出文件的总数
while (i < file.length) {
synchronized (this) {
//复制文件的操作部分是临界区,只允许一条线程进入操作
if (oldPath.endsWith(File.separator)) {
temp = new File(oldPath + file[i]);
} else {
temp = new File(oldPath + File.separator + file[i]);
}
System.out.println(Thread.currentThread().getName()
+ "正在复制" + temp);
if (temp.isFile()) {
FileInputStream input = new FileInputStream(temp);
//复制到新位置并加前缀rename_就这句,拯救各位在忘期末设计的苦逼们!
FileOutputStream output = new FileOutputStream(newPath
+ "/" + "rename_" + (temp.getName()).toString());
byte[] bufferarray = new byte[1024 * 64];
int prereadlength;
while ((prereadlength = input.read(bufferarray)) != -1) {
output.write(bufferarray, 0, prereadlength);
}
output.flush();
output.close();
input.close();
}
if (temp.isDirectory()) {
FileCopy(oldPath + "/" + file[i], newPath + "/"
+ file[i]);
}
i++;
}
//每条线程操作完毕,就睡1秒,休息一下,毕竟我想看到实验结果
Thread.currentThread().sleep(1000);
}
} catch (Exception e) {
//如果程序读完就停止进程
Thread.yield();
}
}
//这个与上面完全一样的方法,纯粹为了实现如果遇到游标文件夹的话,则用迭代,完成文件夹内文件的复制操作
private void FileCopy(String oldPath, String newPath) {
try {
(new File(newPath)).mkdirs();
File filelist = new File(oldPath);
String[] file = filelist.list();
File temp = null;
for (int i = 0; i < file.length; i++) {
if (oldPath.endsWith(File.separator)) {
temp = new File(oldPath + file[i]);
} else {
temp = new File(oldPath + File.separator + file[i]);
}
System.out.println(Thread.currentThread().getName() + "正在复制"
+ temp);
if (temp.isFile()) {
FileInputStream input = new FileInputStream(temp);
FileOutputStream output = new FileOutputStream(newPath
+ "/" + "rename_" + (temp.getName()).toString());
byte[] bufferarray = new byte[1024 * 64];
int prereadlength;
while ((prereadlength = input.read(bufferarray)) != -1) {
output.write(bufferarray, 0, prereadlength);
}
output.flush();
output.close();
input.close();
}
if (temp.isDirectory()) {
FileCopy(oldPath + "/" + file[i], newPath + "/" + file[i]);
}
}
} catch (Exception e) {
Thread.yield();
}
}
}
3、因此整个程序如下:
import java.io.*;
class FileCopy implements Runnable {
//首先是构造函数、成员声明部分
private String oldPath;
private String newPath;
//这个i是游标,记录到拷贝的第几个个文件的游标,拷贝完毕这个游标是向下移
//同时这个i也是所谓的“临界资源”
private int i = 0;
public FileCopy(String oldPath, String newPath) {
this.oldPath = oldPath;
this.newPath = newPath;
}
public void run() {
try {
(new File(newPath)).mkdirs();
File filelist = new File(oldPath);
String[] file = filelist.list();
File temp = null;
//file.length取出文件的总数
while (i < file.length) {
synchronized (this) {
//复制文件的操作部分是临界区,只允许一条线程进入操作
if (oldPath.endsWith(File.separator)) {
temp = new File(oldPath + file[i]);
} else {
temp = new File(oldPath + File.separator + file[i]);
}
System.out.println(Thread.currentThread().getName()
+ "正在复制" + temp);
if (temp.isFile()) {
FileInputStream input = new FileInputStream(temp);
//复制到新位置并加前缀rename_就这句,拯救各位在忘期末设计的苦逼们!
FileOutputStream output = new FileOutputStream(newPath
+ "/" + "rename_" + (temp.getName()).toString());
byte[] bufferarray = new byte[1024 * 64];
int prereadlength;
while ((prereadlength = input.read(bufferarray)) != -1) {
output.write(bufferarray, 0, prereadlength);
}
output.flush();
output.close();
input.close();
}
if (temp.isDirectory()) {
FileCopy(oldPath + "/" + file[i], newPath + "/"
+ file[i]);
}
i++;
}
//每条线程操作完毕,就睡1秒,休息一下,毕竟我想看到实验结果
Thread.currentThread().sleep(1000);
}
} catch (Exception e) {
//如果程序读完就停止进程
Thread.yield();
}
}
//这个与上面完全一样的方法,纯粹为了实现如果遇到游标文件夹的话,则用迭代,完成文件夹内文件的复制操作
private void FileCopy(String oldPath, String newPath) {
try {
(new File(newPath)).mkdirs();
File filelist = new File(oldPath);
String[] file = filelist.list();
File temp = null;
for (int i = 0; i < file.length; i++) {
if (oldPath.endsWith(File.separator)) {
temp = new File(oldPath + file[i]);
} else {
temp = new File(oldPath + File.separator + file[i]);
}
System.out.println(Thread.currentThread().getName() + "正在复制"
+ temp);
if (temp.isFile()) {
FileInputStream input = new FileInputStream(temp);
FileOutputStream output = new FileOutputStream(newPath
+ "/" + "rename_" + (temp.getName()).toString());
byte[] bufferarray = new byte[1024 * 64];
int prereadlength;
while ((prereadlength = input.read(bufferarray)) != -1) {
output.write(bufferarray, 0, prereadlength);
}
output.flush();
output.close();
input.close();
}
if (temp.isDirectory()) {
FileCopy(oldPath + "/" + file[i], newPath + "/" + file[i]);
}
}
} catch (Exception e) {
Thread.yield();
}
}
}
public class ThreadFileCopy {
public static void main(String[] args) throws Exception {
FileCopy filecopy = new FileCopy("f:/a", "f:/b");
new Thread(filecopy, "线程1").start();
new Thread(filecopy, "线程2").start();
new Thread(filecopy, "线程3").start();
new Thread(filecopy, "线程4").start();
}
}
四、展望
这个程序还存在一点缺陷,因为一旦文件游标遇到文件夹的时候,使用迭代的方法完成拷贝,而此时操作的线程依旧存在于临界区,所以,如果文件游标遇到f:\a里面的文件夹“新建文件夹”里面的文件,只能使用单线程来完成“新建文件夹”里面的文件的拷贝工作,无法并发,毕竟迭代这种方式在实际编程中,你最好不要这样来玩,会形成高的时间复杂度与空间复杂度,这还是其次,主要是其他人比较难以看懂。如何在迭代中完成线程的并发是一个值得思考的问题,但是在根目录下的文件拷贝绝对是线程并发的。