惰性图结构缓存和并发
Lazy graph structure caching and concurrency
我看到了一些奇怪的 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.v
是 null
。据我了解 happens-before 关系,有可能,当 Manager.get
被同时调用并且还没有键的条目时,我可以取回一个尚未完全初始化的值。
我在 Value
的构造函数中注册对象,因为 Value
对象之间的对象图可以有循环,如果没有这个,我会得到一个 Whosebug 异常。
现在的问题是,如何确保doIt
中的Value
和所有连接的值都被完全初始化?我正在考虑在 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;
}
有没有人更好地了解如何解决这个问题或发现该代码的并发问题?
这里的问题是您在构造函数中进行 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()
。
我采用的解决方案是可扩展的,据我所知,它是安全的:
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();
}
}
我看到了一些奇怪的 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.v
是 null
。据我了解 happens-before 关系,有可能,当 Manager.get
被同时调用并且还没有键的条目时,我可以取回一个尚未完全初始化的值。
我在 Value
的构造函数中注册对象,因为 Value
对象之间的对象图可以有循环,如果没有这个,我会得到一个 Whosebug 异常。
现在的问题是,如何确保doIt
中的Value
和所有连接的值都被完全初始化?我正在考虑在 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;
}
有没有人更好地了解如何解决这个问题或发现该代码的并发问题?
这里的问题是您在构造函数中进行 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()
。
我采用的解决方案是可扩展的,据我所知,它是安全的:
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();
}
}