Java SE 5.0 introduced classes for locking called as Lock API. They are part of java.util.concurrent.locks package. Until then, only synchronized methods and synchronized blocks were available for locking.
A built-in lock called intrinstic lock or monitor lock is obtained on the object which is locked by synchronized method or block. This lock is acquired by the thread executing synchronized method/block and released when the thread leaves it. The intrinsic lock is released under the following scenarios:
- the synchronized method/block execution is completed normally
- there is an exception thrown which is not handled within the synchronized method/block
wait()
is called in the synchronized method/block
Limitations of intrinsic lock
The intrinstic lock acts as a mutex (mutally exclusive lock) where only one thread can run in the object’s monitor until the lock is released. There cannot be more than one owning thread in an intrinsic lock.
The intrinsic locks are mutually exclusive locks. In an object, we may have a property which can be read or modified concurrently by multiple threads. The threads which are reading the property cannot obtain a shared lock.
When an intrinsic lock is acquired by a thread on an object the other threads trying to access the object’s monitor will have to keep waiting until the executing thread releases the lock and leaves the object’s monitor. When a thread leaves the object’s monitor, the JVM can grant its lock to any other thread waiting for its access and not necessarily the oldest thread in the waiting state. In other words, there is no fairness guarentee on lock acquisition when synchronization is used. We can also say that intrinsic locks are unfair. The JVM guarentees that all threads waiting for access to the object’s monitor will be granted access before its shutdown but it does not guarentee the order of locking. Thus, intrinsic locks could lead to thread starvation.
There is no non-blocking way to obtain a lock using synchronization. The thread which wants to obtain a lock will have to keep waiting if the lock is being held by other thread. The requesting thread cannot check whether a lock is available and perform other operations if its unavailable.
When a thread acquires an intrinsic lock, it cannot be interrupted or the thread’s task cannot be cancelled before the thread leaves the intrinsic lock.
There is no way to obtain the collection of threads waiting to acquire the lock. We also cannot determine the number of threads waiting to acquire the lock.
Lock API
To overcome the limitations of intrinsic lock you can use the Lock API. This API provides Lock, ReadWriteLock
and Condition
interfaces and their implementing classes.
The wait()
allows only one thread to own the object’s monitor. If you need to use multiple wait-sets in a single object then you will have to use the Condition
object provided by the Lock
factory method newCondition()
. Read this example of bounded buffer implementation using multiple conditions.
The ReadWriteLock
implementing classes can be used by a Publisher-Subscriber framework where the publisher thread modifies a property and subscriber threads read it. Shared locks can be held by a subscriber threads instead of the mutually exclusive intrinsic lock for better concurrency. The ReentrantReadWriteLock
is the implementing class for ReadWriteLock
.
ReentrantLock
is an implementation of Lock
which provides reentrant mutually exclusive lock. If you want to grant locks to the threads in the same order they asked for it then you can create a fair lock. A fair lock can be created by using new ReentrantLock(true)
. However, use fair locks only when required as they reduce throughput in comparison with the unfair locks.
The Lock
interface provides a tryLock()
method with which a lock can be obtained in a non-blocking way. A thread can attempt to obtain a lock and it will be granted if it is not being held by any other thread. The thread requesting for a lock need not keep waiting for the lock. This is also called as polled lock. The lock obtained using tryLock()
is unfair. You can use the alternate tryLock(0, TimeUnit.SECONDS)
method to obtain a fair lock if the fairness is set to true using the ReentrantLock
constructor. In case of unfair locking, the current thread requesting for a lock using tryLock()
will acquire the lock even if other threads are waiting for the lock. In the case of fair locking, the current thread will not acquire the lock if other threads are waiting for it. This method returns true
if the thread obtains the lock on object’s monitor.
There is also a timed tryLock
method to specify the time limit for which a thread can wait to obtain the lock. This method considers the fairness setting.
The Lock
interface provides lockInterruptibly()
method with which the lock obtained by the executing thread will be released when the thread is interrupted. It allows a thread to stop execution and release the lock before completion of its task when interrupted.
The getQueuedThreads()
and getQueueLength()
methods of ReentrantLock can be used to get the collection of threads waiting to acquire the lock and to get the number of threads waiting to acquire the lock respectively.
Performance comparison of ReentrantLock and synchronized
The ReentrantLock
provides same semantics as synchronized method/block. In Java 5 JVM, the ReentrantLock
has better performance than synchronized under thread contention. However, in Java 6 there is no significant difference in the performance of ReentrantLock
and synchronized.
When should we use the Lock API?
The major drawback of ReentrantLock, ReentrantReadWriteLock.ReadLock
and ReentrantReadWriteLock.WriteLock
classes is that the lock needs to be explicitly released within the finally
block. A programmer may forget to release the lock when its use is over. The Lock API classes should be used only when you need to overcome the limitations of the built-in synchronized method/block. Only expert programmers should write the code which use these classes.