CountDownLatch vs wait()/notify()

Published on — Filed under protip

I'm a java.util.concurrent junkie; always on the lookout for chances to replace old code with stuff from this package.

One of the utilities I use most of the times is the CountDownLatch.

While the base implementation of the HttpRequestFuture (DefaultHttpRequestFuture) interface in hotpotato highly draws inspiration from Netty's own DefaultChannelFuture, I though this was the perfect place for a major refactor using that dearly beloved package.

First off, I replaced most of the synchronized blocks with AtomicBooleans. The performance gain was insignificant, even with higher contention scenarios (multiple threads accessing the synchronized blocks). Plus, I’ve come to realize that this change is not as safe as using synchronized blocks.

Secondly, I decided to replace the wait() calls with a CountDownLatch. Looking at the code, it looked like a great refactor. Swapped lots of lines and error-prone manual synchronization with elegant one-liners. But this change made everything go south.

Swapping manual synchronization with a CountDownLatch caused the loss of around ~500 requests per second (the test was 1000 batches of 1000 requests).

I then extracted the core of the problem to a micro-benchmark to confirm the performance issue:

public class Test2 {
  private static final int THREADS = 10;
  private static final int ITERATIONS = 100000;

  public static void main(String[] args) {
    // warmup
    runTest(new ArrayList<long>(1000), 100, 10);

    // actual test
    int iterations = ITERATIONS;
    int nThreads = THREADS;
    final List<long> times = Collections
        .synchronizedList(new ArrayList<long>(nThreads * iterations));
    runTest(times, iterations, nThreads);

    long total = 0;
    for (long time : times) {
        total += time;
    }
    System.err.println("Total spent on toggleCondition(): " +
                       ((double) total / 1000000));
    System.err.println("Average time spent on toggleCondition(): " +
                       ((double) total / 1000000) / ITERATIONS);
  }

  private static void runTest(final List<long> times, int iterations,
                              int nThreads) {
    for (int i = 0; i < iterations; i++) {
      Thread[] threads = new Thread[nThreads];
      // Switch these for different tests.
      final Condition condition = new WaitCondition(nThreads);
      //final Condition condition = new LatchCondition(nThreads);
      for (int j = 0; j < nThreads; j++) {
        threads[j] = new Thread(new Runnable() {
          public void run() {
            long start, end;
            start = System.nanoTime();
            try {
              condition.toggleCondition();
            } catch (InterruptedException ignored) { }
            end = System.nanoTime();
            times.add(end - start);
          }
        });
        threads[j].setName("thread-" + j);
      }

      for (Thread thread : threads) {
        thread.start();
      }
      for (Thread thread : threads) {
        try {
          thread.join();
        } catch (InterruptedException ignored) { }
      }
    }
  }

  public static interface Condition {
    void toggleCondition() throws InterruptedException;
  }

  public static class WaitCondition
        implements Condition {

    private final int number;
    private int waiters;

    public WaitCondition(int number) {
      this.number = number;
      this.waiters = 0;
    }

    public void toggleCondition()
        throws InterruptedException {
      synchronized (this) {
        this.waiters++;
        if (this.waiters >= this.number) {
          this.notifyAll();
        } else {
          this.wait();
        }
        this.waiters--;
      }
    }
  }

  public static class LatchCondition
      implements Condition {
    private final CountDownLatch latch;

    public LatchCondition(int number) {
      this.latch = new CountDownLatch(number);
    }

    public void toggleCondition()
        throws InterruptedException {
      this.latch.countDown();
      this.latch.await();
    }
  }
}

And the results (10 threads, 10000000 iterations):

Hardly enough to justify the performance loss I'm experiencing in the aforementioned case. I'll have to keep digging for the cause...

With this, I guess it's safe to say that CountDownLatch is a perfect replacement for manual wait()/notify() synchronization; fewer lines, clearer code and same performance.