Reputation: 1676
Consider the following code:
ConcurrentHashMap<String, Value> map = new ConcurrentHashMap<>();
boolean foo(String key) {
Value value = map.get(key);
if (value == null) {
value = map.putIfAbsent(key, new Value());
if (value == null) {
// do some stuff
return true;
}
}
// do some other stuff
return false;
}
Assume that foo()
is called by multiple threads concurrently. Also assume that calling new Value()
is expensive. The code is verbose and can still result in redundant Value
objects created. Can the above logic be implemented in a way that guarantees no redundant Value
objects are created (i.e. new Value()
is called at most once)? I'm looking for a clean implementation- minimal code without acquiring locks explicitly.
computeIfAbsent
could have been a good alternative, however its return semantics are not in line with the required logic.
Upvotes: 6
Views: 1431
Reputation: 12346
Consider your method foo
( which is literally a wrapper for putIfAbsent ).
String key = "test";
if(foo(key)){
//Positive Branch.
} else{
//Negative Branch.
}
Now, in a multi-threaded environment, thread A calls and completes foo and adds a new value. It is going down the Positive branch. Before it enters the Positive branch, it gets scheduled for later. Thread B calls and completes foo, it continues executing and goes down the Negative branch.
The Negative branch is getting executed before the Positive branch, which in my mind is not wanted. In your particular case it might be OK. compute
might be a better replacement.
map.compute( key, ( k, old ) -> {
if(old==null){
Value v = new Value();
//positive branch.
return v;
} else{
//negative branch.
return old;
}
});
Now you would be acting atomically on whether or not the value exists.
Upvotes: 0
Reputation: 328737
One solution is to store Future<Value>
instead of Value
in the map:
ConcurrentHashMap<String, Future<Value>> map = new ConcurrentHashMap<>();
boolean foo(String key) {
Future<Value> value = map.get(key);
if (value == null) {
value = map.putIfAbsent(key, new FutureTask<Value>(() -> new Value()));
if (value == null) {
// do some stuff
return true;
}
}
// do some other stuff
return false;
}
You can access the underlying value by calling value.get()
, which will block until the computation is complete.
There is a chance that more than one FutureTask is created, but only one will reach the map and only one computation of new Value()
will be done.
Upvotes: 2
Reputation: 425198
Some minimal code that does the job:
boolean foo(String key) {
AtomicBoolean flag = new AtomicBoolean();
Value value = map.computeIfAbsent(key, k -> {flag.set(true); return new Value();});
if (flag.get()) {
// do some stuff
} else {
// do some other stuff
}
return flag.get();
}
Upvotes: 3
Reputation: 45329
One way is to use local state and update it in computeIfAbsent
's mapping function:
boolean foo(String key) {
boolean[] b = { false };
map.computeIfAbsent(key, k -> {
// do some stuff
b[0] = true;
return new Value();
});
return b[0];
}
Because mappingFunction
is only run if the key is not present in the map, you can guarantee that the heavy new Value()
is only called when necessary and that the return value is set to true
only when there was no mapping before the call.
Upvotes: 1
Reputation: 44200
First let's fix the fact that you are not acting atomically, and do a needless look-up. Two threads could both simultaneously pass the first value == null
check. Not really a problem now (except 2 Value
s will be created, which is slow), but a bug waiting to happen if someone adds an else clause to the second value == null
check. It's cleaner this way too.
boolean foo(String key) {
Value value = map.putIfAbsent(key, new Value());
if (value == null) {
// do some stuff
return true;
}
else {
// do some other stuff
return false;
}
}
Now let's address the fact that Value
creation is slow (sounds like you are abusing constructor, but anyway).
boolean foo(String key) {
final AtomicBoolean wasCreated = new AtomicBoolean(false);
final Value value = map.computeIfAbsent(key, k -> {
wasCreated.set(true);
return new Value();
});
if (wasCreated.get()) {
// do some stuff
return true;
}
else {
// do some other stuff
return false;
}
}
Upvotes: 1