Engineering Full Stack Apps with Java and JavaScript
Java concurrency package give you the ability to use explicit locks. With explicit locks, you can see if a lock is available and acquire only if it is available. This way you can avoid deadlocks. We already wrote a deadlock simulation program without using explicit locks and now we will write the same program using explicit locks. This is just a rewrite and hence will also deadlock like hte older one.
We will use the Lock interface and its implementation ReentrantLock for our example. Both these belong to the package java.util.concurrent.locks. Reentrant lock behaves similar to the intrinsic locking mechanism using synchronized keyword when we use its lock and unlock methods. Calling lock on a ReentrantLock implementation can be compared to entering a synchronized region and calling unlock can be compared to exiting a synchronized region. Since calling the unlock is an explicit and important task, it is always a good practice to have it inside a finally block. We will see explicit locks in detail later, but for now refer to the java doc for any API related doubts.
package deadlock;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class DeadLockExampleExplicitLock {
final static Lock lock1 = new ReentrantLock();
final static Lock lock2 = new ReentrantLock();
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable3());
t1.setName("Thread A");
t1.start();
Thread t2 = new Thread(new Runnable4());
t2.setName("Thread B");
t2.start();
}
}
class Runnable3 implements Runnable
{
public void run() {
DeadLockExampleExplicitLock.lock1.lock();
try {
System.out.println(Thread.currentThread().getName()
+ ": Got lockObject1. Trying for lockObject2");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
DeadLockExampleExplicitLock.lock2.lock();
try {
System.out.println(Thread.currentThread().getName()
+ ": Got lockObject2.");
} finally {
DeadLockExampleExplicitLock.lock2.unlock();
}
} finally {
DeadLockExampleExplicitLock.lock1.unlock();
}
}
}
class Runnable4 implements Runnable
{
public void run()
{
DeadLockExampleExplicitLock.lock2.lock();
try {
System.out.println(Thread.currentThread().getName()
+ ": Got lockObject2. Trying for lockObject1");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
DeadLockExampleExplicitLock.lock1.lock();
try {
System.out.println(Thread.currentThread().getName()
+ ": Got lockObject1.");
} finally {
DeadLockExampleExplicitLock.lock1.unlock();
}
} finally {
DeadLockExampleExplicitLock.lock2.unlock();
}
}
}
This program will deadlock after printing below statements:
Thread A: Got lockObject1. Trying for lockObject2
Thread B: Got lockObject2. Trying for lockObject1
Lock interface also has a method called tryLock which will acquire the lock only if it is free at the time of invocation. We can make use of the tryLock feature to avoid deadlock. We will need to try acquiring the lock within a loop and release locks if we can’t acquire both locks. We are avoiding the necessary condition of hold and wait to prevent deadlock here. We will also need to introduce some random timing between tries to avoid an infinite loop.