synchronized does not operate on variables but objects. It also doesn’t lock memory, but only provides mutual exclusion and memory visibility guarantees for threads performing synchronized on the same object.
This implies that any synchronization on a mutable variable doesn’t work at all.
For your specific case, you’d first have to ensure that the ArrayList is pre-filled with the necessary number of elements before the multi-threaded access even starts and never changes its size, as otherwise, even the memory visibility of its internal array reference and size fields are not guaranteed.
Then, the general issues with synchronizing on a mutable variable arise.
A code like
synchronized(variable) {
return variable;
}
bears two reads of variable, the first one being performed before entering the synchronized block, to determine, which object to synchronize on. Since this first read is performed without any synchronization primitive, it is not guaranteed to see the most recent value. Though, “most recent” doesn’t have much meaning anyway, as the writers performing
synchronized(variable) {
variable = newValue;
}
are synchronizing on different objects and hence, have no ordering relation anyway.
But even the most trivial scenario, one writer performing synchronized(variable) { variable = newValue; }, followed by one reader performing synchronized(variable) { return variable; } does not work, as the writer synchronized on the old value, while the reader may read any value in the first access, including the most recent value. Since both threads synchronize on different objects, there are no guarantees, which makes the reader’s second read unspecified. As unintuitive as it is, the second read could even read an older value.
But this is not the worst outcome. When you are not using immutable objects like String or Integer, this racy read may return a more recent object reference, but does not guaranty the visibility of its member fields, ending up in an entirely inconsistent state.
In other words, your issues are not about the mutual exclusion covering only reading and writing a single variable, but the missing memory visibility guarantees.
When you are fine with a fixed size, which your example using only List.set and List.get implies, you can use an AtomicReferenceArray:
public class Program {
// provide needed size
final AtomicReferenceArray<Object> data = new AtomicReferenceArray<>(100);
Object readFromList(int index) {
return data.get(index);
}
// returns old object
Object writeToList(int index, Object obj) {
return data.getAndSet(index, obj);
}
}
If you need a changeable size or really need to lock a place for a nontrivial operation, you could use, e.g.
public class Program {
final ConcurrentHashMap<Integer, Object> data = new ConcurrentHashMap<>();
// returns the new index
int addToList(Object o) {
int ix;
do ix = data.size(); while(data.putIfAbsent(ix, o) != null);
return ix;
}
Object readFromList(int index) {
return data.get(index);
}
void updateList(int index, Object input) {
data.compute(index, (ix, old) -> {
// compute based on old and input
return input;// or computed value
});
}
}
readFromListelement gets the item from the list and then immediately releases the lock; so, the only thing I can imagine you hoping for here is visibility of the element up to the time it was last written withwriteToList.list(i) = obj;is not valid syntax