遇到的问题
在最近工作中遇到了一个千万级数据量从数据库导出到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中进行获取和操作,从而避免业务中的多线程问题。