线程池处理数倍于线程池最大线程数和阻塞队列的方法
最近工作过程中有个场景需要在一次请求过程中下载多个文件并且打包上传,考虑了几种方案,下面介绍下考虑取舍以及实现的过程,仅本人拙见,还望各位大佬手下留情,多多交流!
场景复现
业务要求需要实现一个多文件下载的功能,这个功能要求实现下载某些工单的全部附件,打包并上传
第一次尝试 (线程池 + java 事件)
异步线程池实现
@Configuration
@EnableAsync
public class AsyncConfiguration implements AsyncConfigurer {
@Override
@Bean(name = "syncExecutor")
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10);
executor.setMaxPoolSize(20);
executor.setQueueCapacity(20);
executor.setThreadNamePrefix("sync-Executor-");
return new ExceptionHandlingAsyncTaskExecutor(executor);
}
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return new SimpleAsyncUncaughtExceptionHandler();
}
}
java 事件实现
java事件组成是由:事件源,事件,事件监听器三大块组成
事件 事件在定义时需要继承ApplicationEvent 并且父类方法,需要的参数是个对象,当需要传递的参数不止一个时采用对象包含这些属性
@ToString
@EqualsAndHashCode
public class Event extends ApplicationEvent implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 涉及的工单sheId(函件工单sheID)
*/
@JsonSerialize
private EventVo eventVo;
public Event(EventVo eventVo) {
super(eventVo);
this.eventVo = eventVo;
}
public EventVo getEventVo() {
return eventVo;
}
public void setEventVo(EventVo eventVo) {
this.eventVo = eventVo;
}
}
@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class EventVo implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 涉及的工单sheId(函件工单sheID)
*/
private String sheId;
@JsonSerialize
private List<SaasFilesn> files;
@JsonSerialize
private List<SaasFileBean> sFiles;
}
事件源
public void tesEvent(@RequestBody EventVo eventBean){
EventVo event = new EventVo(eventBean.getSheId(), eventBean.getFiles(), letterEventBean.getSFiles());
System.out.println("准备发送的事件内容为" + eEvent);
System.out.println("发送的线程为" + Thread.currentThread().getName());
Event event1 = new Event(event);
applicationEventPublisher.publishEvent(event1);
}
事件监听
@Component
public class EventListener implements ApplicationListener<Event> {
@SneakyThrows
@Async
@Override
public void onApplicationEvent(LetterEvent event) {
//todo ......
}
}
第二次尝试(线程池 + rabbitMQ + CountDownLatch)
CountDownLatch 与 CyclicBarrier的理解和区别
CountDownLatch 与 CyclicBarrier的理解和区别
简而言之,CountDownLatch是等其他线程全部执行完毕,主线程才开始执行;CyclicBarrier执行主体是其他线程,不管主线程是否已经结束,最终结束是由其他线程决定的
线程池:
线程池7大核心参数
我使用的 核心线程10 最大线程数20 30分钟存活, 阻塞队列50(之所以是50,依据业务决定)
@Configuration
public class ThreadPoolUtil {
public static final Logger log = LoggerFactory.getLogger(ThreadPoolUtil.class);
@Bean(name = "domCommonExecutor")
public ThreadPoolExecutor getCommonExecutor() {
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(10, 20, 30, TimeUnit.MINUTES, new ArrayBlockingQueue<>(50));
return threadPoolExecutor;
}}
rabbitMQ声明交换机、消息队列、消息路由
@Component
public class AConfigQueue {
/**
* 下载管理监听mq消息队列
*/
public static final String DOWNLOAD_QUEUE = "queue_download";
/**
* 建立下载管理消息队列
*/
@Bean
public Queue queueDownLoad(){
return new Queue(RabbitMQConstant.DOWNLOAD_QUEUE);
}
/**
*
*/
@Bean
public Binding bindingDownLoad(Queue queueDownLoad, @Qualifier("exchangeDownload") TopicExchange exchangeDownload) {
return BindingBuilder.bind(queueDownLoad).to(exchangeDownload).with("routing-key");
}
}
第三次尝试(线程池 + rabbitMQ + CountDownLatch + java集合切分 )
由于一次下载可能上千个文件,如果一个个往线程池里面仍是不现实的,当线程池中存活的线程达到最大线程数,并且阻塞队列中的个数已达最大数量,那么线程池将会执行拒绝策略,抛出rejectException, 所以采用类似于分页的格式使用一个线程阻塞执行多个任务,然后使用CountDownLatch对已经执行的子集合个数进行计数,当所有的线程均执行完毕,主线程继续执行下面的业务。
集合切分 引用 ([java集合子集切割](https://blog.csdn.net/Mclaughling/article/details/113248120))
try {
//将集合进行切割
List<List<SaasFileBean>> lists = averageAssignList(saasFileBeanList, 50);
//调用线程池
ThreadPoolExecutor commonExecutor = threadPoolUtil.getCommonExecutor();
//线程同步围栏
CountDownLatch countDownLatch = new CountDownLatch(lists.size());
for (List<SaasFileBean> list : lists) {
commonExecutor.submit(()-> {
dealWithSubList(list, filePath, type);
countDownLatch.countDown();
});
}
countDownLatch.await();
} catch (Exception e) {
logger.error("文件下载异常:{}", e);
throw new BusinessException("文件下载异常");
}
//todo ....
}
private File toZip(String filePath, String format, String type, String absolutePath) {
File zip = null;
try {
if (FileUtil.isWindows()) {
zip = ZipUtil.zip(filePath, absolutePath + type + "文件导出-" + format + ".zip");
} else {
//linux
zip = ZipUtil.zip(filePath, absolutePath + type + "文件导出-" + format + ".zip");
}
logger.info("=====文件压缩成功===============");
} catch (UtilException e) {
throw new BusinessException("文件压缩异常");
}
return zip;
}
文件上传
try {
//将压缩后的文件进行上传
fis = new FileInputStream(file);
bos = new ByteArrayOutputStream(fis.available());
byte[] b = new byte[1024];
int len = -1;
while ((len = fis.read(b)) != -1) {
bos.write(b, 0, len);
}
byte[] fileByte = bos.toByteArray();
fileDto = fileService.uploadFile(fileByte, data.getSheId(), "文件导出-" + format + ".zip");
logger.info("===============文件上传成功==================");
} catch (IOException e) {
throw new BusinessException("压缩文件上传失败");
} finally {
}
第四次实现(线程池(阻塞队列扩充到集合切割份数)+ CountDownLatch+ rabbitMQ + java集合切分 )
考虑到业务将切割集合的份数定为了50,与阻塞队列的最大个数一致!
后面又提了新需求,将下载的文件进行打包,由于里面含有Excel报表,要求根据excel报表的字段path进行超链接的跳转
这里不赘述,代码如下
public static void setSpecialCell(Row row, int start, List<Object> values, CellStyle cellStyle, SXSSFWorkbook workBook, String format1) {
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
for (int i = start; i < values.size() + start; i++) {
if(i < values.size() + start - 1) {
row.createCell(i).setCellValue(getInfo(values.get(i - start), format));
row.getCell(i).setCellStyle(cellStyle);
} else {
Cell cell = row.createCell(values.size() - 1);
try {
CreationHelper creationHelper = workBook.getCreationHelper();
Hyperlink hyperlink = creationHelper.createHyperlink(HyperlinkType.FILE);
hyperlink.setAddress("/Downloads/文件导出-" + format1 + "/" + getInfo(values.get(values.size() - 1), format));
cell.setHyperlink(hyperlink);
} catch (Exception e) {
log.error("文件格式错误:" + getInfo(values.get(values.size() - 1), format), e);
}
cell.setCellValue(getInfo(values.get(values.size() - 1), format));
row.getCell(values.size() - 1).setCellStyle(cellStyle);
}
}
}
核心:
**
CreationHelper creationHelper = workBook.getCreationHelper();
Hyperlink hyperlink = creationHelper.createHyperlink(HyperlinkType.FILE);
hyperlink.setAddress("/Downloads/文件导出-" + format1 + "/" + getInfo(values.get(values.size() - 1), format));
cell.setHyperlink(hyperlink);
**