Hello readers! Welcome to another blog in the Java concurrency series. Today, we are going to look at the Reentrant Locks in Java. What it is, how to use it, and mainly, what is the difference between using Reentrant Locks and synchronization. So, let’s dive straight into it.
Reentrant Locks
Reentrant locks work just like the synchronized locks in java, but have much more facilities and extended capabilities. Let’s look at some example code.
import java.util.concurrent.locks.ReentrantLock; | |
public class ReentrantLockDemo { | |
private static ReentrantLock reentrantLock = new ReentrantLock(); | |
public static void main(String[] args) throws InterruptedException { | |
ReentrantLockDemo reentrantLockDemo = new ReentrantLockDemo(); | |
Thread t1 = new Thread(new Runnable() { | |
@Override | |
public void run() { | |
reentrantLockDemo.checkSync(); | |
} | |
}); | |
Thread t2 = new Thread(new Runnable() { | |
@Override | |
public void run() { | |
reentrantLockDemo.checkSync(); | |
} | |
}); | |
t1.start(); | |
t2.start(); | |
t1.join(); | |
t2.join(); | |
System.out.println("Finished.."); | |
} | |
private void checkSync() { | |
reentrantLock.lock(); | |
System.out.println("Acquired.." + reentrantLock.toString()); | |
try { | |
Thread.sleep(1000); | |
System.out.println("Done.."); | |
} catch (InterruptedException e) { | |
e.printStackTrace(); | |
} | |
reentrantLock.unlock(); | |
} | |
} |
In the above example, we have created one reentrant lock object as reentrantLock. We have created two threads that run the same method, checkSync, which is doing nothing but taking some time to complete. In that method, we have first of all acquired a lock for whichever thread asks for it first. For acquiring the lock, we have used the above reentrantLock object that we had defined before. Once a lock has been acquired by one thread, all the other threads trying to acquire the same lock have to wait till the thread, that has the lock, has unlocked it, which is done by calling the unlock() method of the Reentrant lock.
As you can see, it is very similar to using the synchronized way of synchronization. But, instead of defining the method body in a block of synchronized, we can just have our body in between lock() and unlock() calls. Moreover, we can also pass the reentrant object to different methods, thus allowing us to have lock() and unlock() calls in different methods.
Be careful that the code between lock() and unlock() methods can throw exception, and if it did, then without a finally block, we will not be able to unlock the lock, thus never releasing the lock. Other waiting threads will never be able to acquire the lock ever, thus making our application stuck forever! So, always keep your method body in a try block when acquiring a lock, and call unlock() in finally block.
Let’s go ahead and look at some awesome features of Reentrant locks.
Fair Ordering Policy
Reentrant lock has a fair ordering policy. According to this, a lock access will be always granted to the thread that has waited for the longest. To enable the fair ordering policy, you will need to pass in true boolean value when constructing the ReentrantLock. Below is the example for the same.
import java.util.concurrent.locks.ReentrantLock; | |
public class ReentrantLockFairnessDemo { | |
private static ReentrantLock reentrantLock = new ReentrantLock(true); | |
public static void main(String[] args) throws InterruptedException { | |
ReentrantLockFairnessDemo reentrantLockDemo = new ReentrantLockFairnessDemo(); | |
Thread t1 = new Thread(new Runnable() { | |
@Override | |
public void run() { | |
reentrantLockDemo.checkSync(); | |
} | |
}); | |
Thread t2 = new Thread(new Runnable() { | |
@Override | |
public void run() { | |
reentrantLockDemo.checkSync(); | |
} | |
}); | |
Thread t3 = new Thread(new Runnable() { | |
@Override | |
public void run() { | |
reentrantLockDemo.checkSync(); | |
} | |
}); | |
t1.start(); | |
Thread.sleep(1000); | |
t2.start(); | |
Thread.sleep(1000); | |
t3.start(); | |
t1.join(); | |
t2.join(); | |
t3.join(); | |
System.out.println("Finished.."); | |
} | |
private void checkSync() { | |
reentrantLock.lock(); | |
System.out.println("Acquired.." + reentrantLock.toString()); | |
try { | |
Thread.sleep(5000); | |
System.out.println("Done.."); | |
} catch (InterruptedException e) { | |
e.printStackTrace(); | |
} | |
reentrantLock.unlock(); | |
} | |
} |
tryLock
tryLock is another feature of Reentrant lock. It tries to acquire the target lock. If the target lock is free, then the lock is handed over to the requesting thread and tryLock() returns true signifying that the lock has been handed over. Else, it immediately returns false.
Another version of tryLock() exists which takes in a timeout value and timeout unit as method parameters. It tries to acquire the target lock. If the target lock is free, then it works normally as explained above. But, if the resource is not free, then it waits until the timeout value to see if the resource becomes free and acquires it. If the resource doesn’t become free even after the timeout value, then it returns false without acquiring the lock.
lockInterruptibly
lockInterruptibly is a very powerful feature of Reentrant Locks. It, as any other lock method, tries to acquire a lock. If the lock is free, then it returns immediately and acquires the lock. Else, it waits for the lock to be free. Here, while waiting, if the thread is interrupted by another thread, then the lock will not be given to the thread. Instead, it will immediately throw an InterruptedException. Below is the code for the same.
import java.util.concurrent.locks.ReentrantLock; | |
public class ReentrantLockInterruptiblyDemo { | |
private static ReentrantLock reentrantLock = new ReentrantLock(); | |
public static void main(String[] args) throws InterruptedException { | |
ReentrantLockInterruptiblyDemo reentrantLockDemo = new ReentrantLockInterruptiblyDemo(); | |
Thread t1 = new Thread(new Runnable() { | |
@Override | |
public void run() { | |
reentrantLock.lock(); | |
reentrantLockDemo.checkSync(); | |
reentrantLock.unlock(); | |
} | |
}); | |
Thread t2 = new Thread(new Runnable() { | |
@Override | |
public void run() { | |
System.out.println("Waiting on thread 2."); | |
reentrantLock.lock(); | |
System.out.println("Acquiring from thread 2."); | |
reentrantLockDemo.checkSync(); | |
reentrantLock.unlock(); | |
} | |
}); | |
Thread t3 = new Thread(new Runnable() { | |
@Override | |
public void run() { | |
try { | |
System.out.println("Waiting on thread 3."); | |
reentrantLock.lockInterruptibly(); | |
System.out.println("Acquiring from thread 3."); | |
reentrantLockDemo.checkSync(); | |
reentrantLock.unlock(); | |
} catch (InterruptedException e) { | |
e.printStackTrace(); | |
} | |
} | |
}); | |
System.out.println("I am thread 1: " + t1.getName()); | |
System.out.println("I am thread 2: " + t2.getName()); | |
System.out.println("I am thread 3: " + t3.getName()); | |
t1.start(); | |
Thread.sleep(500); | |
t2.start(); | |
t3.start(); | |
// t2.interrupt(); | |
t3.interrupt(); | |
System.out.println("Interrupted."); | |
t1.join(); | |
t2.join(); | |
t3.join(); | |
System.out.println(reentrantLock.toString()); | |
System.out.println("Finished.."); | |
} | |
private void checkSync() { | |
System.out.println("Acquired.." + reentrantLock.toString()); | |
long count = 0; | |
for (int i=0; i< Integer.MAX_VALUE ; i++) { | |
count += i; | |
} | |
System.out.println("Done.."); | |
} | |
} |
In the above code, we have created three threads, and t3 is calling lockInterruptibly() while the other two are calling lock(). Here, t1 will successfully acquire the lock and proceed with it’s execution. Then, t2 and t3 will try to acquire the lock and will wait until the lock is free. Then, we have interrupted t3. This means, that when t3 will get a chance at the lock, it will throw an InterruptedException without getting the hold of the lock. t2 will proceed with it’s execution. Here, I have commented out t2.interrupt(). You can comment that in and comment out t3.interrupt(). Once you do this change and run the program, you will notice that t2 is still able to acquire the lock and perform it’s execution, although it was in interrupted state. This is the difference between lock and lockInterruptibly.
That was all for Reentrant Locks. Hope you found this blog helpful and learnt something new today. Thanks for reading and happy blogging!