OptaPlanner:SolverManager::solveAndListen是否存在同步/锁Bug?
问题背景
TL;DR:默认实现DefaultSolverListener未对finalBestSolutionConsumer做正确同步,或者我误解了这个Consumer的用途?这是预期行为吗?
近期测试日志出现警告,显示最后当前最优解与最终最优解不匹配。原因是调用terminateEarly后,方法直接返回,并未等待finalBestSolutionConsumer线程执行完毕。虽然可通过中间解获取最后当前最优解,但最终最优解未及时保存的情况不符合预期。
代码场景
启动优化的调用代码:
this.getSolverManager() .solveAndListen(solutionID, this::findCurrentBest, this::saveCurrentBest, this::saveFinalBest, this::handleException);
saveFinalBest实现(非耗时操作):
this.finalBestConsumer.accept(finalBest); // 默认无操作 this.finalBestSolutionsMap.put(finalBest.getId(), finalBest);
后续执行逻辑:
// 日志记录 this.getSolverManager().terminateEarly(solutionID); // 日志记录 final var finalBest = this.findFinalBest(solutionID); final var crrntBest = this.findCurrentBest(solutionID); final var best = ObjectUtils.max(finalBest, crrntBest); if (!Objects.equals(finalBest, crrntBest)) { // 抛出警告 } return best;
发现terminateEarly返回后,saveFinalBest尚未执行完成,就已经调用findFinalBest读取数据,导致结果不一致。
原因分析
OptaPlanner的terminateEarly方法仅负责发送求解终止信号,不会阻塞等待finalBestSolutionConsumer的执行完成。DefaultSolverListener中处理最终最优解的逻辑属于求解线程的收尾流程,是异步执行的,因此terminateEarly返回时,回调可能还未执行完毕。
解决方案
方案1:手动同步等待回调完成
通过同步工具(如CountDownLatch)标记saveFinalBest的执行状态,在terminateEarly后等待回调完成再读取数据:
// 用线程安全的Map存储每个solutionID对应的等待锁 private final Map<Long, CountDownLatch> terminationWaiters = new ConcurrentHashMap<>(); // 启动求解时初始化锁 CountDownLatch waitLatch = new CountDownLatch(1); terminationWaiters.put(solutionID, waitLatch); this.getSolverManager() .solveAndListen(solutionID, this::findCurrentBest, this::saveCurrentBest, finalBest -> { this.finalBestConsumer.accept(finalBest); this.finalBestSolutionsMap.put(finalBest.getId(), finalBest); // 标记最终解保存完成 waitLatch.countDown(); }, this::handleException); // 触发终止 this.getSolverManager().terminateEarly(solutionID); // 等待最终解保存完成 try { waitLatch.await(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); // 处理中断异常 } terminationWaiters.remove(solutionID); // 后续读取逻辑 final var finalBest = this.findFinalBest(solutionID); final var crrntBest = this.findCurrentBest(solutionID); // ... 其余逻辑
方案2:在回调内触发后续逻辑
如果后续逻辑依赖最终最优解,直接将这部分逻辑放在saveFinalBest中执行,避免跨线程的同步问题:
this.getSolverManager() .solveAndListen(solutionID, this::findCurrentBest, this::saveCurrentBest, finalBest -> { this.finalBestConsumer.accept(finalBest); this.finalBestSolutionsMap.put(finalBest.getId(), finalBest); // 在这里执行原本需要依赖finalBest的逻辑 processFinalBestSolution(finalBest); }, this::handleException);
注意事项
确保finalBestSolutionsMap是线程安全的(比如使用ConcurrentHashMap),避免多线程读写时出现数据不一致问题。
结论:这是预期行为吗?
这不属于Bug,是terminateEarly的设计行为——该方法仅负责触发求解终止,不负责等待所有回调执行完毕。finalBestSolutionConsumer的执行属于求解线程的收尾工作,若需要同步等待回调完成,需自行实现同步机制。
内容的提问来源于stack exchange,提问作者Ryan Moser

