多线程下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的访问和更新做严格的线程安全处理,否则会因竞态条件导致计算结果异常。
原因分析
- 复合操作的非原子性:即使未修改Map结构,
get()取数、加减计算、put()存数是复合操作,非线程安全的Map无法保证这些操作的原子性,多线程并发执行时会出现数值覆盖、计算错误。 - ScoreDirector的契约限制:
ScoreDirector.beforeVariableChanged()和afterVariableChanged()仅用于追踪变量变更,不保证底层数据结构的线程安全,无法解决Map并发修改的问题。
可行解决方案
- 使用线程安全Map并保证原子操作:将
HashMap替换为ConcurrentHashMap,并通过原子方法处理数值更新,例如:
kc.getHist().compute(entrySet.getKey(), (key, currentValue) -> currentValue == null ? -entrySet.getValue() : currentValue - entrySet.getValue());
- 重构为独立影子变量:将Map的每个键对应值拆分为独立的影子变量,这是OptaPlanner推荐的方式,可天然避免集合带来的线程安全问题。
- 避免复杂集合作为影子变量:若业务场景允许,优先使用基础类型或简单结构作为影子变量,降低并发风险。
文档建议
若OptaPlanner官方不推荐多线程场景下使用非线程安全集合作为影子变量,建议在8.29.0.Final及后续版本文档中补充说明:
- 多线程求解时,影子变量优先使用线程安全数据结构或独立变量,而非非线程安全集合。
- 若必须使用集合类型,需自行处理并发修改的线程安全问题,否则可能导致结果异常。
内容的提问来源于stack exchange,提问作者Philip Harding
相关产品推荐
相关产品推荐

