栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

多线程下载文件提升性能

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

多线程下载文件提升性能

多线程下载文件 前言

最近项目中导出和下载文件因为数据量太大,导致下载时间过长,甚至出现下载不下来文件(超时,超出nginx的最大时间)

故此使用多线程优化一下

思考

不管是多线程还是单线程,对于下载和导出文件,然后再下载到浏览器,核心就是根据文件类型,将字节写入到浏览器。

了解知识

浏览器识别文件的类型

已经生成好字节数组和需要读取文件下载

读取文件的时候,需要随机读(指定跳过多少字节),这里我们使用RandomAccessFile这个类,具体可以百度了解一下。

浏览器识别字节的顺序

这里需要注意的是:

单线程下载时候直接不需要顺序,直接向浏览器中写的顺序就是字节顺序、而多线程顺序是不一致的,需要将写入的字节顺序固定,让浏览器自己将字节组装好然后生成文件再展示出来。

http响应体Content-Range去指定字节的顺序

response.setHeader("Content-Range", "bytes=" + finalStart + "-" + finalEnd);

选择适合自己的线程池(其中核心线程数,时间,队列大小,策略等)

//IO密集型来说,一般是最佳线程数 =CPU 核数 * [ 1 +(I/O 耗时 / CPU 耗时)]
    //private static final int corePoolSize = Runtime.getRuntime().availableProcessors() * (1 + (500/100));
    //默认使用拒绝策略
    //todo ??? 待确认设置核心线程数
    public static final ExecutorService pool =  new ThreadPoolExecutor(10,20, 60L, TimeUnit.SECONDS, new linkedBlockingQueue(1000));


多线程分别处理每个文件(也就是字节的一部分,如下图)

既然思路已经明白了,那就直接开始展示代码得了。

字节数组多线程下载

针对在内存中生成好的字节数组

大白话就是:将字节数组按照指定规则分为几个,然后分别给不同的线程去写入到浏览器即可。

   boolean falg = false;
        ArrayList arrayList = new ArrayList<>(content.length);
        for (int i = 0; i < content.length; i++) {
            arrayList.add(content[i]);
        }
        OutputStream outputStream = new BufferedOutputStream(response.getOutputStream());
        int fileLength = content.length;
        response.addHeader("Content-Length", "" + fileLength);
        // 获取每片大小
        List newList = subArrayPey(arrayList, fileLength, 5);

        for (SubObje sub : newList) {
            loggerHelper.infoLog("start =  " + sub.getStart() + " end = " + sub.getEnd() + " size = " + sub.getSubList().size());
            try {
                int finalStart = sub.getStart();
                int finalEnd = sub.getEnd();
                List finalSubList = sub.subList;
                pool.submit(() -> {
                    response.setHeader("Content-Range", "bytes=" + finalStart + "-" + finalEnd);
                    try {
                        byte[] silenArray = new byte[finalSubList.size()];
                        for (int j = 0; j < finalSubList.size(); j++) {
                            silenArray[j] = finalSubList.get(j);
                        }
                        outputStream.write(silenArray);
                        outputStream.flush();
                        response.flushBuffer();
                    } catch (Exception e) {
                        loggerHelper.infoLog(Thread.currentThread().getName() + " 下载异常: " + e.getMessage());
                        e.printStackTrace();
                    }
                }).get(1, TimeUnit.MINUTES);
            } catch (InterruptedException e) {
                //e.printStackTrace();
                loggerHelper.infoLog("多线程导出异常");
                falg = true;
            } catch (ExecutionException e) {
                //e.printStackTrace();
                loggerHelper.infoLog("多线程导出异常");
                falg = true;
            } catch (TimeoutException e) {
                //e.printStackTrace();
                loggerHelper.infoLog("多线程导出超时");
                falg = true;
            }
        }
        if (outputStream!=null){
            outputStream.flush();
            outputStream.close();
        }

        response.flushBuffer();

顺序字节实体类

 @Data
    class SubObje {

        private int start;
        private int end;

        private List subList;

        public SubObje() {
        }

        public SubObje(int start, int end, List subList) {
            this.start = start;
            this.end = end;
            this.subList = subList;
        }
    }

