Synchronized vs. Lock vs. fair Lock
This page is dedicated to show an alarming change in the way Java 1.6 is handling synchronized on multi-core processors.
This can lead to severe starvation effects [3].
The problem
The problem I'm discussing is that several threads are trying to obtain the object monitor of the exact same resource, i.e.
synchronized(someObject) { // do something }
The assumption that multiple threads waiting for ownership of a monitor would receive the monitor in the order that they tried to acquire it ("first come, first serve") is simply incorrect.
This is documented behavior.
See the last paragraph of [1]:
"Likewise, no assumptions should be made about the order in which threads are granted ownership of a monitor or the order in which threads wake in response to the notify or notifyAll method. An excellent reference for these topics is Chapter 9, "Threads," in Joshua Bloch's book Effective Java Programming Language Guide. "
The documentation of Object.notifyAll() [2] states the following:
"[..] The awakened threads will compete in the usual manner with any other threads that might be actively competing to synchronize on this object; for example, the awakened threads enjoy no reliable privilege or disadvantage in being the next thread to lock this object."
The documentation in [5] also lists the following as a weak-spot of Built-in Synchronization in J2SE 1.4.x:
"No way to alter the semantics of a lock, for example, with respect to reentrancy, read versus write protection, or fairness."
Wrong assumptions…
I assumed that this wouldn't be a problem, that synchronized would be sufficiently fair even if it's not totally fair... but all that changed with Java 6 on multi-core systems!
Locking using synchronized has turned totally unfair!
Fairness of object monitor lock acquisition seems to be logical and a "good thing" but it's not specified that way, and for good reasons.
Without fairness in place, the VM can optimize the execution of an application much better. A context switch is a costly operation for a CPU so performance is increased significantly if such a switch can be omitted.
The solution
The only way to prevent this is the introduction of fairness into the locking process. The way to do this is to use ReentrantLock [4], or ReentrantReadWriteLock for that matter.
From the ReentrantLock javadoc:
"The constructor for this class accepts an optional fairness parameter. When set true, under contention, locks favor granting access to the longest-waiting thread. Otherwise this lock does not guarantee any particular access order. Programs using fair locks accessed by many threads may display lower overall throughput (i.e., are slower; often much slower) than those using the default setting, but have smaller variances in times to obtain locks and guarantee lack of starvation."
The proof
Not convinced yet? Think I'm hallucinating? Just test it out for yourself!
Download SynchronizedVsFairLock.java, execute
javac SynchronizedVsFairLock.java
followed by
java SynchronizedVsFairLock
and see the results with your own eyes.
For best results :p you should use a Java 1.5 javac and run the app on a multi-core system using both java 1.5 and 1.6.
Some results, for your convenience…
iMac, 2.4GHz Intel Core 2 Duo, i.e. multi-core
Environment: java.runtime.name = Java(TM) SE Runtime Environment java.runtime.version = 1.6.0_07-b06-153 java.vendor = Apple Inc. java.version = 1.6.0_07 java.vm.name = Java HotSpot(TM) 64-Bit Server VM java.vm.info = mixed mode os.name = Mac OS X os.version = 10.5.7 os.arch = x86_64 ########################################## About to execute usingSynchronized... Results for usingSynchronized: runnables[0]: SynchronizedRunnable[counter=394, running=false] runnables[1]: SynchronizedRunnable[counter=1, running=false] runnables[2]: SynchronizedRunnable[counter=1, running=false] runnables[3]: SynchronizedRunnable[counter=1, running=false] runnables[4]: SynchronizedRunnable[counter=1, running=false] runnables[5]: SynchronizedRunnable[counter=1, running=false] runnables[6]: SynchronizedRunnable[counter=1, running=false] runnables[7]: SynchronizedRunnable[counter=1, running=false] runnables[8]: SynchronizedRunnable[counter=1, running=false] runnables[9]: SynchronizedRunnable[counter=605, running=false] ########################################## About to execute usingUnfairLock... Results for usingUnfairLock: runnables[0]: LockRunnable[counter=997, running=false] runnables[1]: LockRunnable[counter=1, running=false] runnables[2]: LockRunnable[counter=1, running=false] runnables[3]: LockRunnable[counter=1, running=false] runnables[4]: LockRunnable[counter=1, running=false] runnables[5]: LockRunnable[counter=1, running=false] runnables[6]: LockRunnable[counter=1, running=false] runnables[7]: LockRunnable[counter=1, running=false] runnables[8]: LockRunnable[counter=1, running=false] runnables[9]: LockRunnable[counter=1, running=false] ########################################## About to execute usingFairLock... Results for usingFairLock: runnables[0]: LockRunnable[counter=101, running=false] runnables[1]: LockRunnable[counter=101, running=false] runnables[2]: LockRunnable[counter=101, running=false] runnables[3]: LockRunnable[counter=101, running=false] runnables[4]: LockRunnable[counter=100, running=false] runnables[5]: LockRunnable[counter=100, running=false] runnables[6]: LockRunnable[counter=100, running=false] runnables[7]: LockRunnable[counter=100, running=false] runnables[8]: LockRunnable[counter=100, running=false] runnables[9]: LockRunnable[counter=100, running=false] ##########################################
Environment: java.runtime.name = Java(TM) 2 Runtime Environment, Standard Edition java.runtime.version = 1.5.0_16-b06-284 java.vendor = Apple Inc. java.version = 1.5.0_16 java.vm.name = Java HotSpot(TM) Client VM java.vm.info = mixed mode, sharing os.name = Mac OS X os.version = 10.5.7 os.arch = i386 ########################################## About to execute usingSynchronized... Results for usingSynchronized: runnables[0]: SynchronizedRunnable[counter=101, running=false] runnables[1]: SynchronizedRunnable[counter=101, running=false] runnables[2]: SynchronizedRunnable[counter=101, running=false] runnables[3]: SynchronizedRunnable[counter=101, running=false] runnables[4]: SynchronizedRunnable[counter=101, running=false] runnables[5]: SynchronizedRunnable[counter=101, running=false] runnables[6]: SynchronizedRunnable[counter=100, running=false] runnables[7]: SynchronizedRunnable[counter=100, running=false] runnables[8]: SynchronizedRunnable[counter=100, running=false] runnables[9]: SynchronizedRunnable[counter=100, running=false] ########################################## About to execute usingUnfairLock... Results for usingUnfairLock: runnables[0]: LockRunnable[counter=485, running=false] runnables[1]: LockRunnable[counter=495, running=false] runnables[2]: LockRunnable[counter=19, running=false] runnables[3]: LockRunnable[counter=1, running=false] runnables[4]: LockRunnable[counter=1, running=false] runnables[5]: LockRunnable[counter=1, running=false] runnables[6]: LockRunnable[counter=1, running=false] runnables[7]: LockRunnable[counter=1, running=false] runnables[8]: LockRunnable[counter=1, running=false] runnables[9]: LockRunnable[counter=1, running=false] ########################################## About to execute usingFairLock... Results for usingFairLock: runnables[0]: LockRunnable[counter=101, running=false] runnables[1]: LockRunnable[counter=101, running=false] runnables[2]: LockRunnable[counter=101, running=false] runnables[3]: LockRunnable[counter=101, running=false] runnables[4]: LockRunnable[counter=101, running=false] runnables[5]: LockRunnable[counter=100, running=false] runnables[6]: LockRunnable[counter=100, running=false] runnables[7]: LockRunnable[counter=100, running=false] runnables[8]: LockRunnable[counter=100, running=false] runnables[9]: LockRunnable[counter=100, running=false] ##########################################
IBM ThinkPad, Intel Pentium M processor 1.86GHz, i.e. single-core
Environment: java.runtime.name = Java(TM) SE Runtime Environment java.runtime.version = 1.6.0_13-b03 java.vendor = Sun Microsystems Inc. java.version = 1.6.0_13 java.vm.name = Java HotSpot(TM) Client VM java.vm.info = mixed mode, sharing os.name = Windows XP os.version = 5.1 os.arch = x86 ########################################## About to execute usingSynchronized... Results for usingSynchronized: runnables[0]: SynchronizedRunnable[counter=6, running=false] runnables[1]: SynchronizedRunnable[counter=11, running=false] runnables[2]: SynchronizedRunnable[counter=85, running=false] runnables[3]: SynchronizedRunnable[counter=70, running=false] runnables[4]: SynchronizedRunnable[counter=75, running=false] runnables[5]: SynchronizedRunnable[counter=73, running=false] runnables[6]: SynchronizedRunnable[counter=74, running=false] runnables[7]: SynchronizedRunnable[counter=88, running=false] runnables[8]: SynchronizedRunnable[counter=72, running=false] runnables[9]: SynchronizedRunnable[counter=90, running=false] ########################################## About to execute usingUnfairLock... Results for usingUnfairLock: runnables[0]: LockRunnable[counter=64, running=false] runnables[1]: LockRunnable[counter=87, running=false] runnables[2]: LockRunnable[counter=60, running=false] runnables[3]: LockRunnable[counter=60, running=false] runnables[4]: LockRunnable[counter=76, running=false] runnables[5]: LockRunnable[counter=54, running=false] runnables[6]: LockRunnable[counter=54, running=false] runnables[7]: LockRunnable[counter=71, running=false] runnables[8]: LockRunnable[counter=56, running=false] runnables[9]: LockRunnable[counter=67, running=false] ########################################## About to execute usingFairLock... Results for usingFairLock: runnables[0]: LockRunnable[counter=65, running=false] runnables[1]: LockRunnable[counter=65, running=false] runnables[2]: LockRunnable[counter=65, running=false] runnables[3]: LockRunnable[counter=65, running=false] runnables[4]: LockRunnable[counter=65, running=false] runnables[5]: LockRunnable[counter=65, running=false] runnables[6]: LockRunnable[counter=65, running=false] runnables[7]: LockRunnable[counter=64, running=false] runnables[8]: LockRunnable[counter=64, running=false] runnables[9]: LockRunnable[counter=64, running=false] ##########################################
Environment: java.runtime.name = Java(TM) 2 Runtime Environment, Standard Edition java.runtime.version = 1.5.0_13-b05 java.vendor = Sun Microsystems Inc. java.version = 1.5.0_13 java.vm.name = Java HotSpot(TM) Client VM java.vm.info = mixed mode, sharing os.name = Windows XP os.version = 5.1 os.arch = x86 ########################################## About to execute usingSynchronized... Results for usingSynchronized: runnables[0]: SynchronizedRunnable[counter=65, running=false] runnables[1]: SynchronizedRunnable[counter=65, running=false] runnables[2]: SynchronizedRunnable[counter=65, running=false] runnables[3]: SynchronizedRunnable[counter=65, running=false] runnables[4]: SynchronizedRunnable[counter=65, running=false] runnables[5]: SynchronizedRunnable[counter=65, running=false] runnables[6]: SynchronizedRunnable[counter=65, running=false] runnables[7]: SynchronizedRunnable[counter=64, running=false] runnables[8]: SynchronizedRunnable[counter=64, running=false] runnables[9]: SynchronizedRunnable[counter=64, running=false] ########################################## About to execute usingUnfairLock... Results for usingUnfairLock: runnables[0]: LockRunnable[counter=50, running=false] runnables[1]: LockRunnable[counter=86, running=false] runnables[2]: LockRunnable[counter=63, running=false] runnables[3]: LockRunnable[counter=54, running=false] runnables[4]: LockRunnable[counter=72, running=false] runnables[5]: LockRunnable[counter=72, running=false] runnables[6]: LockRunnable[counter=54, running=false] runnables[7]: LockRunnable[counter=72, running=false] runnables[8]: LockRunnable[counter=70, running=false] runnables[9]: LockRunnable[counter=57, running=false] ########################################## About to execute usingFairLock... Results for usingFairLock: runnables[0]: LockRunnable[counter=65, running=false] runnables[1]: LockRunnable[counter=65, running=false] runnables[2]: LockRunnable[counter=65, running=false] runnables[3]: LockRunnable[counter=65, running=false] runnables[4]: LockRunnable[counter=64, running=false] runnables[5]: LockRunnable[counter=64, running=false] runnables[6]: LockRunnable[counter=64, running=false] runnables[7]: LockRunnable[counter=64, running=false] runnables[8]: LockRunnable[counter=64, running=false] runnables[9]: LockRunnable[counter=64, running=false] ##########################################
Conclusion
synchronized was fair on 1.5, is a bit unfair on 1.6 single-core and very unfair on 1.6 multi-core.
Interestingly, unfair locks are even unfairer than synchronized on a 1.6 multi-core system while being as unfair as synchronized using 1.6 in case of 1.5 on a multi-core system. This means that the starvation effect was already present on 1.5 multi-core systems in case of locks while synchronized was still handled fairly.
On single-core systems, everything seems to be ok but isn't.
See Ceki Gülcüs Blog post on the topic: Biased Locking in Java SE 6.0 [6]
References
- [1] http://java.sun.com/j2se/1.5.0/docs/guide/vm/thread-priorities.html
- [2] http://java.sun.com/javase/6/docs/api/java/lang/Object.html#notifyAll()
- [3] http://en.wikipedia.org/wiki/Starvation_(computing)
- [4] http://java.sun.com/javase/6/docs/api/java/util/concurrent/locks/ReentrantLock.html
- [5] http://java.sun.com/developer/technicalArticles/J2SE/concurrency/
- [6] http://ceki.blogspot.com/2009/06/biased-locking-in-java-se-60.html
Related links
- Java SE 6 Performance White Paper
- Biased Locking in HotSpot, an article by David Dice.
- Synchronization in Java SE 6, a PDF containing info on the topic, by David Dice.
- LBCORE-97 - Starvation on AppenderBase.doAppend
- Starvation and Livelock (The Java Tutorials - Essential Classes - Concurrency)
- http://tutorials.jenkov.com/java-concurrency/starvation-and-fairness.html
- http://tutorials.jenkov.com/java-concurrency/locks.html