使用线程池批量下载文件

使用线程池的优点:

减少资源创建 => 减少内存开销,创建线程占用内存
降低系统开销 => 创建线程需要时间,会延迟处理的请求
提高稳定性 => 避免无限创建线程引起的OutOfMemoryError【简称OOM】

Executors有以下四个方法可以创建线程池:

Executors.newFixedThreadPool() => 创建一个装有固定数量线程的线程池
Executors.newSingleThreadExecutor() => 创建单线程的线程池
Executors.newCachedThreadPool() => 让线程池根据处理需求动态地调整线程数量,比如线程池内线程的数量超过需求时,自动回收线程
Executors.newScheduledThreadPool() => 创建一个定时、周期性执行任务的线程池

ThreadPoolExecutor的构造函数共有四个,但最终调用的都是同一个:

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler)

具体参数说明:
                          
corePoolSize => 线程池核心线程数量

maximumPoolSize => 线程池最大数量

keepAliveTime => 空闲线程存活时间

unit => 时间单位

workQueue => 线程池所使用的缓冲队列

threadFactory => 线程池创建线程使用的工厂

handler => 线程池对拒绝任务的处理策略

线程池执行逻辑说明:

1、判断核心线程数是否已满,核心线程数大小和corePoolSize参数有关,未满则创建线程执行任务

2、若核心线程池已满,判断队列是否满,队列是否满和workQueue参数有关,若未满则加入队列中

3、若队列已满,判断线程池是否已满,线程池是否已满和maximumPoolSize参数有关,若未满创建线程执行任务

4、若线程池已满,则采用拒绝策略处理无法执行的任务,拒绝策略和handler参数有关

利用多线程同步批量下载文件样例:

package com.example.demo;

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

public class ThreadUtil {

    /**
     * 创建下载线程池
     *
     * @param threadSize 线程数量
     * @return ExecutorService
     */
    public static ExecutorService buildDownThreadPool(int threadSize) {
        // 空闲线程存活时间
        long keepAlive = 0L;
        // 下载线程名称前缀
        String dwonThreadNamePrefix = "dwonThread-pool";
        // 构建线程工厂
        ThreadFactory factory = ThreadUtil.buildThreadFactory(dwonThreadNamePrefix);
        //创建线程池
        return new ThreadPoolExecutor(threadSize,
                threadSize,
                keepAlive,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(threadSize),
                factory);
    }

    /**
     * 创建自定义线程工厂
     *
     * @param threadNamePrefix 线程名称前缀
     * @return ThreadFactory
     */
    public static ThreadFactory buildThreadFactory(String threadNamePrefix) {
        return new CustomThreadFactory(threadNamePrefix);
    }

    /**
     * 自定义线程工厂
     */
    public static class CustomThreadFactory implements ThreadFactory {
        //线程名称前缀
        private String threadNamePrefix;

        private AtomicInteger counter = new AtomicInteger(1);

        /**
         * 自定义线程工厂
         *
         * @param threadNamePrefix 线程名称前缀
         */
        CustomThreadFactory(String threadNamePrefix) {
            this.threadNamePrefix = threadNamePrefix;
        }

        @Override
        public Thread newThread(Runnable r) {
            String threadName = threadNamePrefix + "-t" + counter.getAndIncrement();
            return new Thread(r, threadName);
        }
    }
}
package com.example.demo;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.*;
import java.util.concurrent.*;

/**
 * 文件下载工具类
 *
 */
public class DownloadUtil {

    private static Logger logger = LoggerFactory.getLogger(DownloadUtil.class);

    /**
     * 下载线程数
     */
    private static final int DOWNLOAD_THREAD_NUM = 8;

    /**
     * 下载线程池
     */
    private static ExecutorService downloadExecutorService = ThreadUtil.buildDownThreadPool(DOWNLOAD_THREAD_NUM);

    /**
     * 单文件下载
     *
     * @param fileUrl 文件url,如:https://img3.doubanio.com//view//photo//s_ratio_poster//public//p2369390663.webp
     * @param path 存放路径,如:D:/opt/img/douban/my.webp
     */
    public static void download(String fileUrl, String path) throws Exception {
        // 判断存储文件夹是否已经存在或者创建成功
        if (!createFolderIfNotExists(path)) {
            logger.error("Can't create folder:{}", getFolder(path));
            return;
        }
        HttpURLConnection conn = null;
        InputStream in = null;
        FileOutputStream out = null;
        try {
            URL url = new URL(fileUrl);
            conn = (HttpURLConnection) url.openConnection();
            conn.setRequestMethod("GET");
            conn.setConnectTimeout(10000);
            in = conn.getInputStream();
            out = new FileOutputStream(path);
            int len;
            byte[] arr = new byte[1024 * 1000];
            while (-1 != (len = in.read(arr))) {
                out.write(arr, 0, len);
            }
            out.flush();
        } catch (Exception e) {
            logger.error("Fail to download: {} by {}", fileUrl, e.getMessage());
            throw new Exception(e.getMessage());
        } finally {
            try {
                if(null != conn){
                    conn.disconnect();
                }
                if (null != out) {
                    out.close();
                }
                if (null != in) {
                    in.close();
                }
            } catch (Exception e) {
                logger.error("Error to close stream: {}", e.getMessage());
                throw new Exception(e.getMessage());
            }
        }
    }

