使用多线程处理大数据量传输的尝试

遇到的问题

​ 在最近工作中遇到了一个千万级数据量从数据库导出到excel中去的业务,由于考虑到内存溢出的问题,所以使用的方案是10万条数据放在一个excel中,讲所有excel的文件存储路径存在一个list中,最后将所有的excel文件放入一个压缩包中,返回给页面。

​ 在最后进行性能测试时无法满足要求,于是想到了使用多线程的方式来进行性能优化,以下是多线程方面的一些尝试。

多线程的一些尝试

1 使用线程池+callable

首先想到的是使用callable的方式,因为业务在进行操作结束后,需要将excel的存储路径返回,所以先想到了callable的方式进行处理,(可以使用,但是要注意防止主线程阻塞)代码如下:

首先创建了callable实现类:

package com.codebull;

import java.util.Random;
import java.util.concurrent.Callable;

/**
 * @Author: CodeBull
 * @Date: 2021/6/8
 * @version: 1.0
 */
public class CallableImpl implements Callable<String> {
    /** 线程名称 */
    private String name;
    /** 开始页码 */
    private int pageNum;
    /** 结束页码 */
    private int count;

    public CallableImpl(String name, int pageNum, int end) {
        this.name = name;
        this.pageNum = pageNum;
        this.count = end;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getStart() {
        return pageNum;
    }

    public void setStart(int start) {
        this.pageNum = start;
    }

    public int getEnd() {
        return count;
    }

    public void setEnd(int count) {
        this.count = count;
    }

    @Override
    public String call() throws Exception {
        //生成一个随机数,模拟从操作时间
        int randem = new Random().nextInt(10) + 1;
        //线程睡眠
        Thread.sleep(randem * 1000);
        return "我是" + name + ",我保存第" + pageNum + "页的" + count + "条数据,花费时间是:" + randem + "秒";
    }

}

然后使用线程池的方式进行了模拟操作:

public static void main(String[] args) throws ExecutionException, InterruptedException {
    //创建集合,接受返回值
    List<String> list = new ArrayList<>();
    //创建线程池
    ExecutorService es = Executors.newFixedThreadPool(10);
    //记录开始时间
    long start = System.currentTimeMillis();
    //使用循环模拟多线程
    for (int i = 0; i < 10; i++) {
        Future<String> future = es.submit(new CallableImpl("我是线程" + i,i,100));
        list.add(future.get());
    }
    //关闭线程池
    es.shutdown();
    //记录结束时间
    long end = System.currentTimeMillis();
    //打印获取的结果
    list.forEach(System.out::println);
    //总耗时
    System.err.println("总耗时:" + ((end - start)/1000));
}

执行后获取执行结果:

总耗时:64
我是我是线程0,我保存第0页的100条数据,花费时间是:5秒
我是我是线程1,我保存第1页的100条数据,花费时间是:6秒
我是我是线程2,我保存第2页的100条数据,花费时间是:1秒
我是我是线程3,我保存第3页的100条数据,花费时间是:8秒
我是我是线程4,我保存第4页的100条数据,花费时间是:2秒
我是我是线程5,我保存第5页的100条数据,花费时间是:9秒
我是我是线程6,我保存第6页的100条数据,花费时间是:10秒
我是我是线程7,我保存第7页的100条数据,花费时间是:9秒
我是我是线程8,我保存第8页的100条数据,花费时间是:7秒
我是我是线程9,我保存第9页的100条数据,花费时间是:7秒

结论:进行数据模拟后,发现由于需要获取线程执行结果,虽然表明上使用了多线程来进行数据的操作,但最后的结果还是等同于使用单一线程进行处理,所以上面这种方式是不可取的。

2 使用线程池+runnable+共享变量的方式

由于使用callable不能满足需求,所以想到了使用共享变量的方式来进行参数的传递

首先创建共享变量的工具类:

package com.codebull;

import java.util.ArrayList;
import java.util.List;

/**
 * @Author: CodeBull
 * @Date: 2021/6/8
 * @version: 1.0
 */
public class Date {
    /**
     * 创建共享变量
     */
    private static List<String> list;

    private static int number;

