views:

80

answers:

2

I want to understand performance in multithreaded environments. For that I have written a small test that I ran on my machine (quad-core Intel, Windows XP, Sun JDK 1.6.0_20), with surprising results.

The test is basically a thread-safe counter that is synchronized using either the synchronized keyword or an explicit lock. Here is the code:

import java.util.concurrent.locks.ReentrantLock;

public class SynchronizedPerformance {

  static class Counter {

    private static final int MAX = 1 << 24;

    int count;
    long lastLog = 0;

    private final ReentrantLock lock = new ReentrantLock();

    private int incrementAndGet() {
      count++;
      if (count == MAX) {
        long now = System.nanoTime();
        if (lastLog != 0) {
          long elapsedTime = now - lastLog;
          System.out.printf("counting took %.2f ns\n", Double.valueOf((double)elapsedTime / MAX));
        }
        lastLog = now;
        count = 0;
      }
      return count;
    }

    synchronized int synchronizedIncrementAndGet() {
      return incrementAndGet();
    }

    int lockedIncrementAndGet() {
      lock.lock();
      try {
        return incrementAndGet();
      } finally {
        lock.unlock();
      }
    }
  }

  static class SynchronizedCounterAccessor implements Runnable {

    private final Counter counter;

    public SynchronizedCounterAccessor(Counter counter) {
      this.counter = counter;
    }

    @Override
    public void run() {
      while (true)
        counter.synchronizedIncrementAndGet();
    }
  }

  static class LockedCounterAccessor implements Runnable {

    private final Counter counter;

    public LockedCounterAccessor(Counter counter) {
      this.counter = counter;
    }

    @Override
    public void run() {
      while (true)
        counter.lockedIncrementAndGet();
    }
  }

  public static void main(String[] args) {
    Counter counter = new Counter();
    final int n = Integer.parseInt(args[0]);
    final String mode = args[1];

    if (mode.equals("locked")) {
      for (int i = 0; i < n; i++)
        new Thread(new LockedCounterAccessor(counter), "ca" + i).start();
    } else if (mode.equals("synchronized")) {
      for (int i = 0; i < n; i++)
        new Thread(new SynchronizedCounterAccessor(counter), "ca" + i).start();
    } else {
      throw new IllegalArgumentException("locked|synchronized");
    }
  }
}

I made the following observations:

  1. java SynchronizedPerformance 1 synchronized works pretty well, and takes about 15 ns per step.
  2. java SynchronizedPerformance 2 synchronized interferes a lot and takes about 150 ns per step.
  3. When I start two independent processes of java SynchronizedPerformance 2 synchronized each of them takes about 100 ns per step. That is, starting the process a second time makes the first one (and the second) faster.

I don't understand the third observation. What plausible explanations exist for this phenomenon?

+1  A: 

The most likely is that there are certain fixed overheads that exist regardless of how many threads exist- for example, garbage collection or other resource management.

DeadMG
A: 

You are running into a situation where performance is entirely dependent on how the scheduler operates. In #3, when any other process in the system wants some time (even a little bit), it will suspend one of your 4 threads. If that thread happens to not hold the lock at when it is suspended, its "pair" can now run uncontested, and will make lots of progress (runs at 20x speed compared to the contested situation).

Of course, if it is swapped out when it does hold the lock, its "pair" will make no progress. So you have two competing factors, and the overall runtime depends on the fraction of time the lock is held by a thread and the penalty/bonus you get for each situation. Your bonus is substantial so I would expect some overall speedup like you saw.

Keith Randall
Are you suggesting that locks in Java are shared across different JVM processes? Because this is not true.
matt b
Sorry, I believe I used "process" in a couple of places where I should have used "thread". I'll edit.
Keith Randall