题目
使用线程池实现多线程文件下载器
信息
- 类型:问答
- 难度:⭐⭐
考点
线程池配置,任务拆分策略,资源同步控制,异常处理
快速回答
实现要点:
- 使用
ThreadPoolExecutor配置核心参数 - 将大文件分割为多个区块并行下载
- 使用
CountDownLatch同步下载线程 - 合并文件时处理字节顺序
- 通过
try-with-resources确保资源关闭
问题场景
实现多线程下载器可显著提升大文件下载效率。核心思路是将文件分割为多个区块,利用线程池并行下载,最后合并为完整文件。
实现方案
public class ConcurrentFileDownloader {
private static final int THREAD_COUNT = 4;
private static final String FILE_URL = "https://example.com/largefile.zip";
public static void main(String[] args) throws Exception {
// 1. 获取文件总大小
long fileSize = getFileSize(FILE_URL);
// 2. 计算区块大小(每个线程下载的字节范围)
long chunkSize = fileSize / THREAD_COUNT;
// 3. 创建线程池
ExecutorService executor = Executors.newFixedThreadPool(THREAD_COUNT);
CountDownLatch latch = new CountDownLatch(THREAD_COUNT);
// 4. 存储下载结果的字节数组
byte[][] chunks = new byte[THREAD_COUNT][];
// 5. 提交下载任务
for (int i = 0; i < THREAD_COUNT; i++) {
final int index = i;
long start = i * chunkSize;
long end = (i == THREAD_COUNT - 1) ? fileSize - 1 : start + chunkSize - 1;
executor.submit(() -> {
try (HttpURLConnection conn = (HttpURLConnection) new URL(FILE_URL).openConnection()) {
conn.setRequestProperty("Range", "bytes=" + start + "-" + end);
try (InputStream is = conn.getInputStream();
ByteArrayOutputStream buffer = new ByteArrayOutputStream()) {
byte[] data = new byte[4096];
int bytesRead;
while ((bytesRead = is.read(data)) != -1) {
buffer.write(data, 0, bytesRead);
}
chunks[index] = buffer.toByteArray();
}
} catch (Exception e) {
// 处理异常并中断其他线程
executor.shutdownNow();
e.printStackTrace();
} finally {
latch.countDown();
}
});
}
// 6. 等待所有任务完成
latch.await();
executor.shutdown();
// 7. 合并文件
try (FileOutputStream fos = new FileOutputStream("downloaded_file.zip")) {
for (byte[] chunk : chunks) {
if (chunk != null) fos.write(chunk);
}
}
}
private static long getFileSize(String url) throws IOException {
HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
conn.setRequestMethod("HEAD");
return conn.getContentLengthLong();
}
}关键原理
- Range请求:通过HTTP Header的
Range字段指定下载字节范围 - 线程池配置:固定大小线程池避免资源耗尽
- 同步机制:
CountDownLatch确保所有区块下载完成后再合并 - 内存管理:每个线程使用
ByteArrayOutputStream暂存数据
最佳实践
- 参数调优:根据网络环境和文件大小动态计算线程数
- 错误重试:为每个下载块添加重试机制
- 进度监控:通过
AtomicLong统计已下载字节 - 资源释放:使用try-with-resources确保连接关闭
常见错误
- 线程数过多:导致服务器拒绝连接或本地资源耗尽
- 字节计算错误:未处理文件大小不能被整除的情况
- 内存溢出:下载超大文件时未使用流式写入磁盘
- 异常处理缺失:未处理线程中断导致程序挂起
扩展知识
- 连接复用:使用HTTP Client API代替
HttpURLConnection支持连接池 - 断点续传:将下载进度持久化到文件
- 流量控制:通过
Semaphore限制下载速度 - 异步回调:使用
CompletableFuture实现非阻塞合并