Java并发编程

119 阅读4分钟

线程并发的顺序问题

/**
 * 绘本分镜文本生成图片
 * 逻辑:
 * 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