当我接手设计一个功能,到底需不需要考虑用到多线程?
个人分析,从我实际经历的项目出手。
需求背景:
各级省BOSS系统(这里简称BOSS-ALL),会把他们的某些数据以excel的形式上传到固定服务器的固定路径,excel的后缀为 ***20210722_APN.xlsx(不同地域省BOSS对应不同文件) ,而我这里的CRM系统(简称CRM)需要:
- 把当天的这些后缀的excel复制到当前服务器的一个临时路径
- 按照不同地域的BOSS文件分别读取数据
- 读取数据后与本系统表里的一些数据整合成新数据
- 将新数据在一个名为info_CRM的文件夹 写成excel
- 把临时路径已经处理好的文件移动到history文件夹
需要注意的点:
- 文件是多个,是可以并行处理的
- 耗费时间是包含数据计算时间和IO操作时间
- 文件里的数据多不多,串行执行的时间和上下文切换的时间相比相差多少
- 多线程会不会增加此模块的系统复杂度,新人接手后会不会有困难
- 数据可见性、数据隔离、线程安全等并发问题存不存在
接下来以不用多线程的代码例子演示一下这个需求怎么做,然后再以多线程的情况演示一下。
首先,创建文件服务器上所需要接受文件的文件夹
#创建三个文件夹
mkdir tempDir
mkdir dataDir
mkdir history
#授予当前用户和同一个群组的用户读写权限
chmod tempDir 664
chmod dataDir 664
chmod history 664
第二步,读取文件并复制到临时文件夹
/**
* 由容器产生FTPClient实例
*/
@Bean
public FTPClient fTPClient(){
//ftp服务器IP
public String hostname = "127.0.0.1";
//ftp服务器端口号默认为21
public Integer port = 21;
//ftp登录账号
public String username = "username";
//ftp登录密码
public String password = "password";
try {
public FTPClient ftpClient = new FTPClient();
ftpClient.setControlEncoding("utf-8");
ftpClient.connect(hostname, port); //连接ftp服务器
ftpClient.login(username, password); //登录ftp服务器
int replyCode = ftpClient.getReplyCode(); //是否成功登录服务器
if(!FTPReply.isPositiveCompletion(replyCode)){
//连接失败,做相应的处理措施
}
}catch(Exception e){
e.printStackTrace();
}
return ftpClient;
}
上传和下载的方法(这里一开始写上传方法,代码写的有点多,影响到了我想要表达的中心思想,后面几个方法,我只写一个外表,用语言描述整体思想)
@Autowried
FTPClient ftpClient;
String tempDir = "/a/b/tempDir";
String dataDir = "/c/d/dataDir";
String history = "/e/f/history";
/**
* 上传方法 (markdown里纯手打,未验证,当伪代码看)
* 上传路径、上传的文件名、待解析的文件
*/
public boolean uploadFile(String pathname,String fileName, String localPath,int excelNum){
boolean flag = false;
InputStream inputStream = null;
FileOutputStream fileOutputStream = null;
Workbook workbook = null;
//临时文件里的临时文件
String fileDir = tempDir + "/" + "data_XX.xlsx";
try {
//1 localPath是从远端复制到本地的excel,先把它的数据解析出来,放入List<Pojo> excelList = new ArrayList();同时更新excelNum = excelNum + excelList.size();
//2 根据excelList,再从数据库查出相关数据,汇总后得到resultList
//3 把resultList写入dataDir
workbook = ExcelWriter.exportData(resultList);
File dataFile = new File(fileDir);
fileOutputStream = new FileOutputStream(dataFile);
workbook.write(fileOutputStream);
fileOutputStream.flush();
fileOutputStream.close();
//5 上传至服务器
ftpClient.setFileType(ftpClient.BINARY_FILE_TYPE);
ftpClient.makeDirectory(pathname);
ftpClient.changeWorkingDirectory(pathname);
ftpClient.enterLocalPassiveMode();
inputStream = new FileInputStream(new File(fileDir));
ftpClient.storeFile(fileName, inputStream);
inputStream.close();
ftpClient.logout();
flag = true;
} catch(Exception e){
e.printStackTrace();
flag = false;
} finally {
if (null != fileOutputStream) {
fileOutputStream.close();
}
if (null != inputStream) {
inputStream.close();
}
if (null != workbook) {
workbook.close();
}
}
return flag;
}
/**
* 下载方法
* (markdown里纯手打,未验证,当伪代码看)
*/
public String downLoadFile(String remotePath){
// 下载文件到临时文件夹里面的随机文件里
String localPath = tempDir +Math.random() +"XXX.xlsx";
try{
File localFile=new File(localPath);
OutputStream ous=null;
ous=new FileOutputStream(localFile);
ftpClient.retrieveFile(remotePath, ous);
} catch (Exception e){
e.printStackTrace();
} finally {
if (null != ous) {
ous.close();
}
}
return localPath;
}
/**
* 找出文件服务器上所有符合要求的文件
* (markdown里纯手打,未验证,当伪代码看)
*/
public List<String> findFile(){
List<String> fileList = new ArrayList<>();
//1 通过使用ftpClient.listNames 得到所有的文件
//2 通过文件名里的时间和APN过滤后,得到附条件的文件名
return fileList;
}
执行方法
/**
* 运行方法
* (markdown里纯手打,未验证,当伪代码看)
*/
public void excute(){
//这次任务总共执行了多少数据
int excelNum = 0;
//1 得到符合条件的所有的文件集合
List<String> fileList = findFile();
//2 遍历
for(String filePath : fileList){
//3 得到临时文件夹复制过来的文件
String localPath = downLoadFile(filePath);
uploadFile(dataDir,Math.random() + "XXX.xslx",localPath,excelNum);
//4 把localPath文件移入历史文件夹
}
}
现在我们来分析下这个最重要的执行方法:
- 确实有一个for循环,而且循环体里执行的内容也很复杂和耗时间
- 结合业务分析,由于是省BOSS上传,那每天至少34份文件(每个文件里的数据不会少)
- 可能存在并发安全的问题点,目前显而易见的有两处:1 excelNum的可见性问题 2 Math.random()生成文件名,对应多线程下的重名问题
模拟下如果用多线程来写这部分代码
/**
* 运行方法,多线程版
* (markdown里纯手打,未验证,当伪代码看)
*/
public void excute(){
//!!!加一个volatile关键字,保证多线程下的可见性!!!
public volatile int excelNum = 0;
//1 得到符合条件的所有的文件集合
List<String> fileList = findFile();
//public ThreadPoolExecutor(int corePoolSize, corePoolSize:该线程池中核心线程数最大值
// int maximumPoolSize, maximumPoolSize: 该线程池中线程总数最大值
// long keepAliveTime,keepAliveTime:该线程池中非核心线程闲置超时时长
// TimeUnit unit,unit:keepAliveTime的单位
// BlockingQueue<Runnable> workQueue,workQueue:阻塞队列BlockingQueue,维护着等待执行的Runnable对象
// ThreadFactory threadFactory,threadFactory:创建线程的接口,需要实现他的Thread newThread(Runnable r)方法
// RejectedExecutionHandler handler)RejectedExecutionHandler:饱和策略,最大线程和工作队列容量且已经饱和时execute方法都将调用RejectedExecutionHandler
//创建线程池
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
10, 20, 10, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(1),
new ThreadPoolExecutor.DiscardPolicy());
//2 遍历
for(String filePath : fileList){
//使用线程池去并行执行任务
threadPool.excute(()->{
//3 得到临时文件夹复制过来的文件
String localPath = downLoadFile(filePath);
//多加了一个当前时间,在34份左右的文件下就不会存在同名问题
uploadFile(dataDir,Math.random()+ System.currentTimeMillis() + "XXX.xslx",localPath,excelNum);
//4 把localPath文件移入历史文件夹
})
}
//等线程池里所有任务完成后才会返回true
threadPool.shutdown();
while(true){
if(pool.isTerminated()){
break;
}
}
System.out.println("======= excelNum :" + excelNum);
}
最后总结:
在保证线程安全的前提下,当前这种每一次循环都会耗费大量时间的任务,还是很有必要使用多线程的。可以看出在代码结构基本不变的情况下,加的这部分多线程代码也不会阅读障碍,至于系统复杂度,目前看来并没有增加。