懒惰的图结构缓存和并发



我看到一些奇怪的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。据我所知,在关系之前发生的情况下,当Manager.get被并发调用时,当键还没有条目时,我可能会得到一个尚未完全初始化的值。

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

现在的问题是,如何在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,因为该对象的Subvaluev可能尚未初始化。

代码还有另一个问题。Manager.get()是一种复合作用,应该封装在synchronised块中。如果一个线程观察到一个键的null值,那么当它进入if块时,该观察可能会过时。由于映射涉及到复合操作,所以引用映射的所有方法都应该由同一个锁保护——基本上,您需要用同一个锁定保护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();
}
}

最新更新