    //初始化变量
    static {
        list = new ArrayList<>();
    }

    /**
     * 预计结果集数量(实际业务中可根据数据总量以及分页条件计算获得)
     */
    public static void setNumber(int number) {
        Date.number = number;
    }

    /**
     * 添加元素到共享变量中
     * @param date 添加的元素
     */
    public static synchronized void addList(String date) {
        list.add(date);
    }

    public static void printList() {
        while (true){
            if (list.size() == number){
                //当结果集数量与预计数量相同时,进行操作(此处模拟操作为打印)
                list.forEach(System.out::println);
                break;
            }else {
                //当结果集数量与预计数量不相同时,等待子线程执行完成后再进行判断
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

接下来是线程实现类:

package com.codebull;

import java.util.Random;

/**
 * @Author: CodeBull
 * @Date: 2021/6/8
 * @version: 1.0
 */
public class RunnableImpl implements Runnable {

    /** 线程名称 */
    private String name;
    /** 开始页码 */
    private int pageNum;
    /** 结束页码 */
    private int count;

    public RunnableImpl(String name, int pageNum, int end) {
        this.name = name;
        this.pageNum = pageNum;
        this.count = end;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getStart() {
        return pageNum;
    }

    public void setStart(int start) {
        this.pageNum = start;
    }

    public int getEnd() {
        return count;
    }

    public void setEnd(int count) {
        this.count = count;
    }


    @Override
    public void run(){
        //生成一个随机数,模拟从操作时间
        int randem = new Random().nextInt(10) + 1;
        //线程睡眠
        try {
            Thread.sleep(randem * 1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        String code = "我是" + name + ",我保存第" + pageNum + "页的" + count + "条数据,花费时间是:" + randem + "秒";
        Date.addList(code);
    }
}

最后进行相关测试

public static void main(String[] args) {
    //创建线程池
    ExecutorService es = Executors.newFixedThreadPool(10);
    //记录开始时间
    long start = System.currentTimeMillis();
    //设置预计数量总数
    Date.setNumber(10);
    //使用循环模拟多线程
    for (int i = 0; i < 10; i++) {
        es.execute(new RunnableImpl("我是线程" + i,i,100));
    }
    //关闭线程池
    es.shutdown();
    //打印获取的结果
    Date.printList();
    //记录结束时间
    long end = System.currentTimeMillis();
    //总耗时
    System.err.println("总耗时:" + ((end - start)/1000));
}

测试结果如下

总耗时:10
我是我是线程4,我保存第4页的100条数据,花费时间是:1秒
我是我是线程3,我保存第3页的100条数据,花费时间是:2秒
我是我是线程6,我保存第6页的100条数据,花费时间是:3秒
我是我是线程1,我保存第1页的100条数据,花费时间是:4秒
我是我是线程9,我保存第9页的100条数据,花费时间是:4秒
我是我是线程2,我保存第2页的100条数据,花费时间是:7秒
我是我是线程5,我保存第5页的100条数据,花费时间是:8秒
我是我是线程7,我保存第7页的100条数据,花费时间是:8秒
我是我是线程0,我保存第0页的100条数据,花费时间是:10秒
我是我是线程8,我保存第8页的100条数据,花费时间是:10秒

可以发现,使用共享变量的方式,测试情况下(实际业务中应该和测试有误差,尤其是线程数量大时对数据库的压力),最终时间基本与花费时间最多的线程时间相同。

还有一个问题,就是共享变量的锁问题。测试例子中使用的是强锁synchronized,如果在线程量大时应该会对性能产生影响,当然,影响是基本可以忽略的。因为共享变量的操作是相对简单操作,而子线程业务随机性也较大。

3 关于线程安全的一些考虑

由于实际业务是多线程同事操作的,所以难免会在多用户同事操作时出现线程安全问题。我在这里想到了使用ThreadLocal来处理各个线程的共享变量相关的问题。也就是说将【测试2】中的静态共享变量设置为普通变量,在使用时创建该对象进行使用,在获取对象操作其中的共享变量时,从ThreadLocal中进行获取和操作,从而避免业务中的多线程问题。


版权声明:本文为qq_46329780原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。