Optaplanner自定义Move与多线程问题:Move耗尽后线程永久阻塞
实现简单的时间段(HalfHourTimeslot,规划实体)到团队(Team,规划变量)的分配任务:
- 单线程求解时一切正常;
- 未使用自定义
FarSlotChangeMove时,多线程求解正常; - 使用该自定义Move后,当自定义Move列表耗尽,所有求解线程陷入WAIT状态永久阻塞,无任何报错信息。所有自定义Move均在每个STEP前生成,全断言检查无异常,疑似死锁。
当前环境:Optaplanner 8.25.0.Final、Java 17。
出现以下日志后,线程进入永久阻塞:
2022-09-14 20:13:28,568 [main] TRACE Created cachedMoveList: size (0), moveSelector (MoveListFactory(class pl.medaxtrans.pre.solver.FarSlotChangeMoveFactory)).
单线程模式下会正常终止,日志如下:
No doable selected move at step index ... Terminating phase early.
需要实现多线程模式下同样的终止行为。
相关代码
Move工厂代码
package pl.medaxtrans.pre.solver; import org.optaplanner.core.impl.heuristic.move.Move; import org.optaplanner.core.impl.heuristic.selector.move.factory.MoveListFactory; import pl.medaxtrans.common.domain.Team; import pl.medaxtrans.pre.domain.HalfHourTimeslot; import pl.medaxtrans.pre.domain.ShiftsSolution; import java.time.DayOfWeek; import java.util.ArrayList; import java.util.Comparator; import java.util.List; import java.util.Optional; import java.util.function.Predicate; import java.util.stream.Collectors; public class FarSlotChangeMoveFactory implements MoveListFactory<ShiftsSolution> { private static Predicate<HalfHourTimeslot> equalTime(HalfHourTimeslot slot2) { return slot -> slot.compareTo(slot2) == 0; } private static Predicate<HalfHourTimeslot> sameDayAndTeam(DayOfWeek day, Team team) { return slot -> slot.getDayOfWeek().equals(day) && slot.getTeam().equals(team); } @Override public List<? extends Move<ShiftsSolution>> createMoveList(ShiftsSolution solution) { List<FarSlotChangeMove> moves = new ArrayList<>(); List<HalfHourTimeslot> slots = solution.getSlots(); List<Team> teams = solution.getTeams(); for (Team fromTeam : teams) { for (Team toTeam : teams) { if (fromTeam != toTeam) { for (DayOfWeek day : DayOfWeek.values()) { List<HalfHourTimeslot> fromTeamSlots = slots.stream().filter(sameDayAndTeam(day, fromTeam)).collect(Collectors.toList()); List<HalfHourTimeslot> toTeamSlots = slots.stream().filter(sameDayAndTeam(day, toTeam)).collect(Collectors.toList()); Optional<HalfHourTimeslot> fromMinOpt = fromTeamSlots.stream().min(Comparator.naturalOrder()); Optional<HalfHourTimeslot> fromMaxOpt = fromTeamSlots.stream().max(Comparator.naturalOrder()); Optional<HalfHourTimeslot> toMaxOpt = toTeamSlots.stream().max(Comparator.naturalOrder()); Optional<HalfHourTimeslot> toMinOpt = toTeamSlots.stream().min(Comparator.naturalOrder()); if (fromMinOpt.isPresent() && fromMaxOpt.isPresent() && toMaxOpt.isPresent() && toMinOpt.isPresent()) { HalfHourTimeslot fromMin = fromMinOpt.get(); HalfHourTimeslot fromMax = fromMaxOpt.get(); HalfHourTimeslot toMax = toMaxOpt.get(); HalfHourTimeslot toMin = toMinOpt.get(); if (toMin.compareTo(fromMin) < 0 && toMax.compareTo(fromMin) > 0 && toTeamSlots.stream().noneMatch(equalTime(fromMin))) { moves.add(new FarSlotChangeMove(fromMin, toTeam)); } if (toMin.compareTo(fromMax) < 0 && toMax.compareTo(fromMax) > 0 && toTeamSlots.stream().noneMatch(equalTime(fromMax))) { moves.add(new FarSlotChangeMove(fromMax, toTeam)); } } } } } } return moves; } }
自定义Move代码
package pl.medaxtrans.pre.solver; import org.optaplanner.core.api.score.director.ScoreDirector; import org.optaplanner.core.impl.heuristic.move.AbstractMove; import pl.medaxtrans.common.domain.Team; import pl.medaxtrans.pre.domain.HalfHourTimeslot; import pl.medaxtrans.pre.domain.ShiftsSolution; import java.util.Collection; import java.util.Collections; import java.util.Objects; public class FarSlotChangeMove extends AbstractMove<ShiftsSolution> { private final HalfHourTimeslot slot; private final Team toTeam; private final Team fromTeam; public FarSlotChangeMove(HalfHourTimeslot slot, Team toTeam) { this.slot = slot; this.toTeam = toTeam; this.fromTeam = slot.getTeam(); } @Override protected AbstractMove<ShiftsSolution> createUndoMove(ScoreDirector<ShiftsSolution> scoreDirector) { return new FarSlotChangeMove(slot, fromTeam); } @Override protected void doMoveOnGenuineVariables(ScoreDirector<ShiftsSolution> scoreDirector) { scoreDirector.beforeVariableChanged(slot, "team"); slot.setTeam(toTeam); scoreDirector.afterVariableChanged(slot, "team"); } @Override public boolean isMoveDoable(ScoreDirector<ShiftsSolution> scoreDirector) { // we don't check overlaps here, because it is already checked by the move factory return !toTeam.equals(fromTeam); } @Override public FarSlotChangeMove rebase(ScoreDirector<ShiftsSolution> destinationScoreDirector) { return new FarSlotChangeMove(destinationScoreDirector.lookUpWorkingObject(slot), destinationScoreDirector.lookUpWorkingObject(toTeam)); } @Override public Collection<?> getPlanningEntities() { return Collections.singletonList(slot); } @Override public Collection<?> getPlanningValues() { return Collections.singletonList(toTeam); } @Override public String toString() { return slot + " {" + fromTeam + "->" + toTeam + "}"; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } final FarSlotChangeMove other = (FarSlotChangeMove) o; return Objects.equals(fromTeam, other.fromTeam) && Objects.equals(toTeam, other.toTeam) && Objects.equals(slot, other.slot); } @Override public int hashCode() { return Objects.hash(slot, toTeam, fromTeam); } }
核心问题
多线程模式下,每个工作线程持有独立的Solution克隆体,而FarSlotChangeMove构造时缓存的fromTeam是基于原Solution的状态,后续工作线程中slot的team可能被修改,导致isMoveDoable方法判断失效;同时,缓存的空Move列表会让线程误以为还有Move待执行,陷入无限等待。
修复步骤
修正
isMoveDoable方法,基于当前工作对象状态判断
将isMoveDoable改为通过ScoreDirector获取当前线程中slot的实际team值,避免依赖构造时的缓存值:@Override public boolean isMoveDoable(ScoreDirector<ShiftsSolution> scoreDirector) { HalfHourTimeslot workingSlot = scoreDirector.lookUpWorkingObject(slot); return !toTeam.equals(workingSlot.getTeam()); }这样能确保在多线程环境中,每个线程都会基于自己持有的Solution克隆体来判断Move是否可执行,避免缓存值失效导致的误判。
禁用Move选择器的缓存
配置中关闭Move列表的缓存,确保每次生成Move列表都基于最新的Solution状态,避免空列表被缓存导致线程阻塞:- XML配置示例:
<moveListFactory> <moveListFactoryClass>pl.medaxtrans.pre.solver.FarSlotChangeMoveFactory</moveListFactoryClass> <cached>false</cached> </moveListFactory> - Java代码配置示例:
SolverFactory<ShiftsSolution> solverFactory = SolverFactory.create(new SolverConfig() // 其他配置... .withPhaseConfigList(List.of(new LocalSearchPhaseConfig() .withMoveSelectorConfig(new MoveListFactoryConfig(FarSlotChangeMoveFactory.class) .withCached(false)))));
- XML配置示例:
验证终止条件触发逻辑
修正后,当Move列表为空或无可执行Move时,Optaplanner的多线程求解逻辑会正确识别到无可用Move,触发与单线程模式一致的终止行为,输出类似No doable selected move at step index ... Terminating phase early.的日志并结束求解。
内容的提问来源于stack exchange,提问作者Michał Dudkiewicz