    /**
     * 批量多线程下载资源
     *
     * @param resourceMap 资源map, key为资源下载url,value为资源存储位置
     */
    public static void batchDownLoad(Map<String, String> resourceMap) {
        if (Objects.isNull(resourceMap) || resourceMap.isEmpty()) {
            return;
        }
        try {
            List<String> keys = new ArrayList<>(resourceMap.keySet());
            int size = keys.size();
            int pageNum = getPageNum(size);
            long startTime = System.currentTimeMillis();
            for (int index = 0; index < pageNum; index++) {
                int start = index * DOWNLOAD_THREAD_NUM;
                int last = getLastNum(size, start + DOWNLOAD_THREAD_NUM);
                //线程计数器
                final CountDownLatch latch = new CountDownLatch(last - start);
                // 获取列表子集
                List<String> urlList = keys.subList(start, last);
                for (String url : urlList) {
                    // 提交任务
                    Runnable task = new DownloadWorker(latch, url, resourceMap.get(url));
                    downloadExecutorService.submit(task);
                }
                latch.await();
            }
            long endTime = System.currentTimeMillis();
            logger.info("Download total time " + (endTime -startTime) + " millisSeconds");
        } catch (Exception e) {
            logger.error("{}", e);
        }finally {
            //关闭线程池
            if(null != downloadExecutorService){
                downloadExecutorService.shutdown();
            }
        }
    }
    /**
     * 创建文件夹,如果文件夹已经存在或者创建成功返回true
     *
     * @param path 路径
     * @return boolean
     */
    private static boolean createFolderIfNotExists(String path) {
        String folderName = getFolder(path);
        if (folderName.equals(path)) {
            return true;
        }
        File folder = new File(getFolder(path));
        if (!folder.exists()) {
            synchronized (DownloadUtil.class) {
                if (!folder.exists()) {
                    return folder.mkdirs();
                }
            }
        }
        return true;
    }

    /**
     * 获取文件夹
     *
     * @param path 文件路径
     * @return String
     */
    private static String getFolder(String path) {
        int index = path.lastIndexOf("/");
        return -1 != index ? path.substring(0, index) : path;
    }

    /**
     * 获取最后一个元素
     *
     * @param size 列表长度
     * @param index 下标
     * @return int
     */
    private static int getLastNum(int size, int index) {
        return index > size ? size : index;
    }

    /**
     * 获取划分页面数量
     *
     * @param size 列表长度
     * @return int 划分页面数量
     */
    private static int getPageNum(int size) {
        int tmp = size / DOWNLOAD_THREAD_NUM;
        return size % DOWNLOAD_THREAD_NUM == 0 ? tmp : tmp + 1;
    }

    /**
     * 下载线程
     */
    static class DownloadWorker implements Runnable {

        private CountDownLatch latch;
        private String url;
        private String path;

        DownloadWorker(CountDownLatch latch, String url, String path) {
            this.latch = latch;
            this.url = url;
            this.path = path;
        }

        @Override
        public void run() {
            try {
                logger.debug("Start batch:[{}] into: [{}]", url, path);
                DownloadUtil.download(url, path);
                logger.debug("Download:[{}] into: [{}] is done", url, path);
            } catch (Exception e) {
            }finally {
                latch.countDown();
            }
        }
    }

    public static void main(String[] args) {
        Map<String, String> map = new HashMap<>();
        map.put("http://jspro-test.oss-cn-shenzhen.aliyuncs.com/8d30007c83084e4bb341f73b6af05ec35470?Expires=1651224353&OSSAccessKeyId=LTAI4FvrfndXDm83BSgCrSoZ&Signature=fieoMu8Biye8KL12eh%2FgwTRus2Q%3D","D:/test/11/1.MP4");
        map.put("http://jspro-test.oss-cn-shenzhen.aliyuncs.com/179299140060451eae4b1f42ba1449ba1730?Expires=1651224414&OSSAccessKeyId=LTAI4FvrfndXDm83BSgCrSoZ&Signature=OMmBBY1AZBSDG3i9br41ndUMaX4%3D","D:/test/11/2.MP4");
        map.put("http://jspro-test.oss-cn-shenzhen.aliyuncs.com/e7e630c9ac0a4ebb89f778ab99a2a7287510?Expires=1651224485&OSSAccessKeyId=LTAI4FvrfndXDm83BSgCrSoZ&Signature=b%2Bdrk8J8V%2BcCWoPPRakCcErrO5k%3D","D:/test/11/3.MP4");
        DownloadUtil.batchDownLoad(map);
    }
}

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值