This article is about locks in Java. Here you can read about their usage, fairness policies and performance.
Locks are available since Java 1.5.
Interfaces
First of all, lets see at interfaces.
1 2 3 4 5 6 7 8 |
|
As we see, it prodives opportunity of acquiring lock by different ways:
- if we are using
lock()
and the lock is not available the current thread will be suspended until the lock will be released; lockInterruptible()
method acquires the lock until the current thread will be interrupted or lock will be released;tryLock()
method acquires the lock only if it is available at the time of invocation (non-blocking, not waiting for the lock will be released);- if we want to acquire the lock interruptibly with the specified waiting timeout we should use
tryLock(...)
method.
There is only one method for unlocking the lock: unlock()
and it works as it named.
Locks are more flexible and configurable alternative for synchronized
methods and statements, but we should remeber that
instead of synchronized, noone will unlock the lock for us if an exception occures or return statement invokes. So,
in most cases, the following idiom should be used:
1 2 3 4 5 6 7 |
|
1 2 3 4 |
|
ReadWriteLock is a pair of assotiated locks. First lock is used only for read-only operations (supports multiple locks at the same time) and the second lock is for write operations (exclusive lock). The read lock can be acquired only if the write lock is released. When we lock the write lock, it will wait for releasing of all read locks and then acquires. ReadWriteLock will improve performance in case of using multiple readers and much smaller number of writers, because many of readers will work at the same time.
Implementations
Java provides two implementations of these locks that we care about - ReentrantLock
and ReentrantReadWriteLock
.
As you see, both of them are reentrant. It means that a thread can acquire the same lock multiple times without any
issue. In fact reentrant locking increments special thread-personal counter (unlocking - decrements) and the lock will
be released only when counter reaches zero.
Each of lock’s implementations has additional methods that help us. Here are some of them:
- for
ReentrantLock
class:isHeldByCurrentThread()
returns true iff the lock is locked by the current thread;getHoldCount()
returns number (int) of locks on the lock by the current thread;getQueueLength()
returns an estimate number of threads that waits to acquire the lock;isLocked()
returns true if any thread holds this lock and false otherwise;
- for
ReentrantReadWriteLock
class:isWriteLocked()
returns true if someone holds the write lock;isWriteLockedByCurrentThread()
returns true if the lock is locked by the current thread;getReadHoldCount()
returns number (int) of locks on the read lock by the current thread.
Fairness
One of the interesting features of reentrant locks is fairness. When we are creating an instance of ReentrantLock or
ReentrantReadWriteLock we can pass fair flag to the constructor. The difference of fair and non-fair locks is in granting
access policy to threads that wait in the queue to lock the lock. The fair lock grants access to the longest-waiting thread,
so it look like FIFO (First-In-First-Out). A non-fair lock does not guarantee any particular access order.
Performance of fair locks is near to synchronized
block and non-fair locks is much faster than them.
By default, all locks are non-fair. Synchronized keyword and statement are non-fair, too. Fair locks have some disadvantages:
- performance degradation;
- fair locks do not guarantiee fair thread ordering in real world.
Micro-benchmarking in Java is not so good as we want. There are several things that can change results of tests:
- JVM warmup (the code becomes faster and faster while its working);
- Class loading (all application classes must be loaded);
- JIT compilation (JVM needs time to find hot parts of the code);
- GC (gc can happen while benchmarking and increase the time much).
Instead of this, I think that we can do some micro-benchmarks to check fair locks performance.
We have T threads, each of them will lock the lock, increment global counter, increment personal counter and unlock the lock.
Test will be ended when the global counter reaches N. So, all threads should have equal values of personal counters
(small deviation can be ignored) with fair lock and different values with non-fair lock. Lets see at average results of running
such micro-benchmark. I ran them for 50 times with N=1000000
and T=10
on my laptop (i3 330m 2.13GHz
, 8Gb RAM
, JDK 1.7.0
)
and aggregated results. ‘Deviation’ - this is how personal counter differs from the expected value (N/T
).
1 2 3 4 5 6 |
|
1 2 3 4 5 6 |
|
You can find sources of the test here. The results of test match javadoc’s warning about fair locks absolutly:
1 2 3 4 5 |
|
As we see “much slower” is up to 100 times slower!
So, we should remember about all features of locks and their configurations. I think that we can use ReentrantLock and ReentrantReadWriteLock without any indeterminacy now.