Java Example - Deadlock and Solution
Deadlock is a situation where multiple threads are blocked, with one or all of them waiting for a resource to be released. Since the threads are blocked indefinitely, the program cannot terminate normally.
The four necessary conditions for a Java deadlock to occur are:
- Mutual exclusion: When a resource is used (occupied) by a thread, other threads cannot use it.
- No preemption: Resource requesters cannot forcibly take resources from resource holders; resources can only be released voluntarily by the resource holders.
- Hold and wait: A resource requester holds the original resource while requesting additional resources.
- Circular wait: There exists a waiting queue: P1 holds P2's resource, P2 holds P3's resource, P3 holds P1's resource. This forms a waiting loop.
When all four conditions are met, a deadlock occurs. However, breaking any of these conditions can resolve the deadlock. Below is a Java code example to simulate deadlock.
The methods to solve the deadlock problem are: one is using synchronized, and the other is using Lock to explicitly lock.
Improper use of locks, especially when locking multiple objects simultaneously, can lead to deadlock, as shown below:
LockTest.java File
import java.util.Date;
public class LockTest {
public static String obj1 = "obj1";
public static String obj2 = "obj2";
public static void main(String[] args) {
LockA la = new LockA();
new Thread(la).start();
LockB lb = new LockB();
new Thread(lb).start();
}
}
class LockA implements Runnable{
public void run() {
try {
System.out.println(new Date().toString() + " LockA 开始执行");
while(true){
synchronized (LockTest.obj1) {
System.out.println(new Date().toString() + " LockA 锁住 obj1");
Thread.sleep(3000); // This wait gives B a chance to lock
synchronized (LockTest.obj2) {
System.out.println(new Date().toString() + " LockA 锁住 obj2");
Thread.sleep(60 * 1000); // For testing purposes, holding the lock
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
class LockB implements Runnable{
public void run() {
try {
System.out.println(new Date().toString() + " LockB 开始执行");
while(true){
synchronized (LockTest.obj2) {
System.out.println(new Date().toString() + " LockB 锁住 obj2");
Thread.sleep(3000); // This wait gives A a chance to lock
synchronized (LockTest.obj1) {
System.out.println(new Date().toString() + " LockB 锁住 obj1");
Thread.sleep(60 * 1000); // For testing purposes, holding the lock
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
The output of the code above is:
Tue May 05 10:51:06 CST 2015 LockB started execution
Tue May 05 10:51:06 CST 2015 LockA started execution
Tue May 05 10:51:06 CST 2015 LockB locked obj2
Tue May 05 10:51:06 CST 2015 LockA locked obj1
At this point, a deadlock occurs.
To solve this problem, we avoid explicit unlocking and use semaphores to control access.
Semaphores can control how many threads can access a resource; here, we specify that only one thread can access, achieving a lock-like effect. Semaphores can also specify a timeout for acquiring access, allowing for additional handling based on this timeout.
For unsuccessful acquisition attempts, one typically retries or specifies a number of attempts before exiting immediately.
Here is the code:
UnLockTest.java file
import java.util.Date;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
public class UnLockTest {
public static String obj1 = "obj1";
public static final Semaphore a1 = new Semaphore(1);
public static String obj2 = "obj2";
public static final Semaphore a2 = new Semaphore(1);
public static void main(String[] args) {
LockAa la = new LockAa();
new Thread(la).start();
LockBb lb = new LockBb();
new Thread(lb).start();
}
}
class LockAa implements Runnable {
public void run() {
try {
System.out.println(new Date().toString() + " LockA started execution");
while (true) {
if (UnLockTest.a1.tryAcquire(1, TimeUnit.SECONDS)) {
System.out.println(new Date().toString() + " LockA locked obj1");
if (UnLockTest.a2.tryAcquire(1, TimeUnit.SECONDS)) {
System.out.println(new Date().toString() + " LockA locked obj2");
Thread.sleep(60 * 1000); // do something
} else {
System.out.println(new Date().toString() + " LockA failed to lock obj2");
}
} else {
System.out.println(new Date().toString() + " LockA failed to lock obj1");
}
UnLockTest.a1.release(); // release
UnLockTest.a2.release();
Thread.sleep(1000); // immediately retry, in real scenarios the action is uncertain
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
class LockBb implements Runnable {
public void run() {
try {
System.out.println(new Date().toString() + " LockB started execution");
while (true) {
if (UnLockTest.a2.tryAcquire(1, TimeUnit.SECONDS)) {
System.out.println(new Date().toString() + " LockB has locked obj2");
if (UnLockTest.a1.tryAcquire(1, TimeUnit.SECONDS)) {
System.out.println(new Date().toString() + " LockB has locked obj1");
Thread.sleep(60 * 1000); // do something
} else {
System.out.println(new Date().toString() + " LockB failed to lock obj1");
}
} else {
System.out.println(new Date().toString() + " LockB failed to lock obj2");
}
UnLockTest.a1.release(); // release
UnLockTest.a2.release();
Thread.sleep(10 * 1000); // This is just for demonstration, so tryAcquire is set to 1 second, and B gives A a chance to execute, otherwise it would always be deadlock
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
The example code outputs:
Tue May 05 10:59:13 CST 2015 LockA starts executing
Tue May 05 10:59:13 CST 2015 LockB starts executing
Tue May 05 10:59:13 CST 2015 LockB has locked obj2
Tue May 05 10:59:13 CST 2015 LockA has locked obj1
Tue May 05 10:59:14 CST 2015 LockB failed to lock obj1
Tue May 05 10:59:14 CST 2015 LockA failed to lock obj2
Tue May 05 10:59:15 CST 2015 LockA has locked obj1
Tue May 05 10:59:15 CST 2015 LockA has locked obj2
Java Examples ```