詳解Java多線程和IO流的應用
Java多線程和流的應用
最近看到了一個例子,是使用多線程的方式下載文件,感覺很有趣,探索了一下,并且嘗試了使用多線程進行本地復制文件。寫完之后,發(fā)現(xiàn)了這兩個其實很相似,無論是本地文件復制,還是網(wǎng)絡多線程下載,對于流的使用都是一樣的。對于本地文件系統(tǒng)來說,輸入流就是從本地文件系統(tǒng)的一個文件來獲取,對于網(wǎng)絡資源來說,是從遠處服務器上的一個文件來獲取。
注: 雖然這個多線程下載的代碼,很多人都寫過了,不過應該不是所有人都能理解吧,我這里就再寫一遍,哈。
使用多線程的一個顯而易見的好處就是:利用空閑的 CPU,加快速度。 但是注意不是線程越多越好,雖然好像n個線程一起下載,每個線程下載一小部分,下載時間就會變?yōu)?/n 了。這是很淺顯的認識,就好像一個人蓋一個房子需要100天,難道10000個人,只需要1/10 天一樣了?(這是一個夸張的說法,哈哈!)
線程之間的切換也是需要系統(tǒng)開銷的,線程的數(shù)目還是要控制在一個合理的范圍內才行。
RamdomAccessFile
這個類比較獨特,它即可以從文件中讀取數(shù)據(jù),也可以向文件中寫入數(shù)據(jù)。但是它不是 OutputStream 和 InputStream 的子類,它是實現(xiàn)了這兩個接口 DataOutput 、DataInput 的一個類。
API 中的介紹:
該類的實例支持讀取和寫入隨機訪問文件。 隨機訪問文件的行為類似于存儲在文件系統(tǒng)中的大量字節(jié)。 有一種游標,或索引到隱含的數(shù)組,稱為文件指針 ; 輸入操作讀取從文件指針開始的字節(jié),并使文件指針超過讀取的字節(jié)。 如果在讀/寫模式下創(chuàng)建隨機訪問文件,則輸出操作也可用; 輸出操作從文件指針開始寫入字節(jié),并將文件指針提前到寫入的字節(jié)。 寫入隱式數(shù)組的當前端的輸出操作會導致擴展數(shù)組。 文件指針可以通過讀取getFilePointer方法和由設置seek方法。
所以,這個類最重要的就是 seek 方法了,使用 seek 方法,可以控制寫入的位置,所以實現(xiàn)多線程就容易的多了。因此,無論是本地文件復制,還是網(wǎng)絡多線程下載都需要這個類的作用。
具體思路是: 首先使用 RandomAccessFile 創(chuàng)建一個 File 對象,然后設置這個文件的大小。(對的,它可以直接設置文件的大小。)將這個文件設置成和要復制或下載的文件一樣的。(雖然我們并沒有向這個文件中寫入數(shù)據(jù),但是這個文件已經(jīng)創(chuàng)建了。)將文件分為若干部分,使用線程復制或者下載每一部分的內容。
這有點類似文件的覆蓋,如果一個已經(jīng)存在的文件,從這個文件的頭部開始寫入數(shù)據(jù),一直寫到文件的尾部,那么原來的文件就不存在了,變成了寫入的新文件。
設置文件大?。?/p>
private static void createOutputFile(File file, long length) throws IOException { try ( RandomAccessFile raf = new RandomAccessFile(file, "rw")){ raf.setLength(length); } }
用圖片來說明: 這個圖表示一個 8191 字節(jié)大小的文件: 每一個部分大小是:8191 / 4 = 2047字節(jié)
將這個文件的分為四個部分,每一個部分使用一個線程進行復制或下載,其中每一個箭頭代表一個線程的開始下載位置。我特意將最后一個部分,沒有設置為 1024 byte,這是因為文件很少是正好能被 1024 byte 整除的。(之所以使用 1024 byte,是因為我每次會讀取 1024 byte,如果讀取到 1024 byte的話, 否則寫入讀取到的相應字節(jié)數(shù))。
按照這個示意圖,每一個線程下載 2047 byte,那么總共下載的字節(jié)數(shù)是:2047 * 4 = 8188 字節(jié) < 8191 字節(jié)(文件的總大小) 所以這就產(chǎn)生了一個問題,下載的字節(jié)數(shù)少于總字節(jié)數(shù),這就是問題了,所以必須要下載的字節(jié)數(shù)大于總字節(jié)數(shù)才行。(多了沒有關系,因為多下載的部分,會被后面的給覆蓋掉,不會產(chǎn)生了問題。)
所以每個部分的大小應該是:8191 / 4 + 1 = 2048 字節(jié)。(這樣四部分的大小相加是超過總大小的,不會發(fā)生數(shù)據(jù)的丟失問題。)
所以,這里這個加 1 是很有必要的。
long size = len / FileCopyUtil.THREAD_NUM + 1;
每個線程下載完成的位置(右邊) 每個線程,只復制下載自己的那部分,所以不需要全部下載完所有的內容,所以讀取文件數(shù)據(jù)并寫入文件的部分,會多加一個判斷。
這里增加一個計數(shù)器:curlen。它表示是當前復制或者下載的長度,然后每次讀取后和 size(每部分的大小)進行比較,如果 curlen 大于 size 就表示相應的部分下載完成了(當然了,這些都要在數(shù)據(jù)沒有讀取完的條件下判斷)。
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. 輸入流跳過不需要讀取的字節(jié)。
這是很重要的一步,應該是很好理解的。
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; /** * 用于進行文件復制,但不是常規(guī)的文件復制 。 * 準備仿照瘋狂Java,寫一個多線程的文件復制工具。 * 即可以本地復制和網(wǎng)絡復制 * */ /** * 設計思路: * 獲取目標文件的大小,然后設置復制文件的大?。ㄟ@樣做是有好處的), * 然后使用將文件分為 n 分,使用 n 個線程同時進行復制(這里我將 n 取為 4)。 * * 可以進一步拓展: * 加強為斷點復制功能,即程序中斷以后, * 仍然可以繼續(xù)從上次位置恢復復制,減少不必要的重復開銷 * */ public class FileCopyUtil { //設置一個常量,復制線程的數(shù)量 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()) { //如果輸出文件夾不存在,將會嘗試創(chuàng)建,創(chuàng)建失敗,則拋出異常。 if(!outputFilePath.mkdir()) { throw new FileNotFoundException("無法創(chuàng)建輸出文件:"+outputPath); } } long len = targetFile.length(); File outputFile = new File(outputFilePath, "copy"+targetFile.getName()); createOutputFile(outputFile, len); //創(chuàng)建輸出文件,設置好大小。 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); } } //創(chuàng)建輸出文件,設置好大小。 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); //跳過不需要讀取的字節(jié)數(shù),注意只能先后跳 raf.seek(position); //跳到需要寫入的位置,沒有這句話,會出錯,但是很難改。 int hasRead = 0; byte[] b = new byte[1024]; /** * 注意,每個線程只是讀取一部分數(shù)據(jù),不能只以 -1 作為循環(huán)結束的條件 * 循環(huán)退出條件應該是兩個,即寫入的字節(jié)數(shù)大于需要讀取的字節(jié)數(shù) 或者 文件讀取結束(最后一個線程讀取到文件末尾) */ 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(); } }
多線程網(wǎng)絡下載(完整代碼)
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 { //下載線程數(shù) 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 = ""; } //創(chuàng)建文件,并設置長度。 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("無法創(chuàng)建輸出路徑:"+output); } } else if (path.isFile()) { throw new FileNotFoundException("輸出路徑不是一個目錄:"+output); } } }
測試代碼及結果
因為這個多線程文件復制和多線程下載是很相似的,所以就放在一起測試了。我也想將兩個寫在一個類里面,這樣可以做成方法的重載調用。 文件復制的第一個參數(shù)可以是 String 或者 URI。 使用這個作為目標文件的參數(shù)。
public File(URI uri)
網(wǎng)絡文件下載的第一個參數(shù),可以使用 String 或者是 URL。 不過,因為先寫的這個文件復制,后寫的多線程下載,就沒有做這部分。不過現(xiàn)在這樣功能也達到了,可以進行本地文件的復制(多線程)和網(wǎng)絡文件的下載(多線程)。
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%2Fw1600h1000%2F20180113%2F0bdc-fyqrewh5850115.jpg", "D:\\DB\\download"); System.out.println("time: "+(System.currentTimeMillis()-start)); } }
運行截圖: 注意:這里這個時間并不是復制和下載需要的時間,實際上它沒有這個功能!
注意:雖然兩部分代碼是相同的,但是第三列數(shù)字,卻不是完全相同的,這個似乎是因為本地和網(wǎng)絡得區(qū)別吧。但是最后得文件是完全相同的,沒有問題得。(我本地文件復制得是網(wǎng)絡下載得那張圖片,使用圖片進行測試有一個好處,就是如果錯了一點(字節(jié)數(shù)目不對),這個圖片基本上就會產(chǎn)生問題。)
產(chǎn)生錯誤之后的圖片: 圖片無法正常顯示,會出現(xiàn)很多的問題,這就說明一定是代碼寫錯了,哈哈。不過代碼的 bug 有時候還是很費時間的。
總結
多線程復制和多線程下載,對于IO 流的使用都是相同的,所以掌握好IO 流的使用是很關鍵的一步,這樣就可以做很多很有趣的事情了。將IO流結合線程和網(wǎng)絡是一片廣闊的天地,但是我也是最近才看了一點網(wǎng)絡的知識。(網(wǎng)絡是很難的,寫這個多線程下載就遇到了一些網(wǎng)絡類上面的問題。)
使用計時器實現(xiàn)了多線程復制文件的斷點復制功能,感興趣的可以了解一下。 多線程斷點復制
到此這篇關于詳解Java多線程和IO流的應用的文章就介紹到這了,更多相關Java多線程和IO流內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
Java字符串拼接+和StringBuilder的比較與選擇
Java 提供了兩種主要的方式:使用 "+" 運算符和使用 StringBuilder 類,本文主要介紹了Java字符串拼接+和StringBuilder的比較與選擇,感興趣的可以了解一下2023-10-10關于springBoot yml文件的list讀取問題總結(親測)
這篇文章主要介紹了關于springBoot yml文件的list讀取問題總結,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-12-12springboot整合Excel填充數(shù)據(jù)代碼示例
這篇文章主要給大家介紹了關于springboot整合Excel填充數(shù)據(jù)的相關資料,文中通過代碼示例介紹的非常詳細,對大家學習或者使用springboot具有一定的參考借鑒價值,需要的朋友可以參考下2023-08-08