最近看到了一個例子,是使用多線程的方式下載文件,感覺很有趣,探索了一下,並且嘗試了使用多線程進行本地複製文件。寫完之後,發現了這兩個其實很相似,無論是本地文件複製,還是網路多執行緒下載,對於流的使用都是一樣的。 對於本機檔案系統來說,輸入流就是從本機檔案系統的一個檔案來獲取,對於網路資源來說,是從遠處伺服器上的一個檔案來獲取。
註: 雖然這個多執行緒下載的程式碼,很多人都寫過了,不過應該不是所有人都能理解吧,我這裡就再寫一遍,哈。
使用多執行緒的一個顯而易見的好處就是:利用空閒的 CPU,加快速度。 但注意不是線程越多越好,雖然好像n個線程一起下載,每個線程下載一小部分,下載時間就會變成1/n 了。這是很淺顯的認識,好像一個人蓋一個房子需要100天,難道10000個人,只需要1/10 天一樣了? (這是一個誇張的說法,哈哈!)
線程之間的切換也是需要係統開銷的,線程的數目還是要控制在一個合理的範圍內才行。
這個類別比較獨特,它也就是可以從檔案讀取數據,也可以寫入資料到檔案。但是它不是 OutputStream 和 InputStream 的子類,它是實作了這兩個介面 DataOutput 、DataInput 的一個類別。
API 中的介紹:
該類別的實例支援讀取和寫入隨機存取檔案。隨機存取檔案的行為類似於儲存在檔案系統中的大量位元組。有一種遊標,或索引到隱含的數組,稱為檔案指標 ; 輸入操作讀取從檔案指標開始的位元組,並使檔案指標超過讀取的位元組。如果在讀取/寫入模式下創建隨機存取文件,則輸出操作也可用; 輸出操作從文件指標開始寫入位元組,並將文件指標提前到寫入的位元組。寫入隱式數組的當前端的輸出操作會導致擴展數組。文件指標可以透過讀取getFilePointer方法和由設定seek方法。
所以,這個類別最重要的就是 seek 方法了,使用 seek 方法,可以控制寫入的位置,所以實現多執行緒就容易的多了。因此,無論是本機檔案複製,或是網路多執行緒下載都需要這個類別的作用。
具體想法是: 首先使用 RandomAccessFile 建立一個 File 對象,然後設定這個檔案的大小。 (對的,它可以直接設定檔案的大小。)將這個檔案設定成和要複製或下載的檔案一樣的。 (雖然我們並沒有在這個文件中寫入數據,但是這個文件已經創建了。)將文件分為若干部分,使用線程複製或下載每一部分的內容。
這有點類似文件的覆蓋,如果一個已經存在的文件,從這個文件的頭部開始寫入數據,一直寫到文件的尾部,那麼原來的文件就不存在了,變成了寫入的新檔案。
設定檔案大小:
private static void createOutputFile(File file, long length) throws IOException { try ( RandomAccessFile raf = new RandomAccessFile(file, "rw")){ raf.setLength(length); } }
用圖片來說明: 這個圖表示一個8191 位元組大小的檔案: 每個部分大小是:8191 / 4 = 2047位元組
將這個檔案的分成四個部分,每個部分使用一個執行緒進行複製或下載,其中每一個箭頭代表一個執行緒的開始下載位置。我特意將最後一個部分,沒有設定為 1024 byte,這是因為檔案很少是正好能被 1024 byte 整除的。 (之所以使用 1024 byte,是因為我每次會讀取 1024 byte,如果讀取到 1024 byte的話, 否則寫入讀取到的相應位元組數)。
依照這個示意圖,每個執行緒下載2047 byte,那麼總共下載的位元組數是:2047 * 4 = 8188 位元組所以這就產生了一個問題,下載的位元組數少於總位元組數,這就是問題了,所以必須下載的位元組數大於總位元組數才行。 (多了沒有關係,因為多下載的部分,會被後面的給覆蓋掉,不會產生了問題。)
所以每個部分的大小應該是:8191 / 4 1 = 2048 位元組。 (這樣四部分的大小相加是超過總大小的,不會發生資料的遺失問題。)
所以,這裡這個加 1 是必要的。
long size = len / FileCopyUtil.THREAD_NUM + 1;
每個線程下載完成的位置(右邊) 每個線程,只複製下載自己的部分,所以不需要全部下載完所有的內容,所以讀取檔案資料並寫入檔案的部分,會多加一個判斷。
这里增加一个计数器:curlen。它表示是当前复制或者下载的长度,然后每次读取后和 size(每部分的大小)进行比较,如果 curlen 大于 size 就表示相应的部分下载完成了(当然了,这些都要在数据没有读取完的条件下判断)。
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]; long curlen = 0; while(curlen < size && (hasRead = bis.read(b)) != -1) { raf.write(b, 0, hasRead); curlen += (long)hasRead; } System.out.println(n+" "+position+" "+curlen+" "+size); } catch (IOException e) { e.printStackTrace(); }
还有需要注意的是,每个线程下载的时候都要: 1. 输出流设置文件指针的位置。 2. 输入流跳过不需要读取的字节。
这是很重要的一步,应该是很好理解的。
bis.skip(position); raf.seek(position);
package dragon; import java.io.BufferedInputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.RandomAccessFile; /** * 用于进行文件复制,但不是常规的文件复制 。 * 准备仿照疯狂Java,写一个多线程的文件复制工具。 * 即可以本地复制和网络复制 * */ /** * 设计思路: * 获取目标文件的大小,然后设置复制文件的大小(这样做是有好处的), * 然后使用将文件分为 n 分,使用 n 个线程同时进行复制(这里我将 n 取为 4)。 * * 可以进一步拓展: * 加强为断点复制功能,即程序中断以后, * 仍然可以继续从上次位置恢复复制,减少不必要的重复开销 * */ public class FileCopyUtil { //设置一个常量,复制线程的数量 private static final int THREAD_NUM = 4; private FileCopyUtil() {} /** * @param targetPath 目标文件的路径 * @param outputPath 复制输出文件的路径 * @throws IOException * */ public static void transferFile(String targetPath, String outputPath) throws IOException { 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); //创建输出文件,设置好大小。 long[] position = new long[4]; //每一个线程需要复制文件的起点 long size = len / FileCopyUtil.THREAD_NUM + 1; for (int i = 0; i < FileCopyUtil.THREAD_NUM; i++) { position[i] = i*size; copyThread(i, position[i], size, targetFile, outputFile); } } //创建输出文件,设置好大小。 private static void createOutputFile(File file, long length) throws IOException { try ( RandomAccessFile raf = new RandomAccessFile(file, "rw")){ raf.setLength(length); } } private static void copyThread(int i, long position, long size, File targetFile, File outputFile) { int n = i; //Lambda 表达式的限制,无法使用变量。 new Thread(()->{ 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 作为循环结束的条件 * 循环退出条件应该是两个,即写入的字节数大于需要读取的字节数 或者 文件读取结束(最后一个线程读取到文件末尾) */ long curlen = 0; while(curlen < size && (hasRead = bis.read(b)) != -1) { raf.write(b, 0, hasRead); curlen += (long)hasRead; } System.out.println(n+" "+position+" "+curlen+" "+size); } catch (IOException e) { e.printStackTrace(); } }).start(); } }
package dragon; import java.io.BufferedInputStream; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.RandomAccessFile; import java.net.URL; import java.net.URLConnection; /* * 多线程下载文件: * 通过一个 URL 获取文件输入流,使用多线程技术下载这个文件。 * */ public class FileDownloadUtil { //下载线程数 private static final int THREAD_NUM = 4; /** * @param url 资源位置 * @param output 输出路径 * @throws IOException * */ public static void transferFile(String url, String output) throws IOException { init(output); URL resource = new URL(url); URLConnection connection = resource.openConnection(); //获取文件类型 String type = connection.getContentType(); if (type != null) { type = "."+type.split("/")[1]; } else { type = ""; } //创建文件,并设置长度。 long len = connection.getContentLength(); String filename = System.currentTimeMillis()+type; try (RandomAccessFile raf = new RandomAccessFile(new File(output, filename), "rw")){ raf.setLength(len); } //为每一个线程分配相应的下载其实位置 long size = len / THREAD_NUM + 1; long[] position = new long[THREAD_NUM]; File downloadFile = new File(output, filename); //开始下载文件: 4个线程 download(url, downloadFile, position, size); } private static void download(String url, File file, long[] position, long size) throws IOException { //开始下载文件: 4个线程 for (int i = 0 ; i < THREAD_NUM; i++) { position[i] = i * size; //每一个线程下载的起始位置 int n = i; // Lambda 表达式的限制,无法使用变量 new Thread(()->{ URL resource = null; URLConnection connection = null; try { resource = new URL(url); connection = resource.openConnection(); } catch (IOException e) { e.printStackTrace(); } try ( BufferedInputStream bis = new BufferedInputStream(connection.getInputStream()); RandomAccessFile raf = new RandomAccessFile(file, "rw")){ //每个流一旦关闭,就不能打开了 raf.seek(position[n]); //跳到需要下载的位置 bis.skip(position[n]); //跳过不需要下载的部分 int hasRead = 0; byte[] b = new byte[1024]; long curlen = 0; while(curlen < size && (hasRead = bis.read(b)) != -1) { raf.write(b, 0, hasRead); curlen += (long)hasRead; } System.out.println(n+" "+position[n]+" "+curlen+" "+size); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } }) .start(); } } private static void init(String output) throws FileNotFoundException { File path = new File(output); if (!path.exists()) { if (!path.mkdirs()) { throw new FileNotFoundException("无法创建输出路径:"+output); } } else if (path.isFile()) { throw new FileNotFoundException("输出路径不是一个目录:"+output); } } }
因为这个多线程文件复制和多线程下载是很相似的,所以就放在一起测试了。我也想将两个写在一个类里面,这样可以做成方法的重载调用。 文件复制的第一个参数可以是 String 或者 URI。 使用这个作为目标文件的参数。
public File(URI uri)
网络文件下载的第一个参数,可以使用 String 或者是 URL。 不过,因为先写的这个文件复制,后写的多线程下载,就没有做这部分。不过现在这样功能也达到了,可以进行本地文件的复制(多线程)和网络文件的下载(多线程)。
package dragon; import java.io.IOException; public class FileCopyTest { public static void main(String[] args) throws IOException { //复制文件 long start = System.currentTimeMillis(); try { FileCopyUtil.transferFile("D:\\DB\\download\\timg.jfif", "D:\\DBC"); } catch (IOException e) { e.printStackTrace(); } long time = System.currentTimeMillis()-start; System.out.println("time: "+time); //下载文件 start = System.currentTimeMillis(); FileDownloadUtil.transferFile("https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1578151056184&di=594a34f05f3587c31d9377a643ddd72e&imgtype=0&src=http%3A%2F%2Fn.sinaimg.cn%2Fsinacn%2Fw1600h2000%2F20180113%2F0bdc-fyqrewh6850115.jpg", "D:\\DB\\download"); System.out.println("time: "+(System.currentTimeMillis()-start)); } }
运行截图: 注意:这里这个时间并不是复制和下载需要的时间,实际上它没有这个功能!
注意:虽然两部分代码是相同的,但是第三列数字,却不是完全相同的,这个似乎是因为本地和网络得区别吧。但是最后得文件是完全相同的,没有问题得。(我本地文件复制得是网络下载得那张图片,使用图片进行测试有一个好处,就是如果错了一点(字节数目不对),这个图片基本上就会产生问题。)
产生错误之后的图片: 图片无法正常显示,会出现很多的问题,这就说明一定是代码写错了。
以上是Java多執行緒與IO流的應用場景與技巧的詳細內容。更多資訊請關注PHP中文網其他相關文章!