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):
// 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):
// 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
// 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
:
Feature | extends Thread | implements Runnable |
---|---|---|
Inheritance | Your class becomes a Thread . | Your class defines a task that can be run by a Thread . |
Multiple Inheritance | Not 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 Type | The 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 Organization | Tightly 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 Sharing | Less 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). |
Flexibility | Less flexible. | More flexible and generally the preferred approach. |
Usage | Simpler 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:
- 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. - Better Separation of Concerns: The
Runnable
interface separates the task to be performed from theThread
object that executes it. This leads to cleaner, more modular design. - 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). - Compatibility with Executor Framework: Java's
Executor
framework (e.g.,ThreadPoolExecutor
) works primarily withRunnable
(andCallable
) tasks, making it the more idiomatic choice for modern concurrent programming.
Thread Creation and Management Demonstrated:
Creation:
extends Thread
:MyThread thread1 = new MyThread(...)
;implements Runnable
:MyRunnable task = new MyRunnable(...)
;Thread threadForTask = new Thread(task, "OptionalThreadName");
Starting a Thread (
start()
):thread1.start();
threadForTask.start();
- Important: You must call
start()
to create a new thread of execution and invoke therun()
method. If you callrun()
directly (e.g.,thread1.run()
), it will execute in the current thread, not a new one.
The
run()
Method:- This is where the actual logic of the thread goes. It's the entry point for the new thread.
Thread Naming:
- Threads can be given names (e.g.,
new Thread(runnable, "MyWorkerThread")
orthread.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 insiderun()
to get the name of the currently executing thread.
- Threads can be given names (e.g.,
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.
Waiting for a Thread to Die (
join()
):thread1.join();
- When a thread calls
anotherThread.join()
, the calling thread (e.g., themain
thread in our demo) will pause and wait untilanotherThread
completes its execution (i.e., itsrun()
method finishes). - This is crucial for scenarios where one thread's work depends on the completion of another.
join()
can also throw anInterruptedException
.
Checking if Alive (
isAlive()
):thread.isAlive()
returnstrue
if the thread has been started and has not yet died (completed itsrun()
method). (Used inMyRunnable.join()
).
To Compile and Run:
- Save the code into three files:
MyThread.java
,MyRunnable.java
, andThreadDemo.java
. - Compile:bash
javac MyThread.java MyRunnable.java ThreadDemo.java
- 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.