题目
设计一个多线程文件下载器,使用线程池管理下载任务
信息
- 类型:问答
- 难度:⭐⭐
考点
线程池配置,多线程任务分割,资源同步,异常处理,性能优化
快速回答
实现要点:
- 使用
ThreadPoolExecutor自定义线程池,避免Executors默认方法 - 通过
Range请求头分割文件为多个分片并行下载 - 使用
CountDownLatch同步下载线程 - 通过
RandomAccessFile实现分片写入 - 异常处理需包含重试机制和线程中断
- 添加进度监控和资源清理逻辑
核心设计原理
通过HTTP的Range头实现文件分片下载,每个线程下载指定字节范围,使用线程池管理下载任务,最后合并成分片文件。
代码实现示例
public class MultiThreadDownloader {
private final ExecutorService executor;
private final int threadCount;
private final String targetUrl;
public MultiThreadDownloader(String url, int threads) {
this.threadCount = threads;
this.targetUrl = url;
// 创建自定义线程池(核心线程数=最大线程数,使用有界队列)
this.executor = new ThreadPoolExecutor(
threads, threads, 0L, TimeUnit.MILLISECONDS,
new ArrayBlockingQueue<>(threads * 2),
new ThreadPoolExecutor.CallerRunsPolicy());
}
public void download(String savePath) throws Exception {
long fileSize = getFileSize();
long chunkSize = fileSize / threadCount;
CountDownLatch latch = new CountDownLatch(threadCount);
RandomAccessFile file = new RandomAccessFile(savePath, "rw");
file.setLength(fileSize); // 预分配空间
for (int i = 0; i < threadCount; i++) {
long start = i * chunkSize;
long end = (i == threadCount - 1) ? fileSize - 1 : start + chunkSize - 1;
executor.execute(() -> {
try (HttpClient client = HttpClient.newHttpClient()) {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(targetUrl))
.header("Range", "bytes=" + start + "-" + end)
.build();
HttpResponse response = client.send(
request, HttpResponse.BodyHandlers.ofInputStream());
try (InputStream is = response.body()) {
file.seek(start);
byte[] buffer = new byte[8192];
int bytesRead;
while ((bytesRead = is.read(buffer)) != -1) {
file.write(buffer, 0, bytesRead);
}
}
} catch (Exception e) {
executor.shutdownNow(); // 中断所有线程
throw new RuntimeException("Download failed", e);
} finally {
latch.countDown();
}
});
}
latch.await();
executor.shutdown();
file.close();
}
private long getFileSize() throws Exception {
// 实现获取文件大小的逻辑(略)
}
}
最佳实践
- 线程池配置:避免使用
Executors.newFixedThreadPool,需自定义ThreadPoolExecutor并设置合理的队列大小和拒绝策略 - 分片策略:根据文件大小动态计算分片,最后一个分片需包含剩余字节
- 资源管理:使用try-with-resources确保HTTP连接和文件句柄关闭
- 错误恢复:实现分片级重试机制(示例中简化处理)
常见错误
- 线程泄露:未正确关闭线程池(必须调用
shutdown()) - 文件写入冲突:未使用
RandomAccessFile的seek()定位写位置 - 内存溢出:使用无界队列导致OOM(应选
ArrayBlockingQueue) - 进度卡死:未处理子线程异常导致
CountDownLatch无法归零
扩展知识
- 动态分片调整:根据网络状况实时调整分片大小
- 断点续传:记录已下载分片位置,重启时恢复
- 速度限制:通过
Semaphore控制下载速率 - 性能监控:添加
ThreadPoolExecutor的RejectedExecutionHandler记录拒绝事件