You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

在CompletableFuture链中线程安全修改DTO的方式是否正确?

问题背景与疑问

我编写了如下测试用例(当前测试结果为通过),用于模拟DTO的链式计算流程——实际业务中该DTO来自MVC Controller的POST请求,经多任务处理后返回给客户端:

@Slf4j
public class CalculationTests {

    @Test
    void testParallelComputation() {
        for (int i = 0; i < 1000; i++) {
            doIt(new CalculationDto(1, 2, 0));
        }
    }

    void doIt(CalculationDto calculationDto) {
        CompletableFuture<CalculationDto> calculationDtoCompletableFuture = CompletableFuture
                .supplyAsync(() -> {
                    calculationDto.setResult(calculationDto.a + calculationDto.b); // a = 1, b = 2 -> result = 3
                    return calculationDto;
                })
                .thenApply(calculationDto1 -> {
                    calculationDto.setA(2); // a = 2, result = 3
                    calculationDto.setB(calculationDto1.getResult()); // b = 3
                    log.error("res 1");
                    return calculationDto;
                }).thenApply(calculationDto2 -> {
                    calculationDto.setResult(calculationDto2.a + calculationDto2.b); // result = 2 + 3 = 5
                    log.error("res 2");
                    return calculationDto;
                });

        assertThat(calculationDtoCompletableFuture.join().getResult()).isEqualTo(5);
    }

    @Getter
    @Setter
    @AllArgsConstructor
    static class CalculationDto {
        private Integer a;
        private Integer b;
        private Integer result;

    }
}

我的问题是:在CompletableFuture链中采用上述方式修改DTO是否属于线程安全的正确做法?


补充说明

我将测试代码修改为如下形式后,测试结果仍为通过,且日志输出顺序固定为res1、res2、res3:

void doIt(CalculationDto calculationDto) {
        CompletableFuture<CalculationDto> calculationDtoCompletableFuture = CompletableFuture
                .supplyAsync(() -> {
                    calculationDto.setResult(calculationDto.a + calculationDto.b); // a = 1, b = 2 -> result = 3
                    log.error("res 1");
                    return calculationDto;
                })
                .thenApply(calculationDto1 -> {
                    calculationDto1.setA(2); // a = 2, result = 3
                    calculationDto1.setB(calculationDto1.getResult()); // b = 3
                    log.error("res 2");
                    return calculationDto1;
                }).thenApply(calculationDto2 -> {
                    calculationDto2.setResult(calculationDto2.a + calculationDto2.b); // result = 2 + 3 = 5
                    log.error("res 3");
                    return calculationDto2;
                });

        assertThat(calculationDtoCompletableFuture.join().getResult()).isEqualTo(5);
    }

请问这种修改后的写法是否同样是线程安全的正确方式?


解答

核心结论:两种写法在当前场景下都是线程安全的,但要注意适用条件

  • 线程安全的关键原因:

    1. 每个doIt方法调用都传入全新的CalculationDto实例,没有多线程共享同一个DTO对象的情况——即使CompletableFuture的各个阶段在不同线程执行,每个线程操作的都是独立的DTO实例,不存在并发修改同一对象的风险。
    2. CompletableFuture的thenApply是串行执行的:supplyAsync执行完成后,才会依次触发后续的thenApply回调,不会出现多个回调同时修改同一个DTO的情况,日志顺序固定也印证了这一点。
  • 两种写法的差异与合理性:

    1. 第一种写法:直接修改原始传入的calculationDto对象,虽然回调参数calculationDto1/calculationDto2没被直接用来修改,但本质上操作的是同一个对象(因为返回的都是原始DTO),由于串行执行+实例独立,所以安全。
    2. 第二种写法:使用回调参数calculationDto1/calculationDto2来修改对象,和第一种逻辑完全等价——因为这些参数就是原始DTO对象的引用,修改它们和修改原始calculationDto是一回事,同样满足线程安全条件。

注意事项

如果业务场景发生变化,比如:

  • 多个CompletableFuture分支共享同一个DTO实例;
  • 使用thenApplyAsync(异步执行回调,可能和其他阶段并发);
  • 同一DTO被多个请求复用;
    此时就会出现线程安全问题,需要考虑:
  • 改用不可变DTO(每次计算返回新实例);
  • 对DTO的修改操作加锁;
  • 使用线程安全的容器/字段。

内容的提问来源于stack exchange,提问作者Thomas Lang

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.03 06:32:35