【问题标题】:Lazy graph structure caching and concurrency惰性图结构缓存和并发
【发布时间】:2020-10-07 17:02:16
【问题描述】:

我看到一些奇怪的 NPE,并发现这是一个并发问题。我正在使用类似于以下的代码:

class Manager {
  private final ConcurrentMap<String, Value> map = new ConcurrentHashMap<>();

  public Value get(String key) {
    Value v = map.get(key);
    if (v == null) {
      new Value(key, this);
      v = map.get(key);
    }
    return v;
  }

  public void doIt(String key) {
    get(key).doIt();
  }

  void register(String key, Value v) {
    map.put(key, v);
  }
}

class Value {
  private final Value[] values;
  private final SubValue v;

  Value(String key, Manager m) {
    m.register(key, this);

    // Initialize some values, this is where a cycle can be introduced
    // This is just some sample code, the gist is, we call Manager.get
    this.vs = new Value[]{ m.get("some-other-key") };
    // Other code ...

    this.v = new SubValue(m);
  }

  public void doIt() {
    this.v.doIt(); // <--- NPE here. v is null sometimes
  }
}

当我调用 Manager.doIt 时,有时我会收到 NPE,因为 Value.vnull。据我了解之前发生的关系,当Manager.get 被同时调用并且还没有键的条目时,我可以取回尚未完全初始化的值。

我在Value 的构造函数中注册对象,因为Value 对象之间的对象图可以有循环,如果没有这个,我会得到一个stackoverflow 异常。

现在的问题是,我如何确保在 doItValue 和所有连接的值都已完全初始化?我正在考虑在Manager.get 中进行某种双重检查锁定,但我不确定如何最好地解决这个问题。像这样的:

  public Value get(String key) {
    Value v = map.get(key);
    if (v == null) {
      synchronized(map) {
        v = map.get(key);
        if (v == null) {
          v = new Value(key, this);
        }
      }
    }
    return v;
  }

有没有人知道如何解决这个问题或发现该代码存在并发问题?

【问题讨论】:

  • 虽然我还在弄清楚程序的其余部分,但双重检查锁定本身就存在内存可见性问题。
  • 这里的问题不是内存可见性。在 V 的构造函数中,你甚至在对象被正确构造之前就使这个引用转义了。
  • 附带说明,您的整个 get 方法无论如何都不是线程安全的。查看computeIfAbsent 并仔细阅读它的文档。

标签: java concurrency jvm java-memory-model


【解决方案1】:

这里的问题是您在构造函数中使this 转义。

class Value {
  private final Value[] values;
  private final SubValue v;

  Value(String key, Manager m) {
    m.register(key, this); <--- (this is not properly constructed)

    // Initialize some values, this is where a cycle can be introduced
    // This is just some sample code, the gist is, we call Manager.get
    this.vs = new Value[]{ m.get("some-other-key") };
    // Other code ...

    this.v = new SubValue(m);
  }

  public void doIt() {
    this.v.doIt(); // <--- NPE here. v is null sometimes
  }
}

现在,如果某个线程调用 doIt 在映射中具有不正确构造的对象的键上,您可能会得到一个 NPE,因为该对象的 Subvalue v 可能尚未初始化。

代码有另一个问题。 Manager.get() 是一个复合动作,应该封装在一个 synchronised 块中。如果一个线程观察到一个键的null 值,那么当它进入if 块时,该观察可能会变得陈旧。由于 map 涉及到复合动作,所有引用 map 的方法都应该被同一个锁保护 - 基本上你需要用同一个锁保护 get()register()

【讨论】:

  • 由于 register 只在Value 的构造函数中被调用,我认为我的解决方案应该是安全的,因为只有在创建Value 时才会获取锁,或者我错过了什么?跨度>
  • 不,这不安全。乳清调用寄存器,你在地图中放置了一个构造不正确的对象。存在竞争条件 - 另一个线程可能会获取该键,获取构造不正确的对象,并取消引用尚未初始化的字段。从构造函数中转义“this”从来都不是一个好习惯,即使它是构造函数的最后一步。
  • 1+,这是正确的。 @ChristianBeikov,您可以很容易地考虑一下:例如,某些线程读取this(无论如何)您调用register,而 before 构造线程执行this.v = new SubValue(m);。当然看到null
  • @ChristianBeikov 循环图的构造根本不依赖于register 方法。如果你只是先构造循环图的所有对象,然后遍历它们,为每个对象调用register,那也没什么区别。
  • @ChristianBeikov 在构造对象A 时,会将自身传递给B 的构造函数。就如此容易。比如this answerManager 绕道并没有改善任何东西。除非您考虑在构建过程中暴露不完整的对象是一种改进。
【解决方案2】:

我采用的解决方案如下,它是可扩展的,据我所知是安全的:

class Manager {
  private final ConcurrentMap<String, Value> map = new ConcurrentHashMap<>();

  public Value get(String key) {
    Value v = map.get(key);
    if (v == null) {
      Map<String, Value> subMap = new HashMap<>();
      new Value(key, subMap);
      map.putAll(subMap);
      v = map.get(key);
    }
    return v;
  }

  public void doIt(String key) {
    get(key).doIt();
  }
}

class Value {
  private final Value[] values;
  private final SubValue v;

  Value(String key, Map<String, Value> subMap) {
    subMap.put(key, this);

    // Initialize some values, this is where a cycle can be introduced
    // This is just some sample code, the gist is, we call Manager.get
    this.vs = new Value[]{ subMap.containsKey("some-other-key") ? subMap.get("some-other-key") : m.get("some-other-key") };
    // Other code ...

    this.v = new SubValue(m);
  }

  public void doIt() {
    this.v.doIt();
  }
}

【讨论】:

    猜你喜欢
    • 2019-01-14
    • 1970-01-01
    • 2013-08-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-09-01
    • 1970-01-01
    • 2019-12-18
    相关资源
    最近更新 更多