Skip to content

Question 4

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

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);
                // Let the thread sleep for a short period to simulate 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; // Optional: to hold the thread object using this Runnable

    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 for distinct output
            }
        } 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.