Java Synchronization Method Performance Comparison

Compares the performance of the various thread synchronization mechanisms provided by Java..

Object Partners

Some highly concurrent work I recently did got me thinking about the various methods Java provides for synchronizing access to data. I decided to do some testing to see how they compare. The test code is at the bottom of the post.

I want to start with a few caveats:

  • This level of detail is great to understand but you should not choose your methods of synchronization based on performance considerations. Use the methods that result in the cleanest code. In most cases where there are synchronization related performance issues the problem is with what the developer synchronized, not the synchronization method. Critical sections (code that is synchronized) should be as short as possible without resulting in code that is difficult to maintain.
  • This functionality is likely to be very implementation dependent. You may get very different results with a different JVM vendor or version, or on different hardware.

I tested the following synchronization methods: synchronized keyword, volatile field, AtomicInteger, Lock, and fair Lock. The test is fairly simple. I ran various numbers of threads that increment counters using the different synchronization methods. Each thread enters a critical section, increments a counter, and exits the critical section in a tight loop so contention is very high. Each synchronization method has its own counter. The result is the time, in milliseconds, it takes the counter to increment to 10 million. I used the following scenarios with various numbers of threads:

  • All threads incrementing a single counter. Each counter has a column in the table. The fair lock method is only tested with 1 and 2 threads and not included in the serial or concurrent tests.
  • Each thread sequentially incrementing each counter. This is the “Serial” column.
  • Each thread incrementing a single counter with the counters distributed among the threads. This is the “Concurrent” column. Since there are 4 methods tested this column is only populated when the thread count is a multiple of 4 so the counters can be evenly distributed among the threads.

The test results are below. My hardware and JVM version are shown in the results. I have a single CPU with 4 hyper-threaded cores so up to 8 threads may be running at a time.

Initializing test...  

Mac OS X (i386) version 10.6.8  

java.version "1.6.0_29"
Java(TM) SE Runtime Environment (1.6.0_29-b11-402-10M3527)
Java HotSpot(TM) Client VM (build 20.4-b02-402, mixed mode)  

JVM Stabilization Count Limit: 10000000
Test Count Limit: 10000000  

Threads Syncronized    Volatile      Atomic        Lock    FairLock      Serial  Concurrent
      1         236          91         116         289         321         734
      2        2195         604         577         922       29686        4461
      3        2197         603         876         564                    4676
      4        2451         965        1071         567                    5093         864
      5        2401        1006        1118         588                    5199
      6        2341        1037        1221         592                    5113
      7        2398        1038        1343         592                    5389
      8        2378        1048        1451         600                    5527        2283
      9        2399        1163        1449         605                    5699
     12        2364        1465        1383         618                    6128        2537
     24        2464        2957        1431         605                    7817        2557
     48        2486        5604        1355         611                   10008        2755
     96        2487       10849        1182         596                   16793        3192  

 Test complete

The results are not hard to interpret so I won’t go through them in detail. Here are the main things I noted:

  • Volatile performs better than other methods under minimal contention but degrades as contention increases.
  • Synchronized and lock are both pretty consistent regardless of contention.
  • Lock is the best choice for performance and consistency
  • Adding threads does not necessarily improve performance. The fastest time incrementing all the counters was the singe serial thread. The amount of contention, the number of cores available, and the amount of time threads spend waiting on resources all affect the point where adding threads reduces performance.
  • Fair lock is extremely slow. It continues to degrade rapidly as contention increases. It was impractical to include it in more tests. You should only use this where fairness is really required.
  • The serial vs concurrent columns show expected behavior but its worth noting in more detail. Where they are both present they use the same number of threads to do the same work. The threads in the serial column are each working with all the counters incrementing them sequentially in a loop. If there are 4 threads then each thread is incrementing every counter. The threads in the concurrent column are each working with one counter. If there are 4 threads then each thread is incrementing a different counter, minimizing contention; if 8 threads then 2 threads are incrementing each counter; and so on. The results clearly demonstrate how important it is to minimize thread contention in highly concurrent applications.

If you find different performance on other JVMs or hardware please post a comment. Also let me know if you find a bug in the test code.

Here’s the test code:

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;  

public class SyncTest {  

	private enum TestType {
		SYNCHRONIZED, VOLATILE, ATOMIC, LOCK, FAIR_LOCK
	}  

	private final Lock lock = new ReentrantLock(false);
	private final Lock fairLock = new ReentrantLock(true);
	private int synchronizedCounter;
	private int fairLockCounter;
	private int lockCounter;
	private volatile int volatileCounter;
	private final AtomicInteger atomicCounter = new AtomicInteger();  

