Is accessing two AtomicIntegers as one operation thread safe?

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.

Answer

From the “Java Concurrency in Practice” book:

NumberRange could be made thread-safe by using locking to maintain its invariants, such as guarding lower and upper with a common lock. It must also avoid publishing lower and upper to prevent clients from subverting its invariants.

That means the following code is thread-safe:

@ThreadSafe
public class NumberRange {

    @GuardedBy("this") private int lower, upper;

    public synchronized void setLower(int i) {
        if (i > upper) {
            throw new IllegalArgumentException("can't set lower to " + i + " > upper");
        }
        lower = i;
    }

    public synchronized void setUpper(int i) {
        if (i < lower) {
            throw new IllegalArgumentException("can't set upper to " + i + " < lower");
        }
        upper = i;
    }

    public synchronized boolean isInRange(int i) {
        return (i >= lower && i <= upper);
    }
}

In this case, the NumberRange provides its own locking to ensure that compound actions are atomic.

Leave a Reply

Your email address will not be published. Required fields are marked *