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 用戶標(biāo)識
我們通過瀏覽器訪問某個網(wǎng)站的時候,會將當(dāng)前瀏覽器的版本,操作系統(tǒng)版本等信息的標(biāo)識發(fā)送到網(wǎng)站所在的服務(wù)器中。當(dāng)用程序代碼去訪問網(wǎng)站時,需要將這個標(biāo)識發(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 標(biāo)識核心線程的數(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 | 當(dāng)沒有空閑的線程時,新的任務(wù)會加入到 workQueue 中排隊等待 |
| threadFactory | 線程工廠,用于創(chuàng)建線程 |
| handler | 拒絕策略,當(dāng)任務(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ù)文件,該步驟其實可忽略,因為最終下載合并的文件名已采用時間戳進行了唯一標(biāo)識;
- 啟動一個線程每隔一秒打印下載情況;
- 切分任務(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ā)送標(biāo)識信息
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;
/**
* 標(biāo)識當(dāng)前是哪一部分
*/
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)文章
JSON的String字符串與Java的List列表對象的相互轉(zhuǎn)換
這篇文章主要介紹了JSON的String字符串與Java的List列表對象的相互轉(zhuǎn)換,如果在瀏覽器端JSON是list則轉(zhuǎn)為string結(jié)構(gòu)來處理,需要的朋友可以參考下2016-04-04
Spring?boot詳解fastjson過濾字段為null值如何解決
這篇文章主要介紹了解決Spring?boot中fastjson過濾字段為null值的問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-07-07
MyBatis SELECT基本查詢實現(xiàn)方法詳解
這篇文章主要介紹了MyBatis SELECT基本查詢實現(xiàn)方法詳解,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2020-08-08
解決springmvc整合Mybatis的Log4j日志輸出問題
這篇文章主要介紹了解決springmvc整合Mybatis的Log4j日志輸出問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-07-07
關(guān)于Spring中一級緩存、二級緩存和三級緩存的那些事
Spring解決循環(huán)依賴的核心思想在于提前曝,下面這篇文章主要給大家介紹了關(guān)于Spring中一級緩存、二級緩存和三級緩存的那些事,文中通過實例代碼介紹的非常詳細,需要的朋友可以參考下2022-02-02
interrupt()和線程終止方式_動力節(jié)點Java學(xué)院整理
線程的thread.interrupt()方法是中斷線程,將會設(shè)置該線程的中斷狀態(tài)位,即設(shè)置為true,中斷的結(jié)果線程是死亡、還是等待新的任務(wù)或是繼續(xù)運行至下一步,就取決于這個程序本身2017-05-05
mybatisPlus 實體類與數(shù)據(jù)庫表映射關(guān)系詳解
這篇文章主要介紹了mybatisPlus 實體類與數(shù)據(jù)庫表映射關(guān)系詳解,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教。2022-01-01

