J'utilise une classe Timer (java.util.Timer
) pour implémenter la fonction de point d'arrêt. J'utilise cette classe pour enregistrer chaque période de temps. Le contenu enregistré est la progression de la copie de chaque fil.
Introduction à la classe Timer :
Une fonctionnalité permettant aux threads de planifier des tâches pour une exécution future dans un thread en arrière-plan. Les tâches peuvent être planifiées pour une exécution unique ou pour une exécution répétée à intervalles réguliers. Outils de planification. tâches pour une exécution future. Les tâches peuvent être programmées pour être exécutées une fois ou être répétées périodiquement.Selon l'introduction de l'API, on peut voir que cette classe Timer ne peut effectuer des tâches qu'une seule fois, ou qu'elle peut effectuer des tâches périodiquement. (Notez que cette classe est la classe java.util.Timer, et non la classe du package javax.) Cette classe a de nombreuses méthodes liées au temps, qui ne seront pas présentées ici. Ceux qui sont intéressés peuvent en apprendre davantage. Je vais seulement les présenter ici.
public void schedule(TimerTask task, long delay, long period)
Planifie la tâche spécifiée pour une exécution répétée à délai fixe commençant après le délai spécifié. Les exécutions ultérieures ont lieu à des intervalles approximativement réguliers séparés par la période spécifiée.Utilisez cette méthode pour enregistrer les informations de progression de la copie de chaque fil à un intervalle de temps fixe. Partie codeClasse de tâches planifiéesPlanifie la tâche spécifiée pour une exécution répétée à délai fixe commençant après le délai spécifié. . Les exécutions ultérieures se produisent à intervalles approximatifs à des intervalles spécifiés.
package dragon.local; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectOutputStream; import java.util.List; import java.util.Timer; import java.util.TimerTask; public class RecordTask extends TimerTask { public static final String filename = "breakPointRecord.txt"; private Timer timer; private List<FileCopyThread> copyThreads; private String outputPath; public RecordTask(Timer timer, List<FileCopyThread> copyThreads, String outputPath) { this.timer = timer; this.copyThreads = copyThreads; this.outputPath = outputPath; } @Override public void run() { try { this.breakPointRecord(); } catch (IOException e) { e.printStackTrace(); } } public void breakPointRecord() throws FileNotFoundException, IOException { int aliveThreadNum = 0; //存活线程数目 //不使用追加方式,这里只需要最新的记录即可。 File recordFile = new File(outputPath, filename); try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(recordFile))){ //每次记录一个线程的下载位置,但是取出来又需要进行转换,太麻烦了。 //我们直接使用序列化来进行操作,哈哈! long[] curlen = new long[4]; int index = 0; for (FileCopyThread copyThread : copyThreads) { if (copyThread.isAlive()) { aliveThreadNum++; } curlen[index++] = copyThread.getCurlen(); System.out.println(index+" curlen: "+copyThread.getCurlen()); } //创建 Record 对象,并序列化。 oos.writeObject(new Record(curlen)); } //当所有的线程都死亡时,关闭计时器,删除记录文件。(所有线程死亡的话,就是文件已经复制完成了!) if (aliveThreadNum == 0) { timer.cancel(); recordFile.delete(); } System.out.println("线程数量: "+aliveThreadNum); } }
Explication :
if (aliveThreadNum == 0) { timer.cancel(); recordFile.delete(); }Si les fils de discussion sont terminés, cela signifie que le programme s'est terminé normalement.
Supprimez le fichier d'enregistrement à ce moment. Le fichier d'enregistrement ici est un indicateur. Si le fichier d'enregistrement existe, cela signifie que le programme ne s'est pas terminé normalement. Lorsqu'il sera redémarré, la copie du point d'arrêt sera effectuée.
Remarque : les exceptions IO pendant le processus de copie ne sont pas prises en compte ici. Si le thread génère une exception IO, le statut du thread prendra également fin. Cependant, étant donné qu'il existe relativement peu d'exceptions d'E/S dans la copie de fichiers locaux, je n'ai pas considéré que s'il était téléchargé depuis Internet, la fonction de ce programme devait être améliorée. Classe d'enregistrement des informationsÀ chaque fois, les informations de chaque thread doivent être écrites dans l'ordre, mais elles doivent quand même être converties après l'avoir lu, ce qui semble encore trop gênant. Ici, nous utilisons directement le mécanisme de sérialisation de Java. Parfois, il est pratique de manipuler des objets directement. Remarque : L'indice du tableau représente la position de chaque fil.
package dragon.local; import java.io.Serializable; public class Record implements Serializable{ /** * 序列化 id */ private static final long serialVersionUID = 1L; private long[] curlen; public Record(long[] curlen) { this.curlen = curlen; } public long[] getCurlen() { return this.curlen; } }Copiez la classe de fil
package dragon.local; import java.io.BufferedInputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.RandomAccessFile; public class FileCopyThread extends Thread { private int index; private long position; private long size; private File targetFile; private File outputFile; private long curlen; //当前下载的长度 public FileCopyThread(int index, long position, long size, File targetFile, File outputFile) { this.index = index; this.position = position; this.size = size; this.targetFile = targetFile; this.outputFile = outputFile; this.curlen = 0L; } @Override public void run() { try ( BufferedInputStream bis = new BufferedInputStream(new FileInputStream(targetFile)); RandomAccessFile raf = new RandomAccessFile(outputFile, "rw")){ bis.skip(position); //跳过不需要读取的字节数,注意只能先后跳 raf.seek(position); //跳到需要写入的位置,没有这句话,会出错,但是很难改。 int hasRead = 0; byte[] b = new byte[1024]; /** * 注意,每个线程只是读取一部分数据,不能只以 -1 作为循环结束的条件 * 循环退出条件应该是两个,即写入的字节数大于需要读取的字节数 或者 文件读取结束(最后一个线程读取到文件末尾) */ while(curlen < size && (hasRead = bis.read(b)) != -1) { raf.write(b, 0, hasRead); curlen += (long)hasRead; //强制停止程序。 //if (curlen > 17_000_000) { //System.exit(0); //} } System.out.println(index+" "+position+" "+curlen+" "+size); } catch (IOException e) { e.printStackTrace(); } } public long getCurlen() { //获取当前的进度,用于记录,以便必要时恢复读取进度。 return position+this.curlen; } }
//强制停止程序。 //if (curlen > 17_000_000) { //System.exit(0); //}Classe d'outil de copie
package dragon.local; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.ObjectInputStream; import java.io.RandomAccessFile; import java.util.ArrayList; import java.util.List; import java.util.Timer; /** * 设计思路: * 获取目标文件的大小,然后设置复制文件的大小(这样做是有好处的), * 然后使用将文件分为 n 分,使用 n 个线程同时进行复制(这里我将 n 取为 4)。 * * 进一步拓展: * 加强为断点复制功能,即程序中断以后, * 仍然可以继续从上次位置恢复复制,减少不必要的重复开销 * */ public class FileCopyUtil { //设置一个常量,复制线程的数量 private static final int THREAD_NUM = 4; private FileCopyUtil() {} /** * @param targetPath 目标文件的路径 * @param outputPath 复制输出文件的路径 * @throws IOException * @throws ClassNotFoundException * */ public static void transferFile(String targetPath, String outputPath) throws IOException, ClassNotFoundException { File targetFile = new File(targetPath); File outputFilePath = new File(outputPath); if (!targetFile.exists() || targetFile.isDirectory()) { //目标文件不存在,或者是一个文件夹,则抛出异常 throw new FileNotFoundException("目标文件不存在:"+targetPath); } if (!outputFilePath.exists()) { //如果输出文件夹不存在,将会尝试创建,创建失败,则抛出异常。 if(!outputFilePath.mkdir()) { throw new FileNotFoundException("无法创建输出文件:"+outputPath); } } long len = targetFile.length(); File outputFile = new File(outputFilePath, "copy"+targetFile.getName()); createOutputFile(outputFile, len); //创建输出文件,设置好大小。 //创建计时器 Timer 对象 Timer timer = new Timer(); long[] position = new long[4]; //每一个线程需要复制文件的起点 long size = len / FileCopyUtil.THREAD_NUM + 1; //保存复制线程的集合 List<FileCopyThread> copyThreads = new ArrayList<>(); Record record = getRecord(outputPath); for (int i = 0; i < FileCopyUtil.THREAD_NUM; i++) { //如果已经有了 记录文件,就从使用记录数据,否则就是新的下载。 position[i] = record == null ? i*size : record.getCurlen()[i]; FileCopyThread copyThread = new FileCopyThread(i, position[i], size, targetFile, outputFile); copyThread.start(); //启动复制线程 copyThreads.add(copyThread); //将复制线程添加到集合中。 } timer.schedule(new RecordTask(timer, copyThreads, outputPath), 0L, 100L); //立即启动计时器,每隔10秒记录一次位置。 System.out.println("开始了!"); } //创建输出文件,设置好大小。 private static void createOutputFile(File file, long length) throws IOException { try ( RandomAccessFile raf = new RandomAccessFile(file, "rw")){ raf.setLength(length); } } //获取以及下载的位置 private static Record getRecord(String outputPath) throws FileNotFoundException, IOException, ClassNotFoundException { File recordFile = new File(outputPath, RecordTask.filename); if (recordFile.exists()) { try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(recordFile))){ return (Record) ois.readObject(); } } return null; } }
Instructions : Déterminez s'il faut démarrer la copie du point d'arrêt en fonction de la présence ou non d'un fichier d'enregistrement dans le répertoire copié.
private static Record getRecord(String outputPath) throws FileNotFoundException, IOException, ClassNotFoundException { File recordFile = new File(outputPath, RecordTask.filename); if (recordFile.exists()) { try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(recordFile))){ return (Record) ois.readObject(); } } return null; }Le démarrage de la copie du point d'arrêt s'avère très simple. C'est la même chose que la copie, sauf que la position de départ de la copie devient la position enregistrée.
//如果已经有了 记录文件,就从使用记录数据,否则就是新的下载。 position[i] = record == null ? i*size : record.getCurlen()[i];
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!