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

多线程下OptaPlanner中Map类型影子变量的竞态问题问询

OptaPlanner多线程场景下Map类型影子变量的支持问题

场景背景

在云负载均衡问题变体中,Process类使用Map存储正权重:

Map<Long, Long> groupMap = new HashMap<>();

其中键为领域特定标识,值为权重。Computer类(参考云负载均衡示例)包含一个同类型的影子变量hist,通过自定义VariableListener更新:

public class HistListener implements VariableListener {
    @Override
    public void beforeVariableChanged(ScoreDirector scoreDirector, Object o) {
        Process p = (Process) o;
        if (p.getComputer() != null) { 
            Computer kc = p.getComputer();
            // 更新hist Map
            scoreDirector.beforeVariableChanged(kc, "hist");
            for (Map.Entry<Long, Long> entrySet : p.getGroupMap().entrySet()){
                kc.getHist().put(entrySet.getKey(), kc.getHist().get(entrySet.getKey()) - entrySet.getValue());
            }
            scoreDirector.afterVariableChanged(kc, "hist");
        }
    }
}

afterVariableChanged方法逻辑类似,仅数值增减符号相反。Process和Computer均标注@PlanningEntity并在solverConfig中注册,无约束时求解器可任意分配computer给process,预期hist值为自然数(含0)。

单线程与多线程结果差异

  • 单线程模式(<moveThreadCount>NONE</moveThreadCount>)结果符合预期:
<"Computer"+computer.id: hist>

Computer0: {0=0, 1=0, 2=20, 3=0, 4=10, 5=20, 6=0, 7=10, 8=10, 9=20}
Computer1: {0=0, 1=10, 2=0, 3=0, 4=10, 5=0, 6=10, 7=0, 8=0, 9=0}
Computer2: {0=0, 1=0, 2=0, 3=0, 4=0, 5=0, 6=0, 7=0, 8=0, 9=0}
  • 多线程模式(<moveThreadCount>AUTO</moveThreadCount>)结果异常,hist出现负数:
Computer0: {0=0, 1=-20, 2=30, 3=0, 4=-40, 5=50, 6=-10, 7=30, 8=40, 9=150}
Computer1: {0=0, 1=-40, 2=-20, 3=0, 4=-90, 5=-50, 6=-40, 7=-20, 8=-20, 9=-30}
Computer2: {0=0, 1=80, 2=-20, 3=0, 4=30, 5=-30, 6=50, 7=0, 8=-20, 9=-50} 

问题解答

核心结论

OptaPlanner 8.29.0.Final的多线程场景下,不直接支持非线程安全的Map(如HashMap)作为影子变量,除非对Map的访问和更新做严格的线程安全处理,否则会因竞态条件导致计算结果异常。

原因分析

  1. 复合操作的非原子性:即使未修改Map结构,get()取数、加减计算、put()存数是复合操作,非线程安全的Map无法保证这些操作的原子性,多线程并发执行时会出现数值覆盖、计算错误。
  2. ScoreDirector的契约限制:ScoreDirector.beforeVariableChanged()和afterVariableChanged()仅用于追踪变量变更,不保证底层数据结构的线程安全,无法解决Map并发修改的问题。

可行解决方案

  1. 使用线程安全Map并保证原子操作:将HashMap替换为ConcurrentHashMap,并通过原子方法处理数值更新,例如:
kc.getHist().compute(entrySet.getKey(), (key, currentValue) -> 
    currentValue == null ? -entrySet.getValue() : currentValue - entrySet.getValue());
  1. 重构为独立影子变量:将Map的每个键对应值拆分为独立的影子变量,这是OptaPlanner推荐的方式,可天然避免集合带来的线程安全问题。
  2. 避免复杂集合作为影子变量:若业务场景允许,优先使用基础类型或简单结构作为影子变量,降低并发风险。

文档建议

若OptaPlanner官方不推荐多线程场景下使用非线程安全集合作为影子变量,建议在8.29.0.Final及后续版本文档中补充说明:

  • 多线程求解时,影子变量优先使用线程安全数据结构或独立变量,而非非线程安全集合。
  • 若必须使用集合类型,需自行处理并发修改的线程安全问题,否则可能导致结果异常。

内容的提问来源于stack exchange,提问作者Philip Harding

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.05 14:30:33