线程并发的顺序问题
/**
* 绘本分镜文本生成图片
* 逻辑:
* 1. 新建生图任务
* 2. 构建sd生图参数
* 3. 调用生图接口
* 4. 将生图接口返回值存储到task中(包括生图参数)
*
* ps:任务1执行完成就直接返回,不需要等待后续任务的执行结果
*
* @param scriptPartPictureRes
* @return
*/
// todo:考虑线程池中的事务
public Long generateScriptPartPic(ScriptPartPictureRes scriptPartPictureRes){
Long partId = scriptPartPictureRes.getPartId();
CompletableFuture<Long> task1Future = CompletableFuture.supplyAsync(() -> {
AigcWebuiTaskDO aigcWebuiTaskDO = buildTask(null, scriptPartPictureRes);
aigcWebuiTaskDAO.insert(aigcWebuiTaskDO);
Long taskId = aigcWebuiTaskDO.getId();
scriptPartPictureRes.setTaskId(taskId);
return taskId;
}, voiceGenExecutor);
task1Future.thenApplyAsync(taskId -> {
String partText = scriptPartPictureRes.getPartText();
return createPromptOfSd(partText);
}).thenApplyAsync(promptOfGPT -> {
AigcWebuiTaskDO taskDO = buildTask(promptOfGPT, scriptPartPictureRes);
aigcWebuiTaskDAO.updateByPrimaryKey(taskDO);
return taskDO;
}).thenApplyAsync(taskDO -> {
// 自动生图服务
return generatePicService.generatePicture(taskDO, partId);
}).thenApplyAsync(optionalTask -> {
WebuiTaskDTO webuiTaskDTO = optionalTask.get();
AigcWebuiTaskDO webuiTaskDO = webuiTaskDTO.getAigcWebuiTaskDO();
Long taskId = webuiTaskDO.getId();
String pictureUrl = webuiTaskDTO.getPictureUrl();
if (StringUtils.isBlank(pictureUrl)) {
webuiTaskDO.setGmtModified(new Date());
log.info(String.format("生图失败,taskId = %d", taskId));
} else {
webuiTaskDO.setGmtModified(new Date());
webuiTaskDO.setTaskStatus(GeneratePicTaskStageEnum.PROCESSED.getCode());
webuiTaskDO.setImgUrl(pictureUrl);
scriptPartPictureRes.setPictureUrl(pictureUrl);
scriptPartPictureRes.setTaskId(taskId);
log.info(String.format("生图成功,taskId = %d", taskId));
}
aigcWebuiTaskDAO.updateByPrimaryKeySelective(webuiTaskDO);
return webuiTaskDO.getId();
}).exceptionally(e -> {
log.info(e);
throw new BizException(ErrorCode.BIZ_AUTO_GEN_PIC_TASK_ERROR);
});
try {
// 等待第一个任务完成
Long taskId = task1Future.get();
// 在后台执行后续任务
CompletableFuture.runAsync(() -> {
task1Future.thenAccept(result -> {
// 执行后续任务的逻辑
});
});
return taskId;
} catch (Exception e) {
e.printStackTrace();
log.error(String.format("生图任务错误!e = %s", e), e);
throw new BizException(ErrorCode.BIZ_AUTO_GEN_PIC_TASK_ERROR);
}
}
上述代码解决多个线程执行顺序的问题。
如何实现多个线程执行顺序的编排?
有两种解决办法,一种是非线程池的方法,另一种是线程池的方法;这个话题就想到到了线程之间的通信问题。多个线程之间的通知有如下方法:
- 线程自身提供的方法,如wait().notify().notifyAll() etc
- CountDownLauch工具类
- Sample工具类
如果一个任务执行逻辑十分复杂,就要考虑将其拆分为多个任务,仍在线程池中处理。但是需要考虑一个问题,就是考虑子任务之间的以依赖关系和数据共享的问题。
如果任务B需要任务A的执行结果,可以使用如下处理办法?
1 使用线程池中的Feture和Callable:将任务A和B分别封装成Callable对象,在任务B执行之前可以通过任务A是Feture对象获取其执行结果。
public Long test() {
try {
Future<Long> futureA = GenPicExecutor.submit(new TaskA());
Long l = futureA.get();
Future<Long> futureB = GenPicExecutor.submit(new TaskB(l));
return futureB.get();
} catch (Exception e) {
}
return null;
}
class TaskA implements Callable<Long>{
@Override
public Long call() throws Exception {
return null;
}
}
class TaskB implements Callable<Long> {
private Long id;
public TaskB(Long id) {
this.id = id;
}
@Override
public Long call() throws Exception {
return id += 1;
}
}
2 使用CountDownLatch:创建一个CountDownLatch对象,任务B在执行之前调用CountDownLatch的await()方法进行等待,任务A执行完成后,调用CountDownLatch的countDown()方法进行通知,任务B被通知后再执行。
3 使用线程间的通信机制:可以使用wait()和notify()等方法进行线程之间的通信。任务B在执行之前,调用任务A的wait()方法进行等待,任务A执行完成后,调用任务A的notify()方法进行通知,任务B在被通知后再执行。这种方式需要在任务A中持有任务B的引用。
4 使用异步编程框架:可以使用Java 8引入的CompletableFuture来处理任务之间的依赖关系。任务B可以通过调用任务A的CompletableFuture对象的方法来获取其执行结果,同时可以通过CompletableFuture的方法链来串联多个任务,实现复杂的依赖关系。
选择哪种方法取决于具体的业务场景和需求。需要注意的是,在处理任务之间的依赖关系时,要确保避免死锁和线程安全的问题,需要合理地设计任务的执行顺序和数据的共享方式。
如何处理线程池中多个线程的数据共享问题?
一致性:
可见性:volenti
如何保证线程池中的事务处理?
Callable和Runable的区别?
class LearnTask implements Runnable{
@Override
public void run() {
System.out.println("open computer...");
learning();
System.out.println("learn finished");
}
private void learning() {
System.out.println("learning");
}
}
class LoveTask implements Callable<String> {
@Override
public String call() throws Exception {
return doing();
}
private String doing() {
String baby = new String("baby");
return baby;
}
}
Runable是一个无返回值的接口,Callabel是一个有返回值的接口;一个类实现了这两个接口,就变成了一个线程。在一个线程池中,可以将其作为一个任务提交到线程池中运行。
如何创建一个线程池?
private static ThreadPoolExecutor GenPicExecutor = new ThreadPoolExecutor(10, 10, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<>(2000));
param1:核心线程数
param2:最大线程数
param3:线程存活时间
param4:线程存活时间单位
param5:存储线程的数据结构
文件数据处理 (后续写)
1 文件下载到本地
2 文件打成zip包
设计模型 (后续写)
1 状态者模式
2