What are Reentrant Locks?
In Java 5.0 a new addition was made to enhance the intrinsic locking capabilities, called as Reentrant Lock. Prior to this, ‘synchronized’ and ‘volatile’ were the means for achieving concurrency.
public synchronized void doAtomicTransfer(){ //enter synchronized block , acquire lock over this object. operation1() operation2(); } // exiting synchronized block, release lock over this object.
Synchronized uses intrinsic locks or monitors. Every object in Java has an intrinsic lock associated with it. Whenever a thread tries to access a synchronized block or method it acquires the intrinsic lock or the monitor on that object. In case of static methods, the thread acquires the lock over the class object.
Intrinsic locking mechanism is a clean approach in terms of writing code and is pretty good for most of the use-cases. So why do we need additional feature of Explicit Locks? Lets discuss.
Intrinsic locking mechanism can have some functional limitations like:
- It is not possible to interrupt a thread waiting to acquire a lock. ( lock Interruptibly)
- It is not possible to attempt to acquire lock without being willing to wait for it forever. (try lock )
- Can not implement non-block-structured locking disciplines, as intrinsic locks must be released in the same block in which they are acquired.
Aside from that, ReentrantLock supports lock polling and interruptible lock waits that support time-out. ReentrantLock also has support for configurable fairness policy, allowing more flexible thread scheduling.
Lets see few of the methods implemented by ReentrantLock class (which implements Lock):
void lock(); void lockInterruptibly() throws InterruptedException; boolean tryLock(); boolean tryLock(long time, TimeUnit unit) throws InterruptedException; .....
Lets try understand the use of these and see what benefits we can get.
- Polled and timed lock acquisition :
Let see the example code :
public void transferMoneyWithSync(Account fromAccount, Account toAccount, float amount) throws InsufficientAmountException { synchronized (fromAccount) { // acquired lock on fromAccount Object synchronized (toAccount) { // acquired lock on toAccount Object if (amount > fromAccount.getCurrentAmount()) { throw new InsufficientAmountException( "Insufficient Balance"); } else { fromAccount.debit(amount); toAccount.credit(amount); } } } }
In transferMoney() method above there is a possibility of deadlock when 2 threads A and B are trying to transfer money at almost the same time.
A: transferMoney(acc1, acc2, 20); B: transferMoney(acc2, acc1 ,25);
It is possible that thread A has acquired lock on acc1 object and is waiting to acquire lock on acc2 object, meanwhile thread B has acquired lock on acc2 object and is waiting for lock on acc1. This will lead to deadlock and the system would have to be restarted!!
There is however a way to avoid this, which is called “lock ordering”, personally, I find this a bit complex.
A cleaner approach is implemented by ReentrantLock with the use of tryLock() method. This approach is called “timed and polled lock-acquisition”. It lets you regain control if you cannot acquire all the required locks, release the ones you have acquired and retry.
So, using tryLock we will attempt to acquire both locks, if we can not attain both, release if one of these have been acquired, then retry.
public boolean transferMoneyWithTryLock(Account fromAccount, Account toAccount, float amount) throws InsufficientAmountException, InterruptedException { // we are defining a stopTime long stopTime = System.nanoTime() + 5000; while (true) { if (fromAccount.lock.tryLock()) { try { if (toAccount.lock.tryLock()) { try { if (amount > fromAccount.getCurrentAmount()) { throw new InsufficientAmountException( "Insufficient Balance"); } else { fromAccount.debit(amount); toAccount.credit(amount); } } finally { toAccount.lock.unlock(); } } } finally { fromAccount.lock.unlock(); } } if(System.nanoTime() < stopTime) return false; Thread.sleep(100); }//while }
Here we implemented a timed lock, so, if the locks can not be acquired within the specified time transferMoney method will return a failure notice and exit gracefully.
We can also maintain time budget activities using this concept.
- Interruptible lock acquisition:
Interruptible lock acquisition allows locking to be used within cancellable activities.
The lockInterruptibly method allows us to try and acquire lock while being available for interruption. So basically it means that; it allows the thread to immediately react to the interrupt signal sent to it from another thread.
This can be helpful when we want to send a KILL signal to all the waiting locks.Lets see one example, suppose we have a shared line to send messages, we would want to design it in way that if another thread comes and interrupts the current thread should release the lock and perform the exit or shut down operations to cancel the current task.
public boolean sendOnSharedLine(String message) throws InterruptedException{ lock.lockInterruptibly(); try{ return cancellableSendOnSharedLine(message); } finally { lock.unlock(); } } private boolean cancellableSendOnSharedLine(String message){ .......
The timed tryLock is also responsive to interruption.
- Non-block structured locking:
In intrinsic locks, acquire-release pairs are block-structured, i.e a lock is always released in the same basic block in which it was acquired, regardless of how control exits the block.
Extrinsic locks provide the facility to have more explicit control.
Some concepts like Lock Strapping can be acheived more easily using extrinsic locks. Some use cases are seen in hash bashed collections and linked lists. - Fairness:
The ReentrantLock constructor offers a choice of two fairness options :create a nonfair lock or a fair lock. With fair locking threads can acquire locks only in the order in which they requested, whereas an unfair lock allows a lock to acquire it out of its turn, this is called barging (breaking the queue and acquiring the lock when it became available).
Fair locking has a significant performance cost because of overhead of suspending and resuming threads. There could be cases where there is a significant delay between when a suspended thread is resumed and when it actually runs. Lets see a situation :
A -> holds lock B -> has requested and is in suspended state waiting for A to release lock C -> requests the lock at the same time when A releases the lock, C has not yet gone to suspended state.
As C has not yet gone in suspended state there is a chance that it can acquire the lock released by A, use it, and release it before B even finishes waking up. So, in this context unfair lock has significant performance advantage.
Intrinsic locks and Extrinsic locks have the same mechanism inside for locking so the performance improvement is purely subjective. It depends on the usecases we discussed above. Extrinsic locks give more explicit control mechanism for better handling of deadlocks, starvation etc. More examples we will see in the coming blogs.