Skip to content

Question 4

Create two threads using the Thread class and Runnable interface. Explain the differences and demonstrate thread creation and management.

java
/**
 * 1. A class that creates a thread by extending the Thread class.
 */
class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("Thread 1 (extending Thread) is starting...");
        for (int i = 1; i <= 5; i++) {
            System.out.println("Thread 1: " + i);
            try {
                // Pause for half a second to simulate work
                Thread.sleep(500);
            } catch (InterruptedException e) {
                System.out.println("Thread 1 was interrupted.");
            }
        }
        System.out.println("Thread 1 has finished.");
    }
}

/**
 * 2. A class that creates a thread by implementing the Runnable interface.
 */
class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("Thread 2 (implementing Runnable) is starting...");
        for (int i = 1; i <= 5; i++) {
            System.out.println("Thread 2: " + i);
            try {
                // Pause for half a second
                Thread.sleep(500);
            } catch (InterruptedException e) {
                System.out.println("Thread 2 was interrupted.");
            }
        }
        System.out.println("Thread 2 has finished.");
    }
}

/**
 * The main class to demonstrate thread creation and management.
 */
public class ThreadDemo {

    public static void main(String[] args) {
        System.out.println("Main thread is starting.");

        // --- Create and start the first thread (extending Thread) ---
        MyThread thread1 = new MyThread();
        thread1.start(); // This calls the run() method in a new thread

        // --- Create and start the second thread (implementing Runnable) ---
        MyRunnable runnableTask = new MyRunnable();
        Thread thread2 = new Thread(runnableTask); // Pass the task to a Thread
        thread2.start(); // This also calls run() in a new thread

        // --- Thread Management: Wait for threads to finish ---
        try {
            // The main thread will pause here and wait for thread1 and thread2 to complete.
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            System.out.println("Main thread was interrupted.");
        }

        System.out.println("Main thread has finished.");
    }
}

How to Compile and Run

  1. Save: Save the code in a file named ThreadDemo.java.
  2. Compile: Open your terminal or command prompt and run the compiler: javac ThreadDemo.java
  3. Run: After compiling, run the program: java ThreadDemo

Expected Output

Because the threads run concurrently, the exact order of the output from "Thread 1" and "Thread 2" may vary slightly each time you run it. However, the structure will be similar to this:

Main thread is starting.
Thread 1 (extending Thread) is starting...
Thread 2 (implementing Runnable) is starting...
Thread 1: 1
Thread 2: 1
Thread 2: 2
Thread 1: 2
Thread 1: 3
Thread 2: 3
Thread 2: 4
Thread 1: 4
Thread 1: 5
Thread 2: 5
Thread 2 has finished.
Thread 1 has finished.
Main thread has finished.

Program Explanation

This program creates and starts two separate threads that run concurrently with the main thread. Each thread simply counts from 1 to 5, printing a message at each step.

  1. Method 1: Extending the Thread Class (MyThread)

    • A class MyThread is created that inherits from the java.lang.Thread class.
    • It must override the run() method. The code inside run() is what the new thread will execute.
    • To start the thread, you create an instance of MyThread and call its start() method.
  2. Method 2: Implementing the Runnable Interface (MyRunnable)

    • A class MyRunnable is created that implements the java.lang.Runnable interface.
    • This also requires implementing the run() method, which contains the code for the thread to execute.
    • To start the thread, you first create an instance of MyRunnable. Then, you pass this instance into the constructor of a new Thread object. Finally, you call the start() method on that Thread object.

Key Differences and Best Practice

FeatureExtending ThreadImplementing Runnable
InheritanceYour class cannot extend any other class because Java does not support multiple inheritance. This is a major limitation.Your class can still extend another class. This provides much more flexibility.
DesignMixes the "thread" (the worker) with the "task" (the code to run).Separates the task (Runnable) from the worker (Thread). This is a better object-oriented design.
RecommendationGenerally avoided unless you need to modify the Thread class's fundamental behavior.This is the preferred method. It is more flexible and promotes better code design.

1. Creating Threads

a) Using the Thread class (by extending it):

java
// MyThread.java
class MyThread extends Thread {
    private String threadName;
    private int count;

    public MyThread(String name, int count) {
        this.threadName = name;
        this.count = count;
        System.out.println("Creating " +  threadName );
    }

