A livelock in Java will force two or more threads to enter an infinite loop and, therefore, stop their execution.
How Does a Livelock Work?
A livelock is similar to a deadlock but with a piece of extra logic that will release the lock using a timer, but the result is the same.
The image below shows a livelock in action:
T1 successfully locked (or synchronized) resource A and is waiting for resource B to be unlocked. Meanwhile, T2 successfully locked (or synchronized) resource B and is waiting for resource A to be unlocked.
After N milliseconds, this loop is produced:
T1rolls back and starts over with resourceBT2rolls back and starts over with resourceA- The situation repeats over and over.
While the thread's execution is never truly blocked indefinitely, the program cannot move forward since the threads in a livelock enter into an infinite loop of repeated execution.
Take a look at the code below, which creates this livelock in a program.
import java.util.concurrent.*;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class LiveLock {
// Create two Locks
// Lock functionality is similar to synchronized block,
// but it provides explicit lock/unlock methods
private static Lock lock1 = new ReentrantLock(true);
private static Lock lock2 = new ReentrantLock(true);
public static void main(String[] args) {
new Thread(new A(lock1, lock2), "A").start();
new Thread(new B(lock1, lock2), "B").start();
}
}
class A implements Runnable {
// Keep references on resource
private final Lock lock1;
private final Lock lock2;
public A(Lock lock1, Lock lock2) {
this.lock1 = lock1;
this.lock2 = lock2;
}
@Override
public void run() {
System.out.println("Instance of class A run()");
while (true) {
try {
// System.out.println("Instance of class A acquiring lock1");
// Attempt to acquire the lock with a timeout of 50 milliseconds
// the timeout ensures that the attempts will
// be stopped in 50 milliseconds
// Attention!!! It is the first part that
// demonstrates the Live lock behavior
lock1.tryLock(50, TimeUnit.MILLISECONDS);
System.out.println("Instance of class A acquired lock1");
// Sleep symbolizes some work that the thread should be doing
System.out.println("Instance of class A doing some work");
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
//System.out.println("Instance of class A acquiring lock2");
if (lock2.tryLock()) {
System.out.println("Instance of class A acquired lock2");
} else {
// Attention!!! It is the second part that
// demonstrates the Live lock behavior
System.out.println("Instance of class A cannot "
+ "acquire lock2, releasing lock1.");
lock1.unlock();
// Continue to the next iteration of the loop
continue;
}
// Never happens
System.out.println("Instance of class A doing "
+ "some work, with lock1 and lock2");
// breaks the loop
break;
}
// Never happens
// Unlock both locks
lock2.unlock();
lock1.unlock();
}
}
class B implements Runnable {
// Keep references on resource
private final Lock lock1;
private final Lock lock2;
public B(Lock lock1, Lock lock2) {
this.lock1 = lock1;
this.lock2 = lock2;
}
@Override
public void run() {
while (true) {
try {
// System.out.println("Instance of class B acquiring lock2");
// Attempt to acquire the lock with a timeout of 50 milliseconds
// the timeout ensures that the attempts
// will be stopped in 50 milliseconds
// Attention!!! It is the first part that
// demonstrates the Live lock behavior
lock2.tryLock(50, TimeUnit.MILLISECONDS);
System.out.println("Instance of class B acquired lock2 ");
// Sleep symbolizes some work that the thread should be doing
System.out.println("Instance of class B doing some work");
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
// System.out.println("Instance of class A acquiring lock1");
if (lock1.tryLock()) {
System.out.println("Instance of class B acquired lock1");
} else {
// Attention!!! It is the second part that
// demonstrates the Live lock behavior
System.out.println("Instance of class B cannot "
+ "acquire lock1, releasing lock2.");
lock2.unlock();
// Continue to the next iteration of the loop
continue;
}
// Never happens
System.out.println("Instance of class B doing "
+ "some work, with lock2 and lock1");
// breaks the loop
break;
}
// Never happens
// Unlock both locks
lock1.unlock();
lock2.unlock();
}
}
Deadlock vs Livelock
The main difference between a deadlock and a livelock is that in a deadlock, the execution is blocked, whereas in a livelock, the execution repeats the same loop indefinitely.
How to Debug and Prevent a Livelock
A livelock has all the same debugging and prevention techniques as a deadlock. However, there are a couple of exceptions:
- A livelock is much harder to identify. All the system components seem to be working. Even automated monitoring systems have trouble identifying the behavior.
- If detected, it is a bit easier to troubleshoot since the system will produce logs, and there is a possibility to dump the system state at different stages and it will provide extra information.
Summary: What is a Livelock
- A livelock is very similar to a deadlock
- A livelock enters into an infinite loop of execution instead of having its execution blocked as it is in a deadlock
- The majority of preventions and debugging techniques for livelocks are the same as for deadlocks
- Livelocks are harder to identify than deadlocks
- Debugging a livelock can be done by looking at the logs and dumping the system state