当我接手设计一个功能,到底需不需要考虑用到多线程?

当我接手设计一个功能,到底需不需要考虑用到多线程?

个人分析,从我实际经历的项目出手。

需求背景:

各级省BOSS系统(这里简称BOSS-ALL),会把他们的某些数据以excel的形式上传到固定服务器的固定路径,excel的后缀为 ***20210722_APN.xlsx(不同地域省BOSS对应不同文件) ,而我这里的CRM系统(简称CRM)需要:

  1. 把当天的这些后缀的excel复制到当前服务器的一个临时路径
  2. 按照不同地域的BOSS文件分别读取数据
  3. 读取数据后与本系统表里的一些数据整合成新数据
  4. 将新数据在一个名为info_CRM的文件夹 写成excel
  5. 把临时路径已经处理好的文件移动到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);
}

最后总结:

在保证线程安全的前提下,当前这种每一次循环都会耗费大量时间的任务,还是很有必要使用多线程的。可以看出在代码结构基本不变的情况下,加的这部分多线程代码也不会阅读障碍,至于系统复杂度,目前看来并没有增加。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值