在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); }
请问这种修改后的写法是否同样是线程安全的正确方式?
解答
核心结论:两种写法在当前场景下都是线程安全的,但要注意适用条件
线程安全的关键原因:
- 每个
doIt方法调用都传入全新的CalculationDto实例,没有多线程共享同一个DTO对象的情况——即使CompletableFuture的各个阶段在不同线程执行,每个线程操作的都是独立的DTO实例,不存在并发修改同一对象的风险。 - CompletableFuture的
thenApply是串行执行的:supplyAsync执行完成后,才会依次触发后续的thenApply回调,不会出现多个回调同时修改同一个DTO的情况,日志顺序固定也印证了这一点。
- 每个
两种写法的差异与合理性:
- 第一种写法:直接修改原始传入的
calculationDto对象,虽然回调参数calculationDto1/calculationDto2没被直接用来修改,但本质上操作的是同一个对象(因为返回的都是原始DTO),由于串行执行+实例独立,所以安全。 - 第二种写法:使用回调参数
calculationDto1/calculationDto2来修改对象,和第一种逻辑完全等价——因为这些参数就是原始DTO对象的引用,修改它们和修改原始calculationDto是一回事,同样满足线程安全条件。
- 第一种写法:直接修改原始传入的
注意事项
如果业务场景发生变化,比如:
- 多个CompletableFuture分支共享同一个DTO实例;
- 使用
thenApplyAsync(异步执行回调,可能和其他阶段并发); - 同一DTO被多个请求复用;
此时就会出现线程安全问题,需要考虑: - 改用不可变DTO(每次计算返回新实例);
- 对DTO的修改操作加锁;
- 使用线程安全的容器/字段。
内容的提问来源于stack exchange,提问作者Thomas Lang
相关产品推荐
相关产品推荐

