←Back to Subject
Notes
Notes
Study material and explanations
Diagrams
Diagrams
Visualize concepts easily
Mnemonics
Mnemonics
Tricks to remember faster
One Night Prep
One Night Prep
Short and focused revision
Revision
Revision
Quick summary and key points
Important Questions
Important Questions
Practice most relevant questions
Notes

Creating Threads (Thread Class vs Runnable), Synchronization & Thread Scheduling

Study material and explanations for Unit 3

Creating Threads, Thread Synchronization & Thread Scheduling

1. Two Ways to Create a Thread in Java

Java provides two ways to create and launch a new thread of execution:

  1. By extending the Thread class (class MyThread extends Thread)
  2. By implementing the Runnable interface (class MyTask implements Runnable)
Figure 3.2: Two Ways of Creating Threads in Java
Method 1: Extending Thread Class
  • class MyThread extends Thread
  • Override public void run()
  • Instantiate: MyThread t = new MyThread();
  • Start thread: t.start();
Method 2: Implementing Runnable Interface
  • class MyTask implements Runnable
  • Implement public void run()
  • Pass to Thread: Thread t = new Thread(new MyTask());
  • Start thread: t.start();

Method 1: Creating a Thread by Extending the Thread Class

  1. Create a class that extends java.lang.Thread.
  2. Override the public void run() method and write the code you want the thread to execute inside it.
  3. Create an object of your class and call start() (which creates a new thread call stack and automatically invokes run()).
class NumberThread extends Thread {
    @Override
    public void run() {
        for (int i = 1; i <= 3; i++) {
            System.out.println(getName() + " printing: " + i);
        }
    }
}

public class ThreadClassDemo {
    public static void main(String[] args) {
        NumberThread t1 = new NumberThread();
        NumberThread t2 = new NumberThread();
        t1.setName("Worker-1");
        t2.setName("Worker-2");

        t1.start(); // Starts Worker-1 concurrently
        t2.start(); // Starts Worker-2 concurrently
    }
}

Method 2: Creating a Thread by Implementing the Runnable Interface

  1. Create a class that implements java.lang.Runnable.
  2. Implement the single abstract method public void run().
  3. Create an object of your class, pass it into the constructor of a Thread object (Thread t = new Thread(taskObj);), and call t.start().
class DownloadTask implements Runnable {
    @Override
    public void run() {
        for (int step = 1; step <= 3; step++) {
            System.out.println(Thread.currentThread().getName() + " - Downloading chunk " + step);
        }
    }
}

public class RunnableDemo {
    public static void main(String[] args) {
        DownloadTask task = new DownloadTask();
        Thread t1 = new Thread(task, "Downloader-A");
        Thread t2 = new Thread(task, "Downloader-B");

        t1.start();
        t2.start();
    }
}

Why do we call t.start() instead of calling t.run() directly? If you call t.run() directly, no new thread is created—run() is executed like a normal method call on the current main thread stack! Calling t.start() instructs the JVM to create a separate thread call stack and then invoke run() concurrently.

Comparison: Extending Thread vs Implementing Runnable (Which is Better?)

Implementing Runnable is always preferred in real-world Java programming! Why?

Basis Extending Thread Class Implementing Runnable Interface
1. Multiple Inheritance Because Java does not allow extending two classes, if your class extends Thread, it cannot extend any other class (like Frame or Applet). Your class can still extend another parent class (class MyApplet extends Frame implements Runnable).
2. Object Sharing Each thread creates a separate unique object. Multiple threads can share the exact same Runnable target object.
3. OOP Design Tightly couples task logic with thread control. Cleanly separates the Task (Runnable) from the Runner (Thread).

2. Thread Synchronization in Java

When two or more threads try to access and modify a shared resource (such as the same Bank Account balance or Printer object) at the exact same time, their operations can overlap and corrupt the data. This bug is called a Race Condition.

Real-Life Example of Race Condition: Suppose a joint bank account has ₹10,000. Husband and Wife both swipe their ATM cards at the exact same millisecond to withdraw ₹8,000 each. Without synchronization, both threads read balance = 10000, see that 10000 >= 8000, and dispense ₹16,000 total!

What is Thread Synchronization?

Thread Synchronization is the mechanism that ensures only ONE thread can access a shared resource (critical section) at a time.

  • In Java, every object has an internal lock called a Monitor (or Intrinsic Lock).
  • When a method or block is declared with the synchronized keyword, a thread must first acquire the object’s Monitor Lock before entering. All other threads trying to enter are blocked until the first thread finishes and releases the lock!

Complete Program Demonstrating synchronized Method

class BankAccount {
    private int balance = 10000;

    // 'synchronized' ensures only ONE thread can withdraw at a time!
    public synchronized void withdraw(String user, int amount) {
        System.out.println(user + " is trying to withdraw Rs." + amount);
        if (balance >= amount) {
            balance -= amount;
            System.out.println("SUCCESS: " + user + " withdrew Rs." + amount + " | Remaining: Rs." + balance);
        } else {
            System.out.println("FAILED: Insufficient balance for " + user + " | Available: Rs." + balance);
        }
    }
}

public class SyncDemo {
    public static void main(String[] args) {
        BankAccount sharedAcc = new BankAccount();

        // Thread 1 (Person A) and Thread 2 (Person B) accessing the SAME account
        Thread t1 = new Thread(() -> sharedAcc.withdraw("Aarav", 8000));
        Thread t2 = new Thread(() -> sharedAcc.withdraw("Priya", 8000));

        t1.start();
        t2.start();
    }
}

Output (Protected against Race Condition):

Aarav is trying to withdraw Rs.8000
SUCCESS: Aarav withdrew Rs.8000 | Remaining: Rs.2000
Priya is trying to withdraw Rs.8000
FAILED: Insufficient balance for Priya | Available: Rs.2000

3. Thread Scheduling and Thread Priority

When multiple threads are waiting in the Runnable queue, who decides which thread gets the CPU next? The Thread Scheduler (part of the JVM) makes this decision using two algorithms:

  1. Preemptive-Priority Scheduling: The thread with the highest priority number is chosen to run first. If a higher-priority thread enters the Runnable state, it preempts (interrupts) the currently running lower-priority thread.
  2. Time-Slicing (Round-Robin): If multiple threads have the exact same priority, each thread is given a fixed slice of CPU time in turn.

Thread Priorities in Java (1 to 10)

Every thread in Java is assigned an integer priority between 1 (Lowest) and 10 (Highest). By default, every thread inherits the priority of its parent thread (main has priority 5).

The Thread class defines 3 built-in priority constants:

  • Thread.MIN_PRIORITY = 1 (Lowest priority)
  • Thread.NORM_PRIORITY = 5 (Default normal priority)
  • Thread.MAX_PRIORITY = 10 (Highest priority)

Methods to Get and Set Thread Priority:

  • int getPriority(): Returns the thread’s current priority.
  • void setPriority(int newPriority): Sets the thread’s priority (1 to 10; throws IllegalArgumentException if outside 1–10).
t1.setPriority(Thread.MIN_PRIORITY);  // Sets priority to 1
t2.setPriority(Thread.MAX_PRIORITY);  // Sets priority to 10

💡 Exam Preparation Tips for DAVV BCA

  • If asked “Why is implementing Runnable better than extending Thread?”, highlight that Java does not support multiple inheritance of classes, so implements Runnable leaves your class free to extend another class!
  • Always remember the 3 priority constants: MIN_PRIORITY = 1, NORM_PRIORITY = 5, and MAX_PRIORITY = 10.