I am seeing some strange NPEs and have found out that this is a concurrency issue. I'm using code similar to the following:
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
}
}
When I call Manager.doIt
, I sometimes get an NPE because Value.v
is null
. As far as I understand the happens-before relationships, it might be possible, that when Manager.get
is called concurrently and when there is no entry yet for a key, that I can get back a not yet fully initialized Value.
I'm registering objects in the constructor of Value
because the object graph between Value
objects can have cycles and without this, I would get a stackoverflow exception.
The question now is, how do I ensure in doIt
that the Value
and all connected values are fully initialized? I'm thinking about doing some kind of double checked locking in Manager.get
but I'm not sure how to solve this best. Something like this:
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;
}
Has anyone a better idea about how to solve this or sees a concurrency issue with that code?