Deadlock-Free Transfers (Lock Ordering)
Problem
Four accounts (0 to 3) start with 1000 each. Every input line is a transfer: from to amount. Run every transfer in its own thread. A transfer needs the locks of both accounts. To avoid a deadlock, always take the lock of the smaller account number first. A transfer from an account to itself changes nothing. Balances may go below 0. After all threads finish, print account i: balance for i = 0..3.
Constraints
- 1 <= transfers <= 200
- 0 <= amount <= 10^6
Examples
Input
0 1 100 1 0 50
Output
account 0: 950 account 1: 1050 account 2: 1000 account 3: 1000
Solutions
A deadlock needs a circle of threads that wait for each other. A fixed global order for taking locks makes such a circle impossible. All changes are additions and subtractions, so the final balances are the same in every run.
/*
* Problem : Final balances.
* Input : Transfer lines.
* Output : Four balance lines.
* Approach: Ordered locks.
* Time : O(n)
* Space : O(n)
* Java : 8 or newer.
*/
import java.util.Scanner;
public class Main {
// Step 1: One lock object per account.
static final Object[] LOCKS = {new Object(), new Object(), new Object(), new Object()};
static final long[] BALANCE = new long[4];
// Step 2: Always lock the smaller account number first. Then two threads can never wait for each other in a circle.
static void transfer(int from, int to, long amount) {
if (from == to) {
return;
}
Object first = LOCKS[Math.min(from, to)];
Object second = LOCKS[Math.max(from, to)];
synchronized (first) {
synchronized (second) {
BALANCE[from] -= amount;
BALANCE[to] += amount;
}
}
}
// This method solves the task. It receives the input and returns the answer.
static String[] solve(String[] lines) {
java.util.Arrays.fill(BALANCE, 1000);
// Step 3: Start one thread for every transfer line.
Thread[] threads = new Thread[lines.length];
for (int i = 0; i < lines.length; i++) {
String[] part = lines[i].split(" ");
int from = Integer.parseInt(part[0]);
int to = Integer.parseInt(part[1]);
long amount = Long.parseLong(part[2]);
threads[i] = new Thread(() -> transfer(from, to, amount));
threads[i].start();
}
// Step 4: Wait for all threads. join() throws a checked exception, so we handle it.
try {
for (Thread thread : threads) {
thread.join();
}
} catch (InterruptedException error) {
throw new IllegalStateException(error);
}
String[] output = new String[4];
for (int i = 0; i < 4; i++) {
output[i] = "account " + i + ": " + BALANCE[i];
}
return output;
}
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
java.util.List<String> all = new java.util.ArrayList<>();
while (scanner.hasNextLine()) {
String line = scanner.nextLine().trim();
if (!line.isEmpty()) {
all.add(line);
}
}
String[] lines = all.toArray(new String[0]);
System.out.println(String.join("\n", solve(lines)));
}
}
Hints (1)
- Why can a circular wait not happen when locks are taken in order?