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?

2
  • 2
    You need to run your micro benchmarks repeatedly, otherwise natural variance will overwhelm any actual signal in your measurements. Run the tens or hundreds of times. Commented Oct 26, 2010 at 22:09
  • I already do. The effect is the same. Especially in scenario 3 the application is slow when I start only one process, and it is faster whenever I start the second process. When I later stop the second process, the first becomes slower again. Commented Oct 27, 2010 at 6:02

2 Answers 2

1

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.

Sign up to request clarification or add additional context in comments.

2 Comments

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

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.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.