java多线程实现复制功能并统计进度
业务描述
复制某目录下的一个大文件,要求使用10个线程同时工作。并且统计复制的完成度,类似于进度条的功能。
业务分析
步骤:
1、在使用多线程进行拷贝的时候,首先要知道文件的大小 然后根据线程的数量,计算出每个线程的工作的数量。需要一个拷贝的类,进行复制,初始化线程数组
2、创建一个统计文件复制进度的线程类。
3、拷贝线程。
4、由于Java的简单类型不能够精确的对浮点数进行运算,提供一个java工具类,对浮点数进行计算。
5、创建主函数类进行测试。
代码如下:
package javasimple; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.RandomAccessFile; import java.math.BigDecimal; public class ThreadCopyFile { /** * @param args */ public static void main(String[] args) { Copyer copyer = new Copyer(); copyer.copy(new File("E:\\gcfr\\hqreportnew.war"), "E:\\", 10); } } /** * 该类执行文件的拷贝功能 * @author haokui * */ class Copyer { private CopyThread[] threads;// 存放所有拷贝线程的数组 /** * 使用多线程去拷贝一个大文件, 1 在使用多线程进行拷贝的时候,首先要知道文件的大小 然后根据线程的数量,计算出每个线程的工作的数量 * 2.然后创建线程,执行拷贝的工作 * * @param scrFile * 源文件 * @param desPath * 目标路径 * @param threadNum * 要使用的线程数量 */ public void copy(File srcFile, String desPath, int threadNum) { // 1.取得文件的大小 long fileLeng = srcFile.length(); System.out.println("文件大小:" + fileLeng); // 2.根据线程数量,计算每个线程的工作量 long threadPerSize = fileLeng / threadNum; // 3.计算出每个线程的开始位置和结束位置 long startPos = 0; long endPos = threadPerSize; // 取得目标文件的文件名信息 String fileName = srcFile.getName(); String desPathAndFileName = desPath + File.separator + fileName; // 初始化线程的数组 threads = new CopyThread[threadNum]; for (int i = 0; i < threadNum; i++) { // 由最后一个线程承担剩余的工作量 if (i == threadNum - 1) { threads[i] = new CopyThread("拷贝线程" + i, srcFile, desPathAndFileName, startPos, fileLeng); } else { // 创建一个线程 threads[i] = new CopyThread("拷贝线程" + i, srcFile, desPathAndFileName, startPos, endPos); } startPos += threadPerSize; endPos += threadPerSize; } // 创建统计线程 new ScheduleThread("统计线程", fileLeng,threads ); } } /** * 负责统计文件拷贝进度的线程 * @author haokui * */ class ScheduleThread extends Thread { private long fileLength; // 文件的大小 private CopyThread[] threads;// 存放所有的拷贝线程的数组 /** * 统计进度线程的构造方法 * * @param name * 线程的名字 * @param fileLeng * 文件的长度 * @param threads * 拷贝线程的数组 */ public ScheduleThread(String name, long fileLength, CopyThread[] threads) { super(name); this.fileLength = fileLength; this.threads = threads; this.start(); } /** * 判断所有的拷贝线程是否已经结束 * * @return 是否结束 */ private boolean isOver() { if (threads != null) { for (CopyThread t : threads) { if (t.isAlive()) { return false; } } } return true; } public void run() { while (!isOver()) { long totalSize = 0; for (CopyThread t : threads) { totalSize += t.getCopyedSize(); } /** * 由于复制功能要比这些代码耗时,所以稍微延迟一下,不用计算的太频繁,最好是一个线程干完之后计算一次,这里就直接给延迟一下就ok,不做精确的处理了。 */ try { Thread.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); } double schedule = Arith.div((double) totalSize, (double) fileLength, 4); System.err.println("文件的拷贝进度:===============>" + schedule * 100 + "%"); } System.err.println("统计线程结束了"); } } /** * 拷贝线程 * @author haokui * */ class CopyThread extends Thread { private File srcFile;// 源文件的路径 private String desPath;// 目标路径 private long startPos; // 线程拷贝的开始位置 private long endPost;// 线程拷贝的结束位置 private long alreadyCopySize;// 线程已经拷贝的位置 private RandomAccessFile rin; // 读取文件的随机流 private RandomAccessFile rout;// 写入文件的随机流 /** * 取得 线程已经拷贝文件的大小 * * @return 线程已经拷贝文件的大小 */ public long getCopyedSize() { return alreadyCopySize - startPos; } /** * 线程的构造方法 * * @param threadName * 线程的名字 * @param scrFile * 源文件 * @param desPathAndName * 目标文件的路径及其名称 * @param startPos * 线程的开始位置 * @param endPost * 线程的结束位置 */ public CopyThread(String threadName, File srcFile, String desPathAndName, long startPos, long endPos) { super(threadName); this.srcFile = srcFile; this.desPath = desPath; this.startPos = startPos; this.endPost = endPos; this.alreadyCopySize = this.startPos; // System.out.println(this.getName() + "开始位置:" + startPos + " 结束位置:" // + endPos); // 初始化随机输入流,输出流 try { rin = new RandomAccessFile(srcFile, "r"); rout = new RandomAccessFile(desPathAndName, "rw"); // 定位随机流的开始位置 rin.seek(startPos); rout.seek(startPos); // 开始线程 this.start(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } public void run() { int len = 0; byte[] b = new byte[1024]; try { while ((alreadyCopySize < endPost) && (len = rin.read(b)) != -1) { alreadyCopySize = alreadyCopySize + len; if (alreadyCopySize >= this.endPost) { int oldSize = (int) (alreadyCopySize - len); len = (int) (this.endPost - oldSize); alreadyCopySize = oldSize + len; } rout.write(b, 0, len); } System.out.println(this.getName() + " 在工作: 开始位置:" + this.startPos + " 拷贝了:" + (this.endPost - this.startPos) + " 结束位置:" + this.endPost); } catch (IOException e) { e.printStackTrace(); } finally { try { if (rin != null) { rin.close(); } } catch (IOException e) { e.printStackTrace(); } try { if (rout != null) { rout.close(); } } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } } /** * 由于Java的简单类型不能够精确的对浮点数进行运算, * 这个工具类提供精 确的浮点数运算,包括加减乘除和四舍五入。 * @author haokui * */ class Arith { // 默认除法运算精度 private static final int DEF_DIV_SCALE = 10; // 这个类不能实例化 private Arith() { } /** * 提供精确的加法运算。 * * @param v1 * 被加数 * @param v2 * 加数 * @return 两个参数的和 */ public static double add(double v1, double v2) { BigDecimal b1 = new BigDecimal(Double.toString(v1)); BigDecimal b2 = new BigDecimal(Double.toString(v2)); return b1.add(b2).doubleValue(); } /** * 提供精确的减法运算。 * * @param v1 * 被减数 * @param v2 * 减数 * @return 两个参数的差 */ public static double sub(double v1, double v2) { BigDecimal b1 = new BigDecimal(Double.toString(v1)); BigDecimal b2 = new BigDecimal(Double.toString(v2)); return b1.subtract(b2).doubleValue(); } /** * 提供精确的乘法运算。 * * @param v1 * 被乘数 * @param v2 * 乘数 * @return 两个参数的积 */ public static double mul(double v1, double v2) { BigDecimal b1 = new BigDecimal(Double.toString(v1)); BigDecimal b2 = new BigDecimal(Double.toString(v2)); return b1.multiply(b2).doubleValue(); } /** * 提供(相对)精确的除法运算,当发生除不尽的情况时,精确到 小数点以后10位,以后的数字四舍五入。 * * @param v1 * 被除数 * @param v2 * 除数 * @return 两个参数的商 */ public static double div(double v1, double v2) { return div(v1, v2, DEF_DIV_SCALE); } /** * 提供(相对)精确的除法运算。当发生除不尽的情况时,由scale参数指 定精度,以后的数字四舍五入。 * * @param v1 * 被除数 * @param v2 * 除数 * @param scale * 表示表示需要精确到小数点以后几位。 * @return 两个参数的商 */ public static double div(double v1, double v2, int scale) { if (scale < 0) { throw new IllegalArgumentException( "The scale must be a positive integer or zero"); } BigDecimal b1 = new BigDecimal(Double.toString(v1)); BigDecimal b2 = new BigDecimal(Double.toString(v2)); return b1.divide(b2, scale, BigDecimal.ROUND_HALF_UP).doubleValue(); } /** * 提供精确的小数位四舍五入处理。 * * @param v * 需要四舍五入的数字 * @param scale * 小数点后保留几位 * @return 四舍五入后的结果 */ public static double round(double v, int scale) { if (scale < 0) { throw new IllegalArgumentException( "The scale must be a positive integer or zero"); } BigDecimal b = new BigDecimal(Double.toString(v)); BigDecimal one = new BigDecimal("1"); return b.divide(one, scale, BigDecimal.ROUND_HALF_UP).doubleValue(); } };
假设复制e:/gcfr下的一个war包到e盘根目录下。运行结果如下:
注意:10个线程同时工作,输出的顺序不一样正式体现。进度最后不是100%是因为统计的时候加了个延时,要看最后一个线程的结束位置,如果和文件的大小相等,表示就复制成功,没有字节丢失。此文件的大小是30995468