将字节数组切分几份

    
    private  List subArrayPey(List arrayList, int fileLength, int poolLength) {
        List newList = new ArrayList<>();
        int slice = fileLength / poolLength;
        for (int i = 0; i < poolLength; i++) {
            int start = i * slice;
            int end = (i + 1) * slice - 1;
            List subList = arrayList.subList(start, end + 1);
            if (i == poolLength - 1) {
                start = i * slice;
                end = fileLength;
                subList = arrayList.subList(start, end);
            }
            newList.add( new SubObje(start,end,subList));
        }
        return newList;
    }
下载文件

具体代码和上面字节数组下载是一样的,唯一不同的是每个线程读取文件需要读取自己负责的那一段字节,具体请看下段代码中的核心代码片段。

                // 3分钟超时
                pool.submit(() ->  makeFile(vcode, fileList, dirPath, cardNo) ).get(3, TimeUnit.MINUTES);
            }

            response.addHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode("xxxx" + ".zip", "UTF-8"));
            response.setContentType("application/octet-stream");
            OutputStream outputStream = new BufferedOutputStream(response.getOutputStream());

            //启动多线程开始下载
            String path = fileList.get(0).getPath();
            String zipPath = path.substring(0, path.lastIndexOf(File.separator));
            FileOutputStream fos2 = new FileOutputStream(zipPath + ".zip");
            ZipUtils.toZip(fileList, fos2);
            //压缩
            String zipName = zipPath + ".zip";
            //查文件
            File file = new File(zipName);
//--------------------  核心代码 -------------
            //"r", "rw", "rws", or "rwd"
            RandomAccessFile rf = new RandomAccessFile(file, "rw");//只读
            //文件长度
            long fileLength = file.length();
            //大小 这里流大小表示总的大小
            response.addHeader("Content-Length", "" + fileLength);
            long poolLength = 5;
            // 获取每片大小
            long slice = fileLength / poolLength;
            for (int i = 0; i < poolLength; i++) {
                long start = i * slice;
                long end = (i + 1) * slice - 1;
                if (i == poolLength - 1) {
                    start = i * slice;
                    end = fileLength;
                }
                //字节范围
                response.setHeader("Content-Range", "bytes=" + start + "-" + end);

                long finalStart = start;
                long finalEnd = end;
                RandomAccessFile finalRf = rf;

                pool.submit(() -> {
                    try {
                        //跳过startPos个字节,表明该线程只下载自己负责哪部分文件。
                        finalRf.seek(finalStart);
                        System.out.println("起始位置" + finalStart);

//--------------------  核心代码 -------------


                        int lent = Long.valueOf(finalEnd - finalStart).intValue() + 1;
                        byte[] buffer = new byte[1024];
                        int read = 0;
                        int lenth = 0;
                        //1 这里循环读 循环写
                        while ((lenth = finalRf.read(buffer)) != -1) {
                            lenth += buffer.length;
                            outputStream.write(buffer);
                            if (lenth == lent) {
                                break;
                            }
                        }
                        // todo 2 也可以一次性读完
                        //finalRf.read(buffer, 0, lent);
                        //outputStream.write(buffer);

                        outputStream.flush();
                        response.flushBuffer();

                    } catch (Exception e) {
                        logger.info(Thread.currentThread().getName() + " 下载异常: " + e.getMessage());
                        e.printStackTrace();
                    }
                }).get(1, TimeUnit.MINUTES);
            }
            if(outputStream!=null){
            outputStream.close();
            }
            response.flushBuffer();
            rf.close();

最后

这里我使用分n份,没有按照分多少字节去分。

就是说,如果一个服务器内存只有1g,需要上传10g的文件,此时就需要使用字节去分;意思就是每次写从内存不能超过1g(理论值),需要一边读一边写,等内存释放了再去读再去写,一次循环即可。就是所说的断点上传文件技术。

不管怎样,总之控制好文件字节顺序就可以了,不论是下载还是写文件。

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/736707.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号