Wednesday, May 17, 2023

Quiz yourself: Crossing Java’s CyclicBarrier in a multithreaded environment

Oracle Java Certification, Core Java, Oracle Java Learning, Java Prep, Oracle Java Learning, Oracle Java Quiz


Given the following class

public class Escape {
  static class Task implements Runnable {
    int i;
    CyclicBarrier cb;
    public Task(int i, CyclicBarrier cb) {this.i=i; this.cb=cb;}
    @Override
    public void run() {
      try {
        cb.await();
      } catch (Exception e) {}
      System.out.printf("Task %d executed%n", i);
    }
  }
  public static void main(String[] args) {
    final ExecutorService es = Executors.newFixedThreadPool(2);
    final CyclicBarrier cb = new CyclicBarrier(3);
    IntStream
      .rangeClosed(1, 10)
      .mapToObj(i -> new Task(i, cb))
      .forEach(t -> es.execute(t));
    es.shutdown();
  }
}

What is the result? Choose one.

A. Nothing is printed.
B. Only tasks 1 and 2 execute in arbitrary order.
C. Only tasks 1, 2, and 3 execute in arbitrary order.
D. Only tasks 1 through 9 execute in arbitrary order.
E. All tasks execute in arbitrary order.

Answer. As often happens with exam questions, the complexity of this code disguises the relative simplicity of the solution. Spotting the thing that really matters to solve the question quickly is really a manifestation of debugging skill. As such, it is not a trick question, but it can certainly be a little frustrating if you miss the key point. Never spend too long on one question until you’ve answered all the others—but we’ve made that point before.

This code uses the rangeClosed method of the IntStream class. This method produces a series of monotonically increasing int values starting with the first argument value and ending with the second argument value; that means 1 through 10 in this case. (If instead the range method had been used, the second argument would behave as a fence value; that is, the range produced would stop short of that second argument’s value.)

These 10 numbers are used to build tasks described by the Task class. The tasks all share a single CyclicBarrier, and each task is passed to the executor service for execution.

In the question, the code uses a thread pool that has two threads and a CyclicBarrier initialized with an argument value of 3.

The behavior of the CyclicBarrier may be thought of as a door with a handle. In this example, the door handle must be turned three times to open the door (that’s due to the argument of 3 in the constructor of the CyclicBarrier). The await method turns the door handle when it’s called. If a call is not the third call to await, the calling thread stops executing until other threads call await again, and at the third call the door opens. When the door opens, those three threads continue executing—they pass through the door, to continue this analogy—and the door closes behind them. Further threads calling await will again be made to wait, until another three calls to await have been made. This process continues; that’s why the CyclicBarrier is cyclic.

In this quiz’s code, there are exactly two threads in the pool. This means that only two tasks can be in process at any one instant. Note that tasks that are blocked in methods such as await are still in process. Therefore, the code has asked the pool to execute 10 tasks, but it gave the pool only two threads, so only two tasks can be in process at one time. As soon as the first two tasks have called await, their execution is on hold. The tasks are in process but waiting for the door to open. Because the pool only has two threads, no threads are available to execute a third task, and the door handle can never be turned for the crucial third time. The system simply stops right there, never to make any more progress. Also note that at this point, no output has been generated.

Next, consider the shutdown() method. This waits until all the in-process tasks are completed, but the two tasks that are waiting for that door to open will never continue executing, and they will never be complete. This also means that the other eight tasks never even start. Because of this, the program never ends, and no output is ever printed. This makes option A correct and options B, C, D, and E incorrect.

To understand how this works, imagine some variant scenarios.

Suppose you created three threads in the pool with Executors.newFixedThreadPool(3). In this situation, three tasks can be in process, and they will be able to call await the necessary three times. At that point, the first three tasks will move on and print the “Task executed” message. After those first three tasks are completed, the threads from the pool will become available and will pick up the next tasks, which are tasks 4, 5, and 6. These too will then be able to execute the await method three times, print their message, and finish. Then tasks 7, 8, and 9 will proceed to completion too. Unfortunately, the 10th task will be stuck, as there will not be the necessary additional two calls to await to allow it to be completed. Notice that in this scenario, the problem is that no task exists to make the call, not that there are no threads to run the tasks. But the bottom line is that task 10 would never print its message, and the program will still never terminate.

Another possible change would be to have two threads with Executors.newFixedThreadPool(2) and require two calls to await with new CyclicBarrier(2) instead of three calls. In this case, the tasks finish in groups of two because you need to turn the door handle only twice to open the door. Consequently, all 10 tasks would be completed, each printing its message. After all 10 have been completed, the program would shut down.

One final point: When three threads have called the await method, there’s no particular expectation that they will resume executing in the same order in which they arrived at the await method. Even if they did, the operating system’s thread scheduling could arbitrarily hold one up while letting another run freely, so that their relative arrival times at the printf statement would still not be predictable. Because of this, any output from the groups of two or three threads in the two alternative scenarios must be assumed to be printed in arbitrary order.

Conclusion. The correct answer is option A.

Source: oracle.com

Related Posts

0 comments:

Post a Comment