Reputation: 34024
Consider the following implementation of some kind of fixed size cache, that allows lookup by an integer handle:
static class HandleCache {
private final AtomicInteger counter = new AtomicInteger();
private final Map<Data, Integer> handles = new ConcurrentHashMap<>();
private final Data[] array = new Data[100_000];
int getHandle(Data data) {
return handles.computeIfAbsent(data, k -> {
int i = counter.getAndIncrement();
if (i >= array.length) {
throw new IllegalStateException("array overflow");
}
array[i] = data;
return i;
});
}
Data getData(int handle) {
return array[handle];
}
}
There is an array store inside the compute function, which is not synchronized in any way. Would it be allowed by the java memory model for other threads to read a null value from this array later on?
PS: Would the outcome change if the id returned from getHandle
was stored in a final
field and only accessed through this field from other threads?
Upvotes: 3
Views: 1049
Reputation: 9096
Assuming that you determine the index for the array read from the value of counter
than yes - you may get a null read
The simplest example (there are others) is a follows:
T1 calls getHandle(data)
and is suspended just after int i = counter.getAndIncrement();
T2 calls handles[counter.get()]
and reads null.
You should be able to easily verify this with a strategically placed sleep
and two threads.
Upvotes: 1
Reputation: 533660
The read access isn't thread safe. You could make it thread safe indirectly however it's likely to be brittle. I would implemented it in a much simpler way and only optimise it later should it prove to a performance problem. e.g. because you see it in a profiler for a realistic test.
static class HandleCache {
private final Map<Data, Integer> handles = new HashMap<>();
private final List<Data> dataByIndex = new ArrayList<>();
synchronized int getHandle(Data data) {
Integer id = handles.get(data);
if (id == null) {
id = handles.size();
handles.put(data, id);
dataByIndex.add(id);
}
return id;
}
synchronized Data getData(int handle) {
return dataByIndex.get(handle);
}
}
Upvotes: 1
Reputation: 29710
From the documentation of ConcurrentHashMap#computeIfAbsent
:
The entire method invocation is performed atomically, so the function is applied at most once per key. Some attempted update operations on this map by other threads may be blocked while computation is in progress, so the computation should be short and simple, and must not attempt to update any other mappings of this map.
The documentation's reference to blocking refers only to update operations on the Map
, so if any other thread attempts to access array
directly (rather than through an update operation on the Map
), there can be race conditions and null
can be read.
Upvotes: 0