Java Multithreading & Synchronization: Thread Lifecycle, Locks & Deadlock Prevention for TU BCA

Author: Bhuban Subedi | Subject: Object-Oriented Programming in Java (CACS205) | Semester: Third Semester


Modern processors feature multiple CPU cores capable of executing millions of instructions concurrently. To build responsive user interfaces, high-concurrency web servers, and background data processors, modern applications rely on Multithreading.

A Thread is the smallest unit of execution within a process, often referred to as a “lightweight process”. Multiple threads within the same Java application share the same heap memory space, enabling fast communication and high performance.

In the Tribhuvan University (TU) BCA Third Semester OOP in Java (CACS205) board exams, questions on Thread Life Cycle, Thread class vs Runnable interface, and Thread Synchronization using synchronized and wait()/notify() appear regularly.

In this guide, we will break down the mechanics of concurrency, analyze race conditions, and write thread-safe Java programs.


1. Process vs. Thread: Understanding the Difference

+------------------------------------+------------------------------------+
| Process (Heavyweight)              | Thread (Lightweight)               |
+------------------------------------+------------------------------------+
| An independent executing program   | A sub-task or unit of execution    |
| with its own dedicated memory space.| inside a process.                 |
+------------------------------------+------------------------------------+
| Context switching between processes| Context switching between threads  |
| is slow and OS-heavy.              | is fast and lightweight.           |
+------------------------------------+------------------------------------+
| Inter-process communication (IPC)  | Threads share the same heap memory |
| requires sockets or shared files.  | and communicate directly.          |
+------------------------------------+------------------------------------+

2. The 6 States of the Java Thread Life Cycle

A Java thread managed by the JVM and the underlying operating system thread scheduler transitions through 6 distinct states defined in java.lang.Thread.State:

                       +-------------------+
                       |        NEW        |  (Thread object created: new Thread())
                       +---------+---------+
                                 | start()
                                 v
                       +-------------------+
                       |     RUNNABLE      | <---------------+
                       |  (Ready / Running)|                 |
                       +---+------+----+---+                 |
                           |      |    |                     |
              sleep() /    |      |    | I/O Wait /          | Lock Acquired /
              join()       |      |    | Waiting for Monitor | Notification
                           v      |    v                     |
              +----------------+  |  +----------------+      |
              | TIMED_WAITING  |  |  |    BLOCKED /   | -----+
              | / WAITING      |  |  |    WAITING     |
              +----------------+  |  +----------------+
                                  |
                                  | run() finishes
                                  v
                       +-------------------+
                       |    TERMINATED     |  (Dead / Finished execution)
                       +-------------------+

3. Creating Threads: extends Thread vs. implements Runnable

Java provides two primary approaches to create a thread:

Approach A: Implementing Runnable Interface (Recommended)

class MyTask implements Runnable {
    @Override
    public void run() {
        System.out.println("Thread running via Runnable interface.");
    }
}

// In main():
Thread t1 = new Thread(new MyTask());
t1.start();

Approach B: Extending Thread Class

class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("Thread running via Thread subclass.");
    }
}

// In main():
MyThread t1 = new MyThread();
t1.start();

Why Runnable is superior: In Java, a class can only extend one single parent class (no multiple inheritance). If you extend Thread, your class cannot inherit from any other base class. Implementing Runnable keeps your class hierarchy flexible and decouples the task logic from thread execution.


4. Race Conditions & The synchronized Keyword

When multiple threads concurrently read and modify shared data without coordination, their operations can interleave unpredictably, causing data corruption known as a Race Condition.

Solution: Mutual Exclusion via Intrinsic Locks

The synchronized keyword guarantees that only one thread at a time can execute a block or method guarded by an intrinsic lock (monitor).

class SharedCounter {
    private int count = 0;

    // Synchronized method guarantees thread safety
    public synchronized void increment() {
        count++;
    }

    public int getCount() {
        return count;
    }
}

5. Complete Java Program: Producer-Consumer using wait() and notify()

The Producer-Consumer Problem is the classic benchmark question in university operating systems and Java concurrency exams.

// Shared Buffer Resource
class SharedBuffer {
    private int data;
    private boolean hasData = false;

    // Synchronized method for Producer
    public synchronized void produce(int value) {
        // Wait while buffer is full
        while (hasData) {
            try {
                wait(); // Releases lock and enters waiting state
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }

        this.data = value;
        this.hasData = true;
        System.out.println("[PRODUCER] Produced Item: " + value);

        // Notify waiting consumer thread that data is ready
        notify();
    }

    // Synchronized method for Consumer
    public synchronized int consume() {
        // Wait while buffer is empty
        while (!hasData) {
            try {
                wait();
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }

        this.hasData = false;
        System.out.println("[CONSUMER] Consumed Item: " + data);

        // Notify waiting producer thread that buffer is empty
        notify();
        return data;
    }
}

// Producer Thread
class Producer implements Runnable {
    private SharedBuffer buffer;

    public Producer(SharedBuffer buffer) {
        this.buffer = buffer;
    }

    @Override
    public void run() {
        for (int i = 1; i <= 5; i++) {
            buffer.produce(i * 10);
            try { Thread.sleep(500); } catch (InterruptedException e) {}
        }
    }
}

// Consumer Thread
class Consumer implements Runnable {
    private SharedBuffer buffer;

    public Consumer(SharedBuffer buffer) {
        this.buffer = buffer;
    }

    @Override
    public void run() {
        for (int i = 1; i <= 5; i++) {
            buffer.consume();
            try { Thread.sleep(1000); } catch (InterruptedException e) {}
        }
    }
}

// Main Driver Class
public class ConcurrencyMasterDemo {
    public static void main(String[] args) {
        System.out.println("=== TU BCA JAVA MULTITHREADING DEMO ===");

        SharedBuffer buffer = new SharedBuffer();

        Thread prodThread = new Thread(new Producer(buffer), "Producer-Thread");
        Thread consThread = new Thread(new Consumer(buffer), "Consumer-Thread");

        // Start both concurrent threads
        prodThread.start();
        consThread.start();
    }
}

Frequently Asked Questions (FAQ)

Q1: What is the difference between start() and run() in Java?

  • Calling start() creates a new underlying operating system thread and then invokes run() asynchronously on the new call stack.
  • Calling run() directly merely executes the method synchronously on the current calling thread like a regular method call, without creating a new thread.

Q2: What causes a Deadlock and how can it be avoided?

A Deadlock occurs when Thread 1 holds Lock A and waits for Lock B, while Thread 2 holds Lock B and waits for Lock A. Neither thread can proceed. Deadlocks can be prevented by always acquiring locks in a strict global hierarchical order.

LEAVE A REPLY

Please enter your comment!
Please enter your name here