In Brian Goetz's Java Concurrency in Practice, there is an example as follows:
public class NumberRange {
// INVARIANT: lower <= upper
private final AtomicInteger lower = new AtomicInteger(0);
private final AtomicInteger upper = new AtomicInteger(0);
public void setLower(int i) {
// Warning -- unsafe check-then-act
if (i > upper.get())
throw new IllegalArgumentException(
"can't set lower to " + i + " > upper");
lower.set(i);
}
public void setUpper(int i) {
// Warning -- unsafe check-then-act
if (i < lower.get())
throw new IllegalArgumentException(
"can't set upper to " + i + " < lower");
upper.set(i);
}
public boolean isInRange(int i) {
return (i >= lower.get() && i <= upper.get());
}
}
I understand that the above code is prone to race conditions.
Then he explains following:
Multivariable invariants like this one create atomicity requirements: related variables must be fetched or updated in a single atomic operation. You cannot update one, release and reacquire the lock, and then update the others, since this could involve leaving the object in an invalid state when the lock was released.
Of what i understand from this paragraph is that if we make the setUpper and the setLower functions synchronized, then also there will be situations when the object might reach an invalid state. However, I think that if both functions are synchronized then only one thread can execute either of the function and each function has necessary checks for the invariant. How can we be left in invalid state. Can anyone demonstrate with an example. What am i missing here?
If i am understanding it correctly, then what is the significance of this line:
You cannot update one, release and reacquire the lock, and then update the others, since this could involve leaving the object in an invalid state when the lock was released.