Home
8Synchronization, Deadlock & Resource Allocation
8

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() and notifyAll() for coordination

Before you start

  • Lab 7: Thread, Runnable, states, sleep() and join()

Plan for the session

  1. Theory recap
  2. Walk through the program, section by section, and run it
  3. In-lab tasks, shown to the instructor
  4. Report exercises at home

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 while loop: a woken thread may find the condition false again.

Implementation design

Execution flow

  1. Create ACC-001 (5000) and ACC-002 (3000)
  2. Do one safe transfer with lock ordering
  3. Run two threads transferring 100 back and forth five times each
  4. join() them and check the total is still 8000
  5. Start two risky transfers that lock in opposite order
  6. 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

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 that the balance stays consistent under concurrent deposits and withdrawals.
Requirements
Show a hint
Check-then-withdraw must happen inside one synchronized region, otherwise another thread can change the balance in between.

In-lab task 2 of 3

Task 2: A simple wait()/notify() buffer

SharedBuffer holds one integer at a time.
Requirements
Show a hint
Use while (!full) wait(); — never if.

In-lab task 3 of 3

Task 3: Deadlock detection with timeout

Make the deadlock happen reliably and detect it.
Requirements
Show a hint
Mark the threads as daemon (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()

A generic BoundedQueue shared by one producer and two consumers.
Requirements

Lab report · exercise 2 of 2

Dining philosophers without deadlock

Five philosophers, five forks, timeout-based acquisition.
Requirements

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)

Name the four conditions for deadlock.
Show the answer
Mutual exclusion, hold and wait, no preemption and circular wait — all four at once.
Which condition does lock ordering remove?
Show the answer
Circular wait: if every thread takes locks in the same global order, no cycle can form.

Discussion and analysis

Viva questions (2 of 2)

Why does transfer() use synchronized blocks rather than a synchronized method?
Show the answer
It needs to hold two different objects’ locks, chosen in a specific order; a synchronized method locks only this.
Why must wait() be inside a while loop?
Show the answer
A woken thread may find the condition false again (another thread got there first, or a spurious wake-up), so it must re-check.

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.