TimeFold变量监听器:满足条件时如何基于调整后结束时间更新开始时间?求解失败
问题解决思路:OptaPlanner ShadowVariable 并发异常与任务时间重叠修复
一、解决ConcurrentModificationException异常
问题根源
在TaskChangeListener的skipBreakTime方法中直接修改adjustedEndTime这类规划实体字段,会触发ShadowVariable的递归更新逻辑,而OptaPlanner求解过程中不允许在事件监听器内直接修改实体状态——这会破坏求解器内部的集合遍历/更新一致性,进而抛出并发修改异常。
修复方案
- 将休息时间避让逻辑移至ShadowVariable计算中
放弃在监听器中修改实体字段,把休息时间的判断与时间调整逻辑整合到ShadowVariable的监听器方法内,让OptaPlanner统一管理变量更新流程。 - 严格通过ScoreDirector管理变量变更(若必须用监听器)
若业务场景需要保留监听器,所有实体字段修改必须用ScoreDirector的方法包裹,确保求解器能跟踪变更:
但更推荐优先采用第一种方案,从根源避免并发冲突。// 在监听器中修改adjustedEndTime的正确方式 scoreDirector.beforeVariableChanged(task, "adjustedEndTime"); task.setAdjustedEndTime(newEndTime); scoreDirector.afterVariableChanged(task, "adjustedEndTime");
二、修复后续任务时间重叠问题
问题根源
仅依赖PiggybackShadowVariable可能无法完整传递结束时间的变更到任务链的所有后续节点,导致休息时间避让后的结束时间未同步更新到下游任务的开始时间配置。
修复方案
- 构建完整的ShadowVariable依赖链
明确任务间的时间变量依赖关系,让后续任务的startTime直接依赖前序任务的adjustedEndTime,确保变更能自动递推:@PlanningEntity public class Task { @PlanningVariable(valueRangeProviderRefs = "taskRange") private Task predecessor; // 原始开始时间(非规划变量) private LocalDateTime originalStartTime; // 实际开始时间:依赖前序任务的adjustedEndTime @ShadowVariable(sourceVariableName = "predecessor", variableListenerClass = TaskStartTimeListener.class) private LocalDateTime startTime; // 调整后结束时间:依赖当前任务的startTime,包含休息时间避让 @ShadowVariable(sourceVariableName = "startTime", variableListenerClass = TaskAdjustedEndTimeListener.class) private LocalDateTime adjustedEndTime; } - 在ShadowVariable监听器中实现完整的时间递推逻辑
- 调整结束时间的监听器:计算
adjustedEndTime时直接跳过休息时间public class TaskAdjustedEndTimeListener implements VariableListener<Task> { @Override public void afterVariableChanged(ScoreDirector scoreDirector, Task task) { LocalDateTime currentEnd = task.getStartTime().plus(task.getDuration()); // 执行休息时间避让逻辑 currentEnd = skipBreakTime(currentEnd, task.getStartTime()); // 通过ScoreDirector更新变量 scoreDirector.beforeVariableChanged(task, "adjustedEndTime"); task.setAdjustedEndTime(currentEnd); scoreDirector.afterVariableChanged(task, "adjustedEndTime"); } private LocalDateTime skipBreakTime(LocalDateTime endTime, LocalDateTime startTime) { LocalDateTime breakStart = LocalDateTime.of(startTime.toLocalDate(), LocalTime.of(11, 30)); LocalDateTime breakEnd = LocalDateTime.of(startTime.toLocalDate(), LocalTime.of(12, 30)); // 判断是否与休息时间重叠,计算需要追加的时长 if (startTime.isBefore(breakEnd) && endTime.isAfter(breakStart)) { Duration overlap = Duration.between( startTime.isBefore(breakStart) ? breakStart : startTime, endTime.isAfter(breakEnd) ? breakEnd : endTime ); return endTime.plus(overlap); } return endTime; } } - 更新后续任务开始时间的监听器:确保前序任务
adjustedEndTime变更后,下游任务的startTime同步更新public class TaskStartTimeListener implements VariableListener<Task> { @Override public void afterVariableChanged(ScoreDirector scoreDirector, Task task) { // 遍历当前任务的所有后续任务,更新其startTime for (Task successor : task.getSuccessors()) { scoreDirector.beforeVariableChanged(successor, "startTime"); successor.setStartTime(task.getAdjustedEndTime()); scoreDirector.afterVariableChanged(successor, "startTime"); // 触发后续任务的endTime更新,确保递推到整个任务链 scoreDirector.triggerVariableListeners(successor, "startTime"); } } }
- 调整结束时间的监听器:计算
- 禁止直接修改ShadowVariable字段
所有ShadowVariable的更新必须通过ScoreDirector的beforeVariableChanged和afterVariableChanged方法包裹,确保OptaPlanner能正确跟踪变量变更,避免任务链更新遗漏。
三、关键注意事项
- 不要在规划实体的setter方法中嵌入业务逻辑(如休息时间避让),所有计算逻辑应放在ShadowVariable监听器内,确保变更流程被OptaPlanner统一管理。
- 当调整
adjustedEndTime后,必须调用scoreDirector.triggerVariableListeners()触发后续任务的变量更新,确保时间变更递推到整个任务链。 - 事件监听器仅用于监听状态变更,不要在其中修改实体字段,否则极易触发并发修改异常。
内容的提问来源于stack exchange,提问作者Vy Le
相关产品推荐
相关产品推荐

