CACS204 Object Oriented Programming in Java

Object Oriented Programming in JavaUnit 108 min read

Multithreading in Java: Threads, Synchronization, and Concurrency

Unit 10 of Object Oriented Programming in Java covers multithreading concepts, thread lifecycle, inter-thread communication, synchronization, and real-world applications of concurrent programming in Java.

TAKEAWAYS:

  • Understand how Java achieves multithreading via the Thread class and Runnable interface.
  • Learn the five states of a thread lifecycle and how to control thread execution using start(), sleep(), and join().
  • Master synchronization mechanisms (synchronized keyword, wait(), notify(), notifyAll()) to prevent race conditions.
  • Compare thread safety issues and solutions (e.g., deadlocks, thread pools) with practical examples.
  • Apply multithreading to real-world scenarios like handling multiple user requests in eSewa or processing orders in Daraz.

Core Concepts of Multithreading

Multithreading allows a program to execute multiple threads concurrently, improving performance by utilizing CPU cores efficiently. Java supports multithreading through the Thread class and Runnable interface.

1. Thread Basics

A thread is the smallest unit of execution within a process. Java provides two ways to create threads:

  • Extending Thread class (not recommended for complex programs due to single inheritance limitation).
  • Implementing Runnable interface (preferred, as it avoids inheritance restrictions).

Example: Creating Threads

// Using Runnable interface (recommended)
class MyRunnable implements Runnable {
    public void run() {
        System.out.println("Thread is running...");
    }
}

public class Main {
    public static void main(String[] args) {
        Thread t1 = new Thread(new MyRunnable());
        t1.start(); // Starts the thread
    }
}

Thread Lifecycle

A thread in Java goes through five states:

startstart()CPU allocationwait()/sleep()notify()/interrupt()run() completesinterrupt()NewRunnableRunningBlocked/WaitingTerminated
Thread lifecycle transitions with triggers
State Description
New Thread is created but not yet started (new Thread()).
Runnable Thread is ready to run (after start() is called).
Running Thread is executing (run() method).
Blocked/Waiting Thread waits for a monitor lock or I/O operation.
Terminated Thread execution completes (run() finishes).

In the Real World

Multithreading is widely used in Nepalese and global applications to handle concurrent tasks efficiently:

  1. eSewa (Nepal)

    • Use: Multithreading handles simultaneous user requests (e.g., bill payments, fund transfers).
    • How? Each request runs in a separate thread, ensuring quick responses even during peak hours.
  2. Daraz (Nepal/Alibaba Group)

    • Use: Order processing (e.g., inventory updates, payment confirmations) runs in parallel threads.
    • How? A thread checks stock availability while another processes payment, reducing delays.
  3. WhatsApp (Meta)

    • Use: Message delivery uses multithreading to send/receive messages concurrently.
    • How? One thread handles incoming messages, another manages file uploads, and a third updates UI.

2. Thread Synchronization

When multiple threads access shared resources (e.g., a bank account balance), race conditions occur, leading to data corruption. Synchronization ensures thread safety.

Key Methods for Synchronization

Method Purpose
synchronized Locks an object to prevent multiple threads from accessing it simultaneously.
wait() Causes a thread to wait until another thread calls notify().
notify() Wakes up a single waiting thread.
notifyAll() Wakes up all waiting threads.

Example: Bank Account Transfer (Thread-Safe)

class BankAccount {
    private int balance = 1000;

    public synchronized void withdraw(int amount) {
        if (balance >= amount) {
            System.out.println("Withdrawing: " + amount);
            balance -= amount;
            System.out.println("New balance: " + balance);
        }
    }
}

public class Main {
    public static void main(String[] args) {
        BankAccount account = new BankAccount();
        Thread t1 = new Thread(() -> account.withdraw(300));
        Thread t2 = new Thread(() -> account.withdraw(400));
        t1.start();
        t2.start();
    }
}

Output (Thread-Safe):

Withdrawing: 300
New balance: 700
Withdrawing: 400
New balance: 300

3. Inter-Thread Communication

Threads often need to coordinate (e.g., producer-consumer problem). Java provides:

  • wait(): Puts a thread in waiting state.
  • notify(): Wakes up one waiting thread.
  • notifyAll(): Wakes up all waiting threads.

Example: Producer-Consumer Problem

class SharedBuffer {
    private int item;
    private boolean empty = true;

```figure
{"type":"network","nodes":["Producer","Buffer","Consumer"],"edges":[["Producer","Buffer","produce()"],["Buffer","Consumer","consume()"],["Buffer","Producer","wait()"],["Buffer","Consumer","notify()"]],"directed":true,"caption":"Producer-consumer interaction with wait/notify"}
public synchronized void produce(int value) {
    while (!empty) wait(); // Wait if buffer is full
    item = value;
    empty = false;
    System.out.println("Produced: " + item);
    notify(); // Notify consumer
}

public synchronized void consume() {
    while (empty) wait(); // Wait if buffer is empty
    System.out.println("Consumed: " + item);
    empty = true;
    notify(); // Notify producer
}

}

public class Main { public static void main(String[] args) { SharedBuffer buffer = new SharedBuffer(); Thread producer = new Thread(() -> buffer.produce(100)); Thread consumer = new Thread(() -> buffer.consume()); producer.start(); consumer.start(); } }

**Output:**

Produced: 100 Consumed: 100


4. Thread Pools (Executor Framework)

Creating threads manually is inefficient. Java’s Executor Framework (ExecutorService) manages thread pools for better performance.

Example: Using ExecutorService

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Main {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(3); // 3 threads
        for (int i = 0; i < 5; i++) {
            Runnable task = () -> System.out.println("Task " + i + " executed by " + Thread.currentThread().getName());
            executor.execute(task);
        }
        executor.shutdown();
    }
}

Output (varies based on thread scheduling):

Task 0 executed by pool-1-thread-1
Task 1 executed by pool-1-thread-2
Task 2 executed by pool-1-thread-3
Task 3 executed by pool-1-thread-1
Task 4 executed by pool-1-thread-2

5. Common Threading Issues & Solutions

Issue Cause Solution
Race Condition Multiple threads access shared data unsafely. Use synchronized blocks.
Deadlock Threads wait indefinitely for locks. Avoid nested locks; use timeouts.
Starvation Some threads never get CPU time. Use fair scheduling (new FairBlockingQueue).
Thread Leak Threads are not terminated properly. Always call shutdown() on ExecutorService.

Exam Tip

  • Define multithreading clearly (execution of two or more threads concurrently).
  • Differentiate Thread vs. Runnable: Prefer Runnable for flexibility.
  • Explain synchronization with a bank account example (race condition → synchronized).
  • Draw thread lifecycle diagrams (New → Runnable → Running → Terminated).
  • Code a producer-consumer problem using wait() and notify().
  • Mention thread pools (ExecutorService) for efficiency in real-world apps.

Visual Summary:

Based on the TU BCA syllabus for Object Oriented Programming in Java (CACS204), unit 10.

Discussion

Loading…