Lab Manual VIII · CSE 202
Synchronization, Deadlock & Resource Allocation
Protect bank balances with synchronized methods and blocks, watch two transfers deadlock when they lock accounts in opposite order, and prevent it with lock ordering.
← → move between slides · F full screen · M slide list
Before we start
Why this lab
Thread 1 moves money from A to B while thread 2 moves money from B to A. Each grabs one account and waits for the other — forever. How do we stop that?
That is a deadlock. In this lab you cause one on purpose, then remove it with a simple rule: always lock accounts in the same order.
Lab 8 of 10
Lab at a glance
After this lab you can
- Explain why shared data needs synchronization
- Describe race conditions and critical sections
- Use synchronized methods and synchronized blocks
- Explain monitor (object) locks
- Name the four necessary conditions for deadlock
- Demonstrate and prevent deadlock with lock ordering
- Use
wait(),notify()andnotifyAll()for coordination
Before you start
- Lab 7: Thread, Runnable, states, sleep() and join()
Plan for the session
- Theory recap
- Walk through the program, section by section, and run it
- In-lab tasks, shown to the instructor
- Report exercises at home
Lab 8 of 10
Contents
Select a part to jump to it.
Theory 1 of 6
Race conditions and critical sections
balance = balance + amount; // read, add, write: three steps
- If two threads interleave those steps, one update can overwrite the other — a race condition.
- Code that touches shared data is a critical section; only one thread should run it at a time.
Theory 2 of 6
synchronized methods and blocks
public synchronized void deposit(double amount) { balance += amount; }
synchronized (account) { // lock a chosen object
account.withdraw(amount);
}- Entering either form acquires the object’s monitor lock; other threads wait (BLOCKED) until it is released.
- Methods protect the whole body; blocks protect only what needs it and let you pick the lock — used to lock two accounts in transfer().
Theory 3 of 6
Deadlock
- Four conditions must all hold: mutual exclusion, hold and wait, no preemption, circular wait.
- Break any one of them and deadlock cannot happen.
Theory 4 of 6
Resource allocation graph
- Threads are circles, resources are squares. Request edge: thread → resource; assignment edge: resource → thread.
- A cycle in the graph means a possible deadlock.
Theory 5 of 6
Prevention: lock ordering
BankAccount first = accountId.compareTo(target.accountId) < 0 ? this : target;
BankAccount second = (first == this) ? target : this;
synchronized (first) {
synchronized (second) {
this.withdraw(amount);
target.deposit(amount);
}
}- Every thread locks the account with the smaller ID first, so a circular wait is impossible.
Theory 6 of 6
wait(), notify() and notifyAll()
synchronized (buffer) {
while (bufferIsEmpty) {
buffer.wait(); // releases the lock while waiting
}
...
buffer.notifyAll();
}- Call them only while holding the object’s lock.
- Always re-check the condition in a
whileloop: a woken thread may find the condition false again.
Implementation design
Execution flow
- Create ACC-001 (5000) and ACC-002 (3000)
- Do one safe transfer with lock ordering
- Run two threads transferring 100 back and forth five times each
- join() them and check the total is still 8000
- Start two risky transfers that lock in opposite order
- join(3000): if a thread is still alive, report a possible deadlock
BankAccount has synchronized deposit/withdraw/getBalance and an ordered transfer(); DeadlockProneTransfer shows the unsafe way.
Code walkthrough 1 of 9 · BankAccount.java
A synchronized withdrawal
public synchronized void withdraw(double amount) {
if (amount <= 0.0) {
throw new IllegalArgumentException("Amount must be positive.");
}
if (amount > balance) {
throw new IllegalArgumentException("Insufficient funds in " + accountId
+ ". Requested: " + amount + ", Available: " + balance);
}
balance = balance - amount;
System.out.println(Thread.currentThread().getName() + " withdrew " + amount
+ " from " + accountId + ". New balance: " + balance);
}- Check and update happen while holding the account’s lock.
Code walkthrough 2 of 9 · BankAccount.java
Choosing the lock order
// Locks are acquired in a fixed order to prevent deadlock.
public void transfer(BankAccount target, double amount) {
if (target == null) {
throw new IllegalArgumentException("Target account cannot be null.");
}
BankAccount first;
BankAccount second;
if (this.accountId.compareTo(target.accountId) < 0) {
first = this;
second = target;
} else {
first = target;
second = this;
}- The account with the smaller ID always becomes
first, whichever direction the money moves.
Code walkthrough 3 of 9 · BankAccount.java
Locking both accounts in that order
synchronized (first) {
synchronized (second) {
System.out.println(Thread.currentThread().getName() + " transferring "
+ amount + " from " + this.accountId + " to " + target.accountId);
this.withdraw(amount);
target.deposit(amount);
System.out.println(Thread.currentThread().getName() + " completed transfer.");
}
}- Both locks are held during the whole transfer.
- Two opposite transfers can never wait for each other in a circle.
Code walkthrough 4 of 9 · DeadlockProneTransfer.java
The risky way: lock the source
// Inconsistent lock ordering: source first, then target.
@Override
public void run() {
synchronized (from) {
System.out.println(Thread.currentThread().getName() + " locked " + from.getAccountId());
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}- Each thread locks its own source first, then sleeps while holding it.
Code walkthrough 5 of 9 · DeadlockProneTransfer.java
…then wait for the target
synchronized (to) {
System.out.println(Thread.currentThread().getName() + " locked " + to.getAccountId());
try {
from.withdraw(amount);
to.deposit(amount);
System.out.println(Thread.currentThread().getName() + " completed risky transfer.");
} catch (IllegalArgumentException e) {
System.out.println("Risky transfer failed: " + e.getMessage());
}
}- Opposite transfers → each holds the lock the other needs. Neither can continue.
Code walkthrough 6 of 9 · Lab8Demo.java
Two threads, opposite transfers, safely
Thread safeThread1 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
safeAccount1.transfer(safeAccount2, 100.0);
try {
Thread.sleep(50);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}, "SafeThread-1");- The lambda is the Runnable.
- safeThread2 is the same with the two accounts swapped.
- transfer() orders the locks, so this cannot deadlock.
Code walkthrough 7 of 9 · Lab8Demo.java
Checking that no money was lost
safeThread1.start();
safeThread2.start();
try {
safeThread1.join();
safeThread2.join();
} catch (InterruptedException e) {
System.out.println("Main thread interrupted.");
Thread.currentThread().interrupt();
}
System.out.println("\nAfter concurrent safe transfers:");
safeAccount1.display();
safeAccount2.display();
double safeTotal = safeAccount1.getBalance() + safeAccount2.getBalance();
System.out.println("Total balance: " + safeTotal);- After join(), the total must still be 8000.
Code walkthrough 8 of 9 · Lab8Demo.java
Provoking the deadlock
BankAccount riskAccount1 = new BankAccount("ACC-001", 5000.0);
BankAccount riskAccount2 = new BankAccount("ACC-002", 3000.0);
Thread riskyThread1 = new Thread(
new DeadlockProneTransfer(riskAccount1, riskAccount2, 500.0), "RiskyThread-1");
Thread riskyThread2 = new Thread(
new DeadlockProneTransfer(riskAccount2, riskAccount1, 700.0), "RiskyThread-2");- Same two accounts, opposite directions, no ordering.
Code walkthrough 9 of 9 · Lab8Demo.java
Detecting it with a timeout
riskyThread1.start();
riskyThread2.start();
try {
riskyThread1.join(3000);
riskyThread2.join(3000);
} catch (InterruptedException e) {
System.out.println("Main thread interrupted during deadlock test.");
Thread.currentThread().interrupt();
}
if (riskyThread1.isAlive() || riskyThread2.isAlive()) {
Code walkthrough 9 of 9 · Lab8Demo.java
Detecting it with a timeout (cont.)
System.out.println("\nPossible deadlock detected.");
System.out.println("At least one risky thread did not finish within timeout.");
System.out.println("This happens because locks may be acquired in opposite order.");
} else {
System.out.println("\nRisky threads completed in this run.");
System.out.println("Deadlock did not occur this time, but the design is unsafe.");
}join(3000)waits at most 3 s; a thread still alive means it is stuck.
Step through the code · 1 of 2
Trace: lock ordering in transfer()
Try both directions: the locks are always taken in the same order, ACC-A first.
Step through the code · 1 of 2
Trace: lock ordering in transfer() (cont.)
Step through the code · 2 of 2
Trace: how a deadlock happens
Two transfers in opposite directions. Choose the timing and watch both threads end up BLOCKED, each holding the lock the other needs.
Step through the code · 2 of 2
Trace: how a deadlock happens (cont.)
For reference · 3 files
The complete program
public class BankAccount {
private String accountId;
private double balance;
public BankAccount(String accountId, double initialBalance) {
this.accountId = accountId;
if (initialBalance >= 0.0) {
this.balance = initialBalance;
} else {
this.balance = 0.0;
}
}
public synchronized void withdraw(double amount) {
if (amount <= 0.0) {
throw new IllegalArgumentException("Amount must be positive.");
}
if (amount > balance) {
throw new IllegalArgumentException("Insufficient funds in " + accountId
+ ". Requested: " + amount + ", Available: " + balance);
}
balance = balance - amount;
System.out.println(Thread.currentThread().getName() + " withdrew " + amount
+ " from " + accountId + ". New balance: " + balance);
}
public synchronized void deposit(double amount) {
if (amount <= 0.0) {
throw new IllegalArgumentException("Amount must be positive.");
}
balance = balance + amount;
System.out.println(Thread.currentThread().getName() + " deposited " + amount
+ " into " + accountId + ". New balance: " + balance);
}
public synchronized double getBalance() {
return balance;
}
public String getAccountId() {
return accountId;
}
// Locks are acquired in a fixed order to prevent deadlock.
public void transfer(BankAccount target, double amount) {
if (target == null) {
throw new IllegalArgumentException("Target account cannot be null.");
}
BankAccount first;
BankAccount second;
if (this.accountId.compareTo(target.accountId) < 0) {
first = this;
second = target;
} else {
first = target;
second = this;
}
synchronized (first) {
synchronized (second) {
System.out.println(Thread.currentThread().getName() + " transferring "
+ amount + " from " + this.accountId + " to " + target.accountId);
this.withdraw(amount);
target.deposit(amount);
System.out.println(Thread.currentThread().getName() + " completed transfer.");
}
}
}
public void display() {
System.out.println("Account: " + accountId + " | Balance: " + getBalance());
}
}public class DeadlockProneTransfer implements Runnable {
private BankAccount from;
private BankAccount to;
private double amount;
public DeadlockProneTransfer(BankAccount from, BankAccount to, double amount) {
this.from = from;
this.to = to;
this.amount = amount;
}
// Inconsistent lock ordering: source first, then target.
@Override
public void run() {
synchronized (from) {
System.out.println(Thread.currentThread().getName() + " locked " + from.getAccountId());
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
synchronized (to) {
System.out.println(Thread.currentThread().getName() + " locked " + to.getAccountId());
try {
from.withdraw(amount);
to.deposit(amount);
System.out.println(Thread.currentThread().getName() + " completed risky transfer.");
} catch (IllegalArgumentException e) {
System.out.println("Risky transfer failed: " + e.getMessage());
}
}
}
}
}public class Lab8Demo {
public static void main(String[] args) {
// 1. Creating Accounts
System.out.println("=== Synchronization and Deadlock Demo ===");
BankAccount account1 = new BankAccount("ACC-001", 5000.0);
BankAccount account2 = new BankAccount("ACC-002", 3000.0);
System.out.println("\n=== Initial Account States ===");
account1.display();
account2.display();
// 2. Safe Transfer using Lock Ordering
System.out.println("\n=== Safe Transfer using Lock Ordering ===");
account1.transfer(account2, 1000.0);
System.out.println("\nAfter safe transfer:");
account1.display();
account2.display();
// 3. Concurrent Safe Transfers
System.out.println("\n=== Concurrent Safe Transfers ===");
BankAccount safeAccount1 = new BankAccount("ACC-001", 5000.0);
BankAccount safeAccount2 = new BankAccount("ACC-002", 3000.0);
Thread safeThread1 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
safeAccount1.transfer(safeAccount2, 100.0);
try {
Thread.sleep(50);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}, "SafeThread-1");
Thread safeThread2 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
safeAccount2.transfer(safeAccount1, 100.0);
try {
Thread.sleep(50);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}, "SafeThread-2");
safeThread1.start();
safeThread2.start();
try {
safeThread1.join();
safeThread2.join();
} catch (InterruptedException e) {
System.out.println("Main thread interrupted.");
Thread.currentThread().interrupt();
}
System.out.println("\nAfter concurrent safe transfers:");
safeAccount1.display();
safeAccount2.display();
double safeTotal = safeAccount1.getBalance() + safeAccount2.getBalance();
System.out.println("Total balance: " + safeTotal);
// 4. Deadlock Risk Demonstration
System.out.println("\n=== Deadlock Risk Demonstration ===");
System.out.println("The following example starts two risky transfer tasks.");
System.out.println("If both threads remain alive after timeout, deadlock is possible.");
BankAccount riskAccount1 = new BankAccount("ACC-001", 5000.0);
BankAccount riskAccount2 = new BankAccount("ACC-002", 3000.0);
Thread riskyThread1 = new Thread(
new DeadlockProneTransfer(riskAccount1, riskAccount2, 500.0), "RiskyThread-1");
Thread riskyThread2 = new Thread(
new DeadlockProneTransfer(riskAccount2, riskAccount1, 700.0), "RiskyThread-2");
riskyThread1.start();
riskyThread2.start();
try {
riskyThread1.join(3000);
riskyThread2.join(3000);
} catch (InterruptedException e) {
System.out.println("Main thread interrupted during deadlock test.");
Thread.currentThread().interrupt();
}
if (riskyThread1.isAlive() || riskyThread2.isAlive()) {
System.out.println("\nPossible deadlock detected.");
System.out.println("At least one risky thread did not finish within timeout.");
System.out.println("This happens because locks may be acquired in opposite order.");
} else {
System.out.println("\nRisky threads completed in this run.");
System.out.println("Deadlock did not occur this time, but the design is unsafe.");
}
// 5. Final Message
System.out.println("\n=== Final Observation ===");
System.out.println("Use consistent lock ordering to prevent circular wait.");
System.out.println("Program demonstration completed.");
}
}Copy or download each file, save it under the same name, then compile and run: javac *.java → java Lab8Demo
Implementation
Expected output
=== Synchronization and Deadlock Demo === === Initial Account States === Account: ACC-001 | Balance: 5000.0 Account: ACC-002 | Balance: 3000.0 === Safe Transfer using Lock Ordering === main transferring 1000.0 from ACC-001 to ACC-002 main withdrew 1000.0 from ACC-001. New balance: 4000.0 main deposited 1000.0 into ACC-002. New balance: 4000.0 main completed transfer. After safe transfer: Account: ACC-001 | Balance: 4000.0 Account: ACC-002 | Balance: 4000.0 === Concurrent Safe Transfers === SafeThread-1 transferring 100.0 from ACC-001 to ACC-002 SafeThread-1 withdrew 100.0 from ACC-001. New balance: 4900.0 SafeThread-1 deposited 100.0 into ACC-002. New balance: 3100.0 SafeThread-1 completed transfer. SafeThread-2 transferring 100.0 from ACC-002 to ACC-001 SafeThread-2 withdrew 100.0 from ACC-002. New balance: 3000.0 SafeThread-2 deposited 100.0 into ACC-001. New balance: 5000.0 SafeThread-2 completed transfer. ... After concurrent safe transfers: Account: ACC-001 | Balance: 5000.0 Account: ACC-002 | Balance: 3000.0 Total balance: 8000.0 === Deadlock Risk Demonstration === The following example starts two risky transfer tasks. If both threads remain alive after timeout, deadlock is possible. RiskyThread-1 locked ACC-001 RiskyThread-2 locked ACC-002 Possible deadlock detected. At least one risky thread did not finish within timeout. This happens because locks may be acquired in opposite order. === Final Observation === Use consistent lock ordering to prevent circular wait. Program demonstration completed.
The risky threads usually deadlock (each holds one lock, then sleeps); on some runs they finish. Note the JVM will not exit while deadlocked threads are alive — stop it from the IDE.
Interactive 1 of 2
Race condition lab
Interactive 2 of 2
Producer–consumer with wait and notify
In-lab task 1 of 3
Task 1: Synchronized printBalance()
Show a hint
In-lab task 2 of 3
Task 2: A simple wait()/notify() buffer
Show a hint
while (!full) wait(); — never if.In-lab task 3 of 3
Task 3: Deadlock detection with timeout
Show a hint
setDaemon(true)) so the program can exit after detecting the deadlock.In-lab tasks
Show your work during the lab
For each task, show the instructor:
- the source code
- a successful compilation
- the running output
- a short explanation of the logic you used
Lab report · exercise 1 of 2
Thread-safe bounded queue with wait()/notifyAll()
Lab report · exercise 2 of 2
Dining philosophers without deadlock
Lab report
Writing the lab report
- Problem statement and your design (classes, fields, methods)
- Complete source code, formatted and commented
- Sample output or screenshots for normal and edge cases
- A short discussion: what each OOP concept did in your program
Academic integrity
Copying from the internet, classmates, seniors or any other unauthorized source is strictly prohibited. Full marks may be deducted if plagiarism, copied work or academic dishonesty is detected. Complete the tasks, implementation, output analysis and report yourself.
Discussion and analysis
Viva questions (1 of 2)
Show the answer
Show the answer
Discussion and analysis
Viva questions (2 of 2)
Show the answer
Show the answer
Check yourself
Quiz
Summary
Summary
- Shared data + concurrent writes = race conditions; protect critical sections.
- synchronized methods and blocks take an object’s monitor lock.
- Deadlock needs mutual exclusion, hold and wait, no preemption and circular wait.
- Consistent lock ordering removes circular wait.
- wait/notify/notifyAll coordinate threads; always wait in a while loop.