	public static void main(String[] args) throws Exception {
		new SyncTest().run();
	}  

	public void run() throws Exception {  

		System.out.println("Initializing test...");
		System.out.println();
		System.out.printf("%s (%s) version %s n", System.getProperty("os.name"), System.getProperty("os.arch"),
				System.getProperty("os.version"));
		System.out.println();
		System.out.printf("java.version "%s"n", System.getProperty("java.version"));
		System.out.printf("%s (%s)n", System.getProperty("java.runtime.name"),
				System.getProperty("java.runtime.version"));
		System.out.printf("%s (build %s, %s)n", System.getProperty("java.vm.name"),
				System.getProperty("java.vm.version"), System.getProperty("java.vm.info"));
		System.out.println();  

		int jvmStabilizationEndValue = 100000;
		int endValue = 10000000;
		System.out.println("JVM Stabilization Count Limit: " + endValue);
		System.out.println("Test Count Limit: " + endValue);
		System.out.println();  

		// run to let JVM do any optimizations and stabilize
		runIndividualTest(TestType.SYNCHRONIZED, 1, jvmStabilizationEndValue);
		runIndividualTest(TestType.VOLATILE, 1, jvmStabilizationEndValue);
		runIndividualTest(TestType.ATOMIC, 1, jvmStabilizationEndValue);
		runIndividualTest(TestType.LOCK, 1, jvmStabilizationEndValue);
		runTestsConcurrently(1, jvmStabilizationEndValue);
		runTestsSerially(1, jvmStabilizationEndValue);  

		System.out
				.printf("Threads Syncronized    Volatile      Atomic        Lock    FairLock      Serial  Concurrentn");  

		runAllTests(1, endValue);
		runAllTests(2, endValue);
		runAllTests(3, endValue);
		runAllTests(4, endValue);
		runAllTests(5, endValue);
		runAllTests(6, endValue);
		runAllTests(7, endValue);
		runAllTests(8, endValue);
		runAllTests(9, endValue);
		runAllTests(12, endValue);
		runAllTests(24, endValue);
		runAllTests(48, endValue);
		runAllTests(96, endValue);  

		System.out.println("n Test complete");
	}  

	private void runAllTests(int threadCount, int endValue) throws Exception {  

		long synchronizedElapsed = runIndividualTest(TestType.SYNCHRONIZED, threadCount, endValue);
		long volatileElapsed = runIndividualTest(TestType.VOLATILE, threadCount, endValue);
		long atomicElapsed = runIndividualTest(TestType.ATOMIC, threadCount, endValue);
		long lockElapsed = runIndividualTest(TestType.LOCK, threadCount, endValue);  

		long serialElapsed = runTestsSerially(threadCount, endValue);
		long concurrenteElapsed = runTestsConcurrently(threadCount, endValue);  

		if (concurrenteElapsed > 0) {  

			System.out.printf("%7d %11d %11d %11d %11d %11s %11d %11dn", threadCount, synchronizedElapsed,
					volatileElapsed, atomicElapsed, lockElapsed, "", serialElapsed, concurrenteElapsed);  

		} else if (threadCount <= 2) {  

			long fairLockElapsed = runIndividualTest(TestType.FAIR_LOCK, threadCount, endValue);
			System.out.printf("%7d %11d %11d %11d %11d %11d %11dn", threadCount, synchronizedElapsed, volatileElapsed,
					atomicElapsed, lockElapsed, fairLockElapsed, serialElapsed);
		} else {  

			System.out.printf("%7d %11d %11d %11d %11d %11s %11dn", threadCount, synchronizedElapsed, volatileElapsed,
					atomicElapsed, lockElapsed, "", serialElapsed);
		}
	}  

	private long runIndividualTest(final TestType testType, int threadCount, final int endValue) throws Exception {  

		final CyclicBarrier testsStarted = new CyclicBarrier(threadCount + 1);
		final CountDownLatch testsComplete = new CountDownLatch(threadCount);  

		for (int i = 0; i < threadCount; i++) {
			startTestThread(testType, testsStarted, testsComplete, endValue);
		}  

		return waitForTests(testsStarted, testsComplete);
	}  

