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

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待执行,陷入无限等待。

修复步骤

  1. 修正isMoveDoable方法,基于当前工作对象状态判断
    将isMoveDoable改为通过ScoreDirector获取当前线程中slot的实际team值,避免依赖构造时的缓存值:

    @Override
    public boolean isMoveDoable(ScoreDirector<ShiftsSolution> scoreDirector) {
        HalfHourTimeslot workingSlot = scoreDirector.lookUpWorkingObject(slot);
        return !toTeam.equals(workingSlot.getTeam());
    }
    

    这样能确保在多线程环境中,每个线程都会基于自己持有的Solution克隆体来判断Move是否可执行,避免缓存值失效导致的误判。

  2. 禁用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)))));
      
  3. 验证终止条件触发逻辑
    修正后,当Move列表为空或无可执行Move时,Optaplanner的多线程求解逻辑会正确识别到无可用Move,触发与单线程模式一致的终止行为,输出类似No doable selected move at step index ... Terminating phase early.的日志并结束求解。

内容的提问来源于stack exchange,提问作者Michał Dudkiewicz

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.19 06:40:23