    @Override
    public void run() {
        System.out.println("Running " +  threadName );
        try {
            for(int i = 0; i < count; i++) {
                System.out.println("Thread: " 
	                + threadName + ", Count: " + i);
            // Using sleep to similate work
                Thread.sleep(50);
            }
        } catch (InterruptedException e) {
            System.out.println("Thread " 
	            +  threadName + " interrupted.");
        }
        System.out.println("Thread " 
			+  threadName + " exiting.");
    }
}

b) Using the Runnable interface (by implementing it):

java
// MyRunnable.java
class MyRunnable implements Runnable {
    private String runnableName;
    private int count;
    private Thread thread;

    public MyRunnable(String name, int count) {
        this.runnableName = name;
        this.count = count;
        System.out.println("Creating Runnable: " 
	        +  runnableName );
    }

    @Override
    public void run() {
        System.out.println("Running task for: " 
	        +  runnableName );
        try {
            for(int i = 0; i < count; i++) {
	// Thread.currentThread().getName() gives the actual thread's name
                System.out.println("Runnable Task: " 
	                + runnableName + " on Thread: " 
					+ Thread.currentThread().getName() 
					+ ", Count: " + i);
                Thread.sleep(70); 
                // Slightly different sleep time
            }
        } catch (InterruptedException e) {
            System.out.println("Runnable task " 
	            +  runnableName + " interrupted.");
        }
        System.out.println("Runnable task " 
	        +  runnableName + " finishing.");
    }

    // Optional method to create and start the thread associated with this Runnable
    public void start() {
        System.out.println("Starting Thread for Runnable: " + runnableName);
        if (thread == null) {
            thread = new Thread(this, runnableName + "-Thread"); // Pass 'this' Runnable and a name for the Thread
            thread.start(); // This calls the run() method of this Runnable instance
        }
    }

    // Optional method to allow joining this runnable's thread
    public void join() throws InterruptedException {
        if (thread != null && thread.isAlive()) {
            thread.join();
        }
    }
}

2. Main Application to Demonstrate Threads

java
// ThreadDemo.java
public class ThreadDemo {

    public static void main(String[] args) {
        System.out.println("--- Main Thread Started ---");

        // --- Method 1: Using Thread Class ---
        System.out.println("\n--- Demonstrating extending Thread Class ---");
        MyThread thread1 = new MyThread("Thread-A (Extends Thread)", 5);
        MyThread thread2 = new MyThread("Thread-B (Extends Thread)", 3);

        // Starting the threads - this calls their run() method in a new execution path
        thread1.start();
        thread2.start();

        // --- Method 2: Using Runnable Interface ---
        System.out.println("\n--- Demonstrating implementing Runnable Interface ---");
        MyRunnable runnableTask1 = new MyRunnable("Runnable-X", 4);
        MyRunnable runnableTask2 = new MyRunnable("Runnable-Y", 6);

        // To run a Runnable, you need to create a Thread object and pass the Runnable instance to it.
        Thread threadForRunnable1 = new Thread(runnableTask1, "Thread-For-Runnable-X"); // Explicitly naming the thread
        Thread threadForRunnable2 = new Thread(runnableTask2); // Thread name will be auto-generated (e.g., "Thread-1")

        // Starting the threads that execute the Runnable tasks
        threadForRunnable1.start();
        threadForRunnable2.start();

        // Alternative way using the start() method within MyRunnable (if you added it)
        MyRunnable runnableTask3 = new MyRunnable("Runnable-Z (Self-Starting)", 3);
        runnableTask3.start(); // This encapsulates thread creation and starting

        // --- Thread Management: Waiting for threads to complete (join()) ---
        System.out.println("\nMain thread is now waiting for other threads to complete...");
        try {
            // Wait for thread1 (extends Thread) to complete
            thread1.join();
            System.out.println(thread1.getName() + " has finished.");

            // Wait for thread2 (extends Thread) to complete
            thread2.join();
            System.out.println(thread2.getName() + " has finished.");

            // Wait for threadForRunnable1 (executes Runnable) to complete
            threadForRunnable1.join();
            System.out.println(threadForRunnable1.getName() + " has finished.");

            // Wait for threadForRunnable2 (executes Runnable) to complete
            threadForRunnable2.join();
            System.out.println(threadForRunnable2.getName() + " has finished.");

            // Wait for runnableTask3's internally managed thread to complete
            runnableTask3.join(); // Using the join method we added to MyRunnable
            System.out.println("Runnable-Z-Thread has finished.");


        } catch (InterruptedException e) {
            System.err.println("Main thread interrupted while waiting: " + e.getMessage());
        }

        System.out.println("\n--- All threads have completed. Main Thread Exiting ---");
    }
}