	private long runTestsSerially(int threadCount, final int endValue) throws Exception {  

		final CyclicBarrier testsStarted = new CyclicBarrier(threadCount + 1);
		final CountDownLatch testsComplete = new CountDownLatch(threadCount);  

		for (int i = 0; i < threadCount; i++) {
			Thread t = new Thread() {
				public void run() {  

					try {
						testsStarted.await();  

						runSynchronizedTest(endValue);
						runVolatileTest(endValue);
						runAtomicTest(endValue);
						runLockTest(endValue);  

					} catch (Throwable t) {
						t.printStackTrace();
					} finally {  

						testsComplete.countDown();
					}
				}  

			};
			t.start();
		}  

		return waitForTests(testsStarted, testsComplete);
	}  

	private long runTestsConcurrently(int threadCount, int endValue) throws Exception {  

		if (threadCount % 4 != 0) {
			return -1;
		}  

		final CyclicBarrier testsStarted = new CyclicBarrier(threadCount + 1);
		final CountDownLatch testsComplete = new CountDownLatch(threadCount);  

		threadCount /= 4;
		for (int i = 0; i < threadCount; i++) {
			startTestThread(TestType.SYNCHRONIZED, testsStarted, testsComplete, endValue);
			startTestThread(TestType.VOLATILE, testsStarted, testsComplete, endValue);
			startTestThread(TestType.ATOMIC, testsStarted, testsComplete, endValue);
			startTestThread(TestType.LOCK, testsStarted, testsComplete, endValue);
		}  

		return waitForTests(testsStarted, testsComplete);
	}  

	private void startTestThread(final TestType testType, final CyclicBarrier testsStarted,
			final CountDownLatch testsComplete, final int endValue) {  

		Thread t = new Thread() {
			public void run() {  

				try {
					testsStarted.await();  

					switch (testType) {
					case SYNCHRONIZED:
						runSynchronizedTest(endValue);
						break;
					case VOLATILE:
						runVolatileTest(endValue);
						break;
					case ATOMIC:
						runAtomicTest(endValue);
						break;
					case LOCK:
						runLockTest(endValue);
						break;
					case FAIR_LOCK:
						runFairLockTest(endValue);
						break;
					}  

				} catch (Throwable t) {
					t.printStackTrace();
				} finally {  

					testsComplete.countDown();
				}  

			}
		};
		t.start();
	}  

	private long waitForTests(CyclicBarrier testsStarted, CountDownLatch testsComplete) throws Exception {  

		testsStarted.await();
		long startTime = System.currentTimeMillis();  

		testsComplete.await();
		long endTime = System.currentTimeMillis();
		reset();  

		return endTime - startTime;
	}  

	private void reset() {  

		synchronized (this) {
			synchronizedCounter = 0;
		}  

		volatileCounter = 0;
		atomicCounter.set(0);  

		lock.lock();
		try {
			lockCounter = 0;
		} finally {
			lock.unlock();
		}  

		fairLock.lock();
		try {
			fairLockCounter = 0;
		} finally {
			fairLock.unlock();
		}
	}  

	private void runSynchronizedTest(long endValue) {  

		boolean run = true;
		while (run) {
			run = incrementSynchronizedCounter(endValue);
		}
	}  

	private synchronized boolean incrementSynchronizedCounter(long endValue) {  

		return ++synchronizedCounter < endValue;
	}  

	private void runVolatileTest(long endValue) {
		boolean run = true;
		while (run) {
			run = ++volatileCounter < endValue;
		}
	}  

	private void runAtomicTest(long endValue) {
		boolean run = true;
		while (run) {
			run = atomicCounter.incrementAndGet() < endValue;
		}
	}  

	private void runLockTest(long endValue) {  

		boolean run = true;
		while (run) {
			lock.lock();
			try {
				run = ++lockCounter < endValue;
			} finally {
				lock.unlock();
			}
		}
	}  

	private void runFairLockTest(long endValue) {  

		boolean run = true;
		while (run) {
			fairLock.lock();
			try {
				run = ++fairLockCounter < endValue;
			} finally {
				fairLock.unlock();
			}
		}
	}
}

Share this Post

Related Blog Posts

JVM

Brief Introduction to REST

November 9th, 2011

This is from a guest lecture that I delivered to a web-programming class at Bethel University this past month.

Object Partners
JVM

Eclipse Service Release Adds Support for Java7

October 6th, 2011

Just last week the Eclipse group released a service release to their Indigo version, including support for Java 7.

Object Partners
JVM

Grails: Overriding the HTTP Method

September 29th, 2011

Grails gives you the option of overriding the HTTP method via an extra parameter(_method) or a custom HTTP Header(X-HTTP-Method-Override).

Object Partners

About the author