java多線程文件下載器的實現(xiàn)
1.簡介
該項目應(yīng)用的知識點包括:
- RandomAccessFile 類的運用
- HttpURLConnection 類的運用
- 線程池的使用
- 原子類 LongAdder 的運用
- CountDownLatch 類的運用
- ScheduledExecutorService 類的運用
2.文件下載的核心
從互聯(lián)網(wǎng)下載文件有點類似于我們將本地某個文件復(fù)制到另一個目錄下,也會利用 IO 流進行操作。對于從互聯(lián)網(wǎng)下載,還需要將本地和下載文件所在的服務(wù)器建立連接。
3.文件下載器的基礎(chǔ)代碼
3.1 HttpURLConnection
從互聯(lián)網(wǎng)中下載文件的話,需要與文件所在的服務(wù)器建立連接,這里可以使用 jdk 提供的 java.net.HttpURLConnection 類來幫助我們完成這個操作。jdk11中有提供 java.net.http.HttpClient 類來替代 HttpURLConnection,由于現(xiàn)在使用的是 jdk8,因此先不用 jdk11 中的 HttpClient。除此之外還有一些其他第三方提供類可以執(zhí)行類似的操作,這里就不贅述了。
3.2 用戶標識
我們通過瀏覽器訪問某個網(wǎng)站的時候,會將當前瀏覽器的版本,操作系統(tǒng)版本等信息的標識發(fā)送到網(wǎng)站所在的服務(wù)器中。當用程序代碼去訪問網(wǎng)站時,需要將這個標識發(fā)送過去。
Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.835.163 Safari/535.1
4.下載信息
4.1 計劃任務(wù)
文件下載的時候最好能夠展示出下載的速度,已下載文件大小等信息。這里可以每隔一段時間來獲取文件的下載信息,比如間隔 1 秒獲取一次,然后將信息打印到控制臺。文件下載是一個獨立的線程,另外還需要再開啟一個線程來間隔獲取文件的信息。java.util.concurrent.ScheduledExecutorService 這個類可以幫助我們來實現(xiàn)此功能。
4.2 ScheduledExecutorService
在該類中提供了一些方法可以幫助開發(fā)者實現(xiàn)間隔執(zhí)行的效果,下面列出一些常見的方法及其參數(shù)說明。我們可以通過下面方式來獲取該類的對象,其中 1 標識核心線程的數(shù)量。
ScheduledExecutorService s = Executors.newScheduledThreadPool(1);
?? schedule方法
該方法是重載的,這兩個重載的方法都是有 3 個形參,只是第一個形參不同。
參數(shù) | 含義 |
---|---|
Runnable / Callable<V> | 可以傳入這兩個類型的任務(wù) |
long delay | 延時的時間數(shù)量 |
TimeUnit unit | 時間單位 |
該方法的作用是讓任務(wù)按照指定的時間延時執(zhí)行。
?? scheduleAtFixedRate方法
該方法的作用是按照指定的時間延時執(zhí)行,并且每隔一段時間再繼續(xù)執(zhí)行。
參數(shù) | 含義 |
---|---|
Runnable command | 執(zhí)行的任務(wù) |
long initialDelay | 延時的時間數(shù)量 |
long period | 間隔的時間數(shù)量 |
TimeUnit unit | 時間單位 |
倘若在執(zhí)行任務(wù)的時候,耗時超過了間隔時間,則任務(wù)執(zhí)行結(jié)束之后直接再次執(zhí)行,而不是再等待間隔時間執(zhí)行。
?? scheduleWithFixedDelay方法
該方法的作用是按照指定的時間延時執(zhí)行,并且每隔一段時間再繼續(xù)執(zhí)行。
參數(shù) | 含義 |
---|---|
Runnable command | 執(zhí)行的任務(wù) |
long initialDelay | 延時的時間數(shù)量 |
long period | 間隔的時間數(shù)量 |
TimeUnit unit | 時間單位 |
在執(zhí)行任務(wù)的時候,無論耗時多久,任務(wù)執(zhí)行結(jié)束之后都會等待間隔時間之后再繼續(xù)下次任務(wù)。
5.線程池簡介
線程在創(chuàng)建,銷毀的過程中會消耗一些資源,為了節(jié)省這些開銷,jdk 添加了線程池。線程池節(jié)省了開銷,提高了線程使用的效率。阿里巴巴開發(fā)文檔中建議在編寫多線程程序的時候使用線程池。
5.1 ThreadPoolExecutor 構(gòu)造方法參數(shù)
在 juc 包下提供了 ThreadPoolExecutor 類,可以通過該類來創(chuàng)建線程池,這個類中有4個重載的構(gòu)造方法,最核心的構(gòu)造方法是有7個形參的,這些參數(shù)所代表的意義如下:
參數(shù) | 含義 |
---|---|
corePoolSize | 線程池中核心線程的數(shù)量 |
maximumPoolSize | 線程池中最大線程的數(shù)量,是核心線程數(shù)量和非核心線程數(shù)量之和 |
keepAliveTime | 非核心線程空閑的生存時間 |
unit | keepAliveTime 的生存時間單位 |
workQueue | 當沒有空閑的線程時,新的任務(wù)會加入到 workQueue 中排隊等待 |
threadFactory | 線程工廠,用于創(chuàng)建線程 |
handler | 拒絕策略,當任務(wù)太多無法處理時的拒絕策略 |
5.2 線程池工作過程
5.3 線程池的狀態(tài)
狀態(tài) | 說明 |
---|---|
RUNNING | 創(chuàng)建線程池之后的狀態(tài)是 RUNNING |
SHUTDOWN | 該狀態(tài)下,線程池就不會接收新任務(wù),但會處理阻塞隊列剩余任務(wù),相對溫和 |
STOP | 該狀態(tài)下會中斷正在執(zhí)行的任務(wù),并拋棄阻塞隊列任務(wù),相對暴力 |
TIDYING | 任務(wù)全部執(zhí)行完畢,活動線程為 0 即將進入終止 |
TERMINATED | 線程池終止 |
5.4 線程池的關(guān)閉
線程池使用完畢之后需要進行關(guān)閉,提供了以下兩種方法進行關(guān)閉。
方法 | 說明 |
---|---|
shutdown() | 該方法執(zhí)行后,線程池狀態(tài)變?yōu)?SHUTDOWN,不會接收新任務(wù),但是會執(zhí)行完已提交的任務(wù),此方法不會阻塞調(diào)用線程的執(zhí)行。 |
shutdownNow() | 該方法執(zhí)行后,線程池狀態(tài)變?yōu)?STOP,不會接收新任務(wù),會將隊列中的任務(wù)返回,并用 interrupt 的方式中斷正在執(zhí)行的任務(wù)。 |
5.5 工作隊列
jdk 中提供的一些工作隊列 workQueue。
隊列 | 說明 |
---|---|
SynchronousQueue | 直接提交隊列 |
ArrayBlockingQueue | 有界隊列,可以指定容量 |
LinkedBlockingDeque | 無界隊列 |
PriorityBlockingQueue | 優(yōu)先任務(wù)隊列,可以根據(jù)任務(wù)優(yōu)先級順序執(zhí)行任務(wù) |
6.代碼實現(xiàn)
6.1 環(huán)境搭建
?? 基本信息
- 開發(fā)工具:IDEA
- JDK 版本:8
- 項目編碼:utf-8
?? 創(chuàng)建項目
在開發(fā)工具中創(chuàng)建一個 javase 項目即可,無需導(dǎo)入第三方 jar 依賴。
6.2 實現(xiàn)邏輯
- 先判斷是否已存在重復(fù)文件,該步驟其實可忽略,因為最終下載合并的文件名已采用時間戳進行了唯一標識;
- 啟動一個線程每隔一秒打印下載情況;
- 切分任務(wù),多線程分快下載;
- 全部塊文件下載完畢,合并分塊文件;
- 合并分塊文件完畢,清理分塊文件;
- 釋放資源,關(guān)閉線程池和連接對象。
6.3 項目結(jié)構(gòu)
包名 | 作用 |
---|---|
constant | 存放常量類的包 |
core | 存放了下載器核心類的包 |
util | 存放工具類的包 |
Main | 主類 |
6.4 類代碼
?? constant 包
?? Constant
/** * Description: 存放項目常量 * * @Author 狐貍半面添 * @Create 2023/11/6 1:22 * @Version 1.0 */ public class Constant { /** * 指定下載目錄的存放位置 */ public static final String PATH = "D:\\download\\"; public static final double MB = 1024d * 1024d; public static final double KB = 1024d; /** * 每次讀取的字節(jié)大小 */ public static final int BYTE_SIZE = 1024 * 100; /** * 塊文件(臨時文件)的后綴 */ public static final String PART_FILE_SUFFIX = ".temp"; /** * 線程數(shù)量 */ public static final int THREAD_NUM = 5; // 創(chuàng)建存放位置的代碼 // public static void main(String[] args) { // File file = new File("D:\\download"); // if (!file.exists()) { // file.mkdir(); // } // } }
?? util 包
?? FileUtils
/** * Description: 文件相關(guān)工具 * * @Author 狐貍半面添 * @Create 2023/11/6 11:46 * @Version 1.0 */ public class FileUtils { /** * 獲取本地文件的大小 * * @param path 文件路徑 * @return 文件大小 */ public static long getFileContentLength(String path) { File file = new File(path); return file.exists() && file.isFile() ? file.length() : 0; } }
?? HttpUtils
/** * Description: Http 相關(guān)工具類 * * @Author 狐貍半面添 * @Create 2023/11/6 1:06 * @Version 1.0 */ public class HttpUtils { private static long id = System.currentTimeMillis(); public static void change() { id = System.currentTimeMillis(); } /** * 獲取下載的文件大小 * * @param url 下載文件鏈接 * @return 文件大小 * @throws IOException */ public static long getHttpFileContentLength(String url) throws IOException { int contentLength; HttpURLConnection httpURLConnection = null; try { httpURLConnection = getHttpURLConnection(url); contentLength = httpURLConnection.getContentLength(); } finally { if (httpURLConnection != null) { httpURLConnection.disconnect(); } } return contentLength; } /** * 分塊下載 * * @param url 下載地址 * @param startPos 下載文件起始位置 * @param endPos 下載文件結(jié)束位置 * @return 連接對象 */ public static HttpURLConnection getHttpURLConnection(String url, long startPos, long endPos) throws IOException { HttpURLConnection httpURLConnection = getHttpURLConnection(url); LogUtils.info("下載的區(qū)間是:{}-{}", startPos, endPos); if (endPos != 0) { httpURLConnection.setRequestProperty("RANGE", "bytes=" + startPos + "-" + endPos); } else { httpURLConnection.setRequestProperty("RANGE", "bytes=" + startPos + "-"); } return httpURLConnection; } /** * 獲取 HttpURLConnection 連接對象 * * @param url 文件的地址 * @return HttpURLConnection 連接對象 */ public static HttpURLConnection getHttpURLConnection(String url) throws IOException { URL httpUrl = new URL(url); HttpURLConnection httpURLConnection = (HttpURLConnection) httpUrl.openConnection(); // 向文件所在的服務(wù)器發(fā)送標識信息 httpURLConnection.setRequestProperty("User-Agent", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.835.163 Safari/535.1"); return httpURLConnection; } /** * 獲取下載文件的名字 * * @param url 下載地址 * @return 文件名 */ public static String getHttpFileName(String url) { String fileName; int startIndex = url.lastIndexOf("/"); int endIndex = url.lastIndexOf("?"); if (endIndex == -1) { fileName = url.substring(startIndex + 1); } else { fileName = url.substring(startIndex + 1, endIndex); } int pointIndex = fileName.lastIndexOf("."); return fileName.substring(0, fileName.lastIndexOf(".")) + "-" + id + fileName.substring(pointIndex); } }
?? LogUtils
/** * Description: 日志工具類 * * @Author 狐貍半面添 * @Create 2023/11/6 1:41 * @Version 1.0 */ public class LogUtils { private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("hh:mm:ss"); public static void info(String msg, Object... args) { print(msg, "-info-", args); } public static void error(String msg, Object... args) { print(msg, "-error-", args); } private static void print(String msg, String level, Object... args) { if (args != null && args.length > 0) { msg = String.format(msg.replace("{}", "%s"), args); } String threadName = Thread.currentThread().getName(); System.out.println(LocalTime.now().format(FORMATTER) + " " + threadName + level + msg); } }
?? core 包
?? DownloadInfoThread
/** * Description: 展示下載信息 * * @Author 狐貍半面添 * @Create 2023/11/6 2:07 * @Version 1.0 */ @SuppressWarnings("AlibabaUndefineMagicConstant") public class DownloadInfoThread implements Runnable { /** * 下載文件總大小 */ private final long httpFileContentLength; /** * 本次累計下載的大小 */ public static volatile LongAdder downSize = new LongAdder(); /** * 前一次下載的大小 */ public double prevSize; public DownloadInfoThread(long httpFileContentLength) { this.httpFileContentLength = httpFileContentLength; } @Override public void run() { // 計算文件總大小 單位是 MB String httpFileSize = String.format("%.2f", httpFileContentLength / Constant.MB); // 計算每秒下載速度 kb int speed = (int) ((downSize.doubleValue() - prevSize) / Constant.KB); prevSize = downSize.doubleValue(); // 剩余文件的大小 double remainSize = httpFileContentLength - downSize.doubleValue(); // 計算剩余時間 String remainTime = String.format("%.1f", remainSize / Constant.KB / speed); if ("Infinity".equalsIgnoreCase(remainTime)) { remainTime = "-"; } // 已下載大小 String currentFileSize = String.format("%.1f", downSize.doubleValue() / Constant.MB); String speedInfo = String.format("已下載 %smb/%smb,速度 %skb/s,剩余時間 %ss", currentFileSize, httpFileSize, speed, remainTime); System.out.print("\r"); System.out.print(speedInfo); } }
?? DownloaderTask
/** * Description: 分塊下載任務(wù) * * @Author 狐貍半面添 * @Create 2023/11/7 0:58 * @Version 1.0 */ public class DownloaderTask implements Callable<Boolean> { private final String url; /** * 下載起始位置 */ private final long startPos; /** * 下載結(jié)束位置 */ private final long endPos; /** * 標識當前是哪一部分 */ private final int part; private final CountDownLatch countDownLatch; public DownloaderTask(String url, long startPos, long endPos, int part, CountDownLatch countDownLatch) { this.url = url; this.startPos = startPos; this.endPos = endPos; this.part = part; this.countDownLatch = countDownLatch; } @Override public Boolean call() throws Exception { // 獲取文件名 String httpFileName = HttpUtils.getHttpFileName(url); // 分塊的文件名 httpFileName = httpFileName + Constant.PART_FILE_SUFFIX + part; // 下載路徑 httpFileName = Constant.PATH + httpFileName; // 獲取分塊下載的連接 HttpURLConnection httpURLConnection = HttpUtils.getHttpURLConnection(url, startPos, endPos); try ( InputStream input = httpURLConnection.getInputStream(); BufferedInputStream bis = new BufferedInputStream(input); RandomAccessFile accessFile = new RandomAccessFile(httpFileName, "rw"); ) { byte[] buffer = new byte[Constant.BYTE_SIZE]; int len; // 循環(huán)讀取數(shù)據(jù) while ((len = bis.read(buffer)) != -1) { // 1s 內(nèi)下載的數(shù)據(jù),通過原子類下載 DownloadInfoThread.downSize.add(len); accessFile.write(buffer, 0, len); } } catch (FileNotFoundException e) { LogUtils.error("下載文件不存在 {}", url); return false; } catch (Exception e) { LogUtils.error("下載出現(xiàn)異常"); return false; } finally { httpURLConnection.disconnect(); countDownLatch.countDown(); } return true; } }
?? Downloader
/** * Description: 下載器 * * @Author 狐貍半面添 * @Create 2023/11/6 1:21 * @Version 1.0 */ public class Downloader { private final ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1); public ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(Constant.THREAD_NUM, Constant.THREAD_NUM, 0, TimeUnit.SECONDS, new ArrayBlockingQueue<>(5)); private CountDownLatch countDownLatch = new CountDownLatch(Constant.THREAD_NUM); public void download(String url) { // 獲取文件名 String httpFileName = HttpUtils.getHttpFileName(url); // 文件下載路徑 httpFileName = Constant.PATH + httpFileName; // 獲取本地文件的大小 long localFileLength = FileUtils.getFileContentLength(httpFileName); HttpURLConnection httpURLConnection = null; DownloadInfoThread downloadInfoThread; try { // 獲取連接對象 httpURLConnection = HttpUtils.getHttpURLConnection(url); // 獲取下載文件的總大小 int contentLength = httpURLConnection.getContentLength(); // 判斷文件是否已下載過 if (localFileLength >= contentLength) { LogUtils.info("{} 已下載完畢,無需重新下載", httpFileName); // 關(guān)閉連接對象 httpURLConnection.disconnect(); // 關(guān)閉線程池 scheduledExecutorService.shutdownNow(); poolExecutor.shutdown(); return; } // 創(chuàng)建獲取下載信息的任務(wù)對象 downloadInfoThread = new DownloadInfoThread(contentLength); // 將任務(wù)交給線程執(zhí)行,每隔 1s 打印一次 scheduledExecutorService.scheduleAtFixedRate(downloadInfoThread, 1, 1, TimeUnit.SECONDS); // 切分任務(wù) ArrayList<Future> list = new ArrayList<>(); split(url, list); countDownLatch.await(); System.out.print("\r"); System.out.println("分塊文件下載完成"); // 合并文件 if (merge(httpFileName)) { // 清除臨時文件 clearTemp(httpFileName); } } catch (IOException | InterruptedException e) { e.printStackTrace(); } finally { System.out.println("本次執(zhí)行完成"); // 關(guān)閉連接對象 if (httpURLConnection != null) { httpURLConnection.disconnect(); } // 關(guān)閉線程池 scheduledExecutorService.shutdownNow(); poolExecutor.shutdown(); } } /** * 文件切分 * * @param url 文件鏈接 * @param futureList 任務(wù)集合 */ public void split(String url, ArrayList<Future> futureList) { try { // 獲取下載文件大小 long contentLength = HttpUtils.getHttpFileContentLength(url); // 計算切分后的文件大小 long size = contentLength / Constant.THREAD_NUM; // 計算分塊個數(shù) for (int i = 0; i < Constant.THREAD_NUM; i++) { // 計算下載起始位置 long startPos = i * size; // 計算結(jié)束位置 long endPos; if (i == Constant.THREAD_NUM - 1) { // 下載最后一塊 endPos = 0; } else { endPos = startPos + size - 1; } // 創(chuàng)建任務(wù)對象 DownloaderTask downloaderTask = new DownloaderTask(url, startPos, endPos, i, countDownLatch); // 將任務(wù)提交到線程池 Future<Boolean> future = poolExecutor.submit(downloaderTask); futureList.add(future); } } catch (IOException e) { throw new RuntimeException(e); } } /** * 文件合并 * * @param fileName 文件名 * @return 是否合并成功 */ public boolean merge(String fileName) { LogUtils.info("開始合并文件 {}", fileName); byte[] buffer = new byte[Constant.BYTE_SIZE]; int len; try ( RandomAccessFile accessFile = new RandomAccessFile(fileName, "rw") ) { for (int i = 0; i < Constant.THREAD_NUM; i++) { try ( BufferedInputStream bis = new BufferedInputStream(new FileInputStream(fileName + Constant.PART_FILE_SUFFIX + i)) ) { while ((len = bis.read(buffer)) != -1) { accessFile.write(buffer, 0, len); } } } LogUtils.info("文件合并完畢 {}", fileName); } catch (Exception e) { e.printStackTrace(); return false; } return true; } /** * 清除臨時文件 * * @param fileName 文件名 */ public void clearTemp(String fileName) { LogUtils.info("清理分塊文件"); for (int i = 0; i < Constant.THREAD_NUM; i++) { String name = fileName + Constant.PART_FILE_SUFFIX + i; File file = new File(name); file.delete(); } LogUtils.info("分塊清除完畢"); } }
?? Main 主類
public class Main { public static void main(String[] args) { // 下載地址 String url = null; if (args == null || args.length == 0) { while (url == null || url.trim().isEmpty()) { System.out.print("請輸入下載鏈接:"); Scanner scanner = new Scanner(System.in); url = scanner.next(); } } else { url = args[0]; } Downloader downloader = new Downloader(); downloader.download(url); } }
到此這篇關(guān)于java多線程文件下載器的實現(xiàn)的文章就介紹到這了,更多相關(guān)java多線程文件下載器內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java的LinkedHashMap的實現(xiàn)原理詳解
這篇文章主要介紹了Java的LinkedHashMap的實現(xiàn)原理詳解,???LinkedHashMap是Map接口的哈希表和鏈接列表實現(xiàn),具有可預(yù)知的迭代順序,此實現(xiàn)提供所有可選的映射操作,并允許使用null值和null鍵,此類不保證映射的順序,特別是它不保證該順序恒久不變,需要的朋友可以參考下2023-09-09利用Spring Cloud Config結(jié)合Bus實現(xiàn)分布式配置中心的步驟
這篇文章主要介紹了利用Spring Cloud Config結(jié)合Bus實現(xiàn)分布式配置中心的相關(guān)資料,文中通過示例代碼將實現(xiàn)的步驟一步步介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友下面來一起看看吧2018-05-05springboot+mybatis-plus基于攔截器實現(xiàn)分表的示例代碼
本文主要介紹了springboot+mybatis-plus基于攔截器實現(xiàn)分表,文中通過示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2021-11-11IDEA之啟動參數(shù),配置文件默認參數(shù)的操作
這篇文章主要介紹了IDEA之啟動參數(shù),配置文件默認參數(shù)的操作,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2021-01-01Maven配置中repositories、distributionManagement、pluginRepositori
這篇文章主要介紹了Maven配置中repositories、distributionManagement、pluginRepositories用法及將已有jar包部署到私服,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2025-03-03Java?Web中ServletContext對象詳解與應(yīng)用
ServletContext是一個容器,可以用來存放變量,供一個web項目中多個Servlet共享,下面這篇文章主要給大家介紹了關(guān)于Java?Web中ServletContext對象詳解與應(yīng)用的相關(guān)資料,需要的朋友可以參考下2023-04-04解決在啟動eclipse的tomcat進行訪問時出現(xiàn)404問題的方法
這篇文章主要介紹了解決在啟動eclipse的tomcat進行訪問時出現(xiàn)404問題的方法,感興趣的小伙伴們可以參考一下2016-04-04