3. Explanation of Differences and Thread Management

Differences between extends Thread and implements Runnable:

Featureextends Threadimplements Runnable
InheritanceYour class becomes a Thread.Your class defines a task that can be run by a Thread.
Multiple InheritanceNot possible if your class already needs to extend another class (Java doesn't support multiple class inheritance).Possible. Your class can extend another class and still implement Runnable (and other interfaces).
Object TypeThe object is a specialized Thread.The object is of your custom type, which is also a Runnable. A separate Thread object is needed to execute it.
Code OrganizationTightly couples the task (code in run()) with the Thread object itself.Promotes loose coupling. The task (Runnable) is separate from the execution mechanism (Thread). This is generally better for design.
Resource SharingLess straightforward if multiple threads need to execute the same instance of a task with shared state (though possible).Easier to share the same Runnable instance among multiple Thread objects, allowing them to operate on the same task logic and potentially shared data (requires careful synchronization).
FlexibilityLess flexible.More flexible and generally the preferred approach.
UsageSimpler for very basic scenarios where the class doesn't need to extend anything else.Recommended for most use cases due to better design and flexibility. Used heavily in thread pools and Executor frameworks.

Why implements Runnable is generally preferred:

  1. Overcomes Single Inheritance Limitation: If your class representing the task needs to extend another superclass, you can still make it runnable by implementing the Runnable interface.
  2. Better Separation of Concerns: The Runnable interface separates the task to be performed from the Thread object that executes it. This leads to cleaner, more modular design.
  3. Resource Sharing: A single Runnable object can be executed by multiple threads. This is useful if multiple threads need to perform the same operation, potentially on shared data (though synchronization then becomes crucial).
  4. Compatibility with Executor Framework: Java's Executor framework (e.g., ThreadPoolExecutor) works primarily with Runnable (and Callable) tasks, making it the more idiomatic choice for modern concurrent programming.

Thread Creation and Management Demonstrated:

  1. Creation:

    • extends Thread: MyThread thread1 = new MyThread(...);
    • implements Runnable:
      • MyRunnable task = new MyRunnable(...);
      • Thread threadForTask = new Thread(task, "OptionalThreadName");
  2. Starting a Thread (start()):

    • thread1.start();
    • threadForTask.start();
    • Important: You must call start() to create a new thread of execution and invoke the run() method. If you call run() directly (e.g., thread1.run()), it will execute in the current thread, not a new one.
  3. The run() Method:

    • This is where the actual logic of the thread goes. It's the entry point for the new thread.
  4. Thread Naming:

    • Threads can be given names (e.g., new Thread(runnable, "MyWorkerThread") or thread.setName("NewName")). This is very useful for debugging. If not named, Java assigns a default name like "Thread-0", "Thread-1", etc.
    • Thread.currentThread().getName() can be used inside run() to get the name of the currently executing thread.
  5. Sleeping (Thread.sleep(milliseconds)):

    • Thread.sleep(50); pauses the current thread for the specified duration.
    • It can throw an InterruptedException, which must be caught or declared. This exception is thrown if another thread interrupts the sleeping thread.
  6. Waiting for a Thread to Die (join()):

    • thread1.join();
    • When a thread calls anotherThread.join(), the calling thread (e.g., the main thread in our demo) will pause and wait until anotherThread completes its execution (i.e., its run() method finishes).
    • This is crucial for scenarios where one thread's work depends on the completion of another.
    • join() can also throw an InterruptedException.
  7. Checking if Alive (isAlive()):

    • thread.isAlive() returns true if the thread has been started and has not yet died (completed its run() method). (Used in MyRunnable.join()).

To Compile and Run:

  1. Save the code into three files: MyThread.java, MyRunnable.java, and ThreadDemo.java.
  2. Compile:
    bash
    javac MyThread.java MyRunnable.java ThreadDemo.java
  3. Run:
    bash
    java ThreadDemo

You will see interleaved output from the different threads, demonstrating concurrent execution. The join() calls ensure that the "All threads have completed" message appears only after all worker threads have finished.

Made with ❤️ for students, by a fellow learner.