侧边栏壁纸
博主头像
colo

欲买桂花同载酒

  • 累计撰写 1823 篇文章
  • 累计收到 0 条评论

设计一个多线程文件下载器,使用线程池管理下载任务

2025-12-12 / 0 评论 / 4 阅读

题目

设计一个多线程文件下载器,使用线程池管理下载任务

信息

  • 类型:问答
  • 难度:⭐⭐

考点

线程池配置,多线程任务分割,资源同步,异常处理,性能优化

快速回答

实现要点:

  • 使用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()
  • 文件写入冲突:未使用RandomAccessFileseek()定位写位置
  • 内存溢出:使用无界队列导致OOM(应选ArrayBlockingQueue
  • 进度卡死:未处理子线程异常导致CountDownLatch无法归零

扩展知识

  • 动态分片调整:根据网络状况实时调整分片大小
  • 断点续传:记录已下载分片位置,重启时恢复
  • 速度限制:通过Semaphore控制下载速率
  • 性能监控:添加ThreadPoolExecutorRejectedExecutionHandler记录拒绝事件