Wednesday, June 21, 2023

Quiz yourself: Locking mechanisms and Java’s postincrement ++ operator

Oracle Java Certification, Oracle Java Prep, Oracle Java Guides, Java Career, Java Jobs, Java Skills, Java Preparation

Let’s see if this quiz code will work consistently.


Imagine that your colleagues want to perform calculations in parallel. In pursuit of thread safety, they implemented the following locking mechanism:

var lock = new ReentrantLock();
var s = IntStream.of(1, 2, 3);
var i = s.parallel().map(
  v -> {
    lock.tryLock();
    var res = v++;
    lock.unlock();
    return res;
  }
).sum();
System.out.println(i);

Which statement is correct about this code? Choose one.

A. It consistently prints 12 to console.
B. It might print 12 to console.
C. It consistently throws an exception.
D. It might throw an exception.
E. It prints nothing, hanging indefinitely.

Answer. This question centers on the behavior of the ReentrantLock.tryLock() method, and it expects you to understand how the postincrement ++ operator works.

Consider the computation performed by the block lambda in the map step of the stream. It takes its argument, v, copies that value to the local variable res, and then returns the value of res. Notice that the increment operator used on v is a postincrement operator. As a result, the numbers that are in the stream at the point of the sum() operation are 1, 2, and 3. Therefore, if any value is printed, it could only be 6. This tells you that options A and B, both of which suggest a possible output of 12, must be incorrect.

Now consider the locking behavior. The tryLock method never blocks—if the lock cannot be obtained, the method immediately returns false, and execution proceeds with no lock held. On the other hand, if the lock is obtained successfully, the method returns true and, as before, continues without blocking.

Because tryLock never blocks, you can determine that option E must be incorrect; there’s no reason for the code to hang indefinitely.

Now you get to the crux of the question. If an unlock call is made on a ReentrantLock that is not in the locked state, it throws an IllegalMonitorStateException. You must decide whether this will definitely happen or is simply a possibility.

Looking at the code, it’s easy to imagine that as the three numbers arrive at the map method, they could do so with a timing that simply results in three nonoverlapping sets of calls: tryLock, unlock; tryLock, unlock; and tryLock, unlock. If that sequence happens, no exception will arise.

This means that the exception might arise but is not guaranteed. From this you know that option D is correct because the code might throw an exception. You also know that option C is incorrect because there’s no guarantee the exception would be thrown consistently.

By the way, the code can definitely be fixed.

Fundamentally, there is no need for locking in this code because the map operation’s lambda does nothing to the data in the stream, and the map operation can simply be deleted. If this code were a placeholder or an error—perhaps the intention was to double the values (expecting the answer 12)—all that’s needed is the following simple step in place of the current map operation:

.map(v -> 2 * v)

This code needs no lock because it interacts only with method-local data and not with anything shared; thus, there’s no thread-safety issue.

If, however, the intention was actually to mutate some shared data (there’s no indication of that effect in this code), locking might be a suitable solution (though such side effects are strongly discouraged in stream code anyway). If that were intended, tryLock would be unsuitable for two reasons: The operation would be unprotected if no lock were obtained, and perhaps the operation would simply be skipped over if the failure to lock caused the code to skip the operation.

The following modification to the existing code would avoid the exception, but it would not actually guarantee a lock and would skip the operation if no lock were obtained:

.map(
  v -> {
    var res = v;
    if (lock.tryLock()) {
      v++; // still useless and skipped if no lock obtained!
      lock.unlock(); // only unlock if lock succeeded
    }
    return res;
  }
)

To actually protect an operation with mutual exclusion, the code could look like the following:

.map(
  v -> {
    var res = v;
    lock.lock() // blocking call, waits for locking to succeed
    try {
      v++; // still useless, but protected by a lock 
    } finally {
      lock.unlock(); // unlock reliably
    }
    return res;
  }
}

Notice the recommended location for an unlock() operation is in a finally block, which ensures that no matter how the body of the try block proceeds or terminates, the lock will definitely be released.

Conclusion. The correct answer is option D.

Source: oracle.com

Related Posts

0 comments:

Post a Comment