Chapter 39:Understanding Multithreading in Java: Part 2

Chapter 39:Understanding Multithreading in Java: Part 2

Table of contents

Introduction to Multithreading in Java

Multithreading is a fundamental concept in Java that allows for the concurrent execution of multiple threads, enabling efficient utilization of CPU resources and better performance for tasks that can be parallelized. In Java, threads are lightweight, independent units of execution that can run concurrently, performing separate tasks simultaneously within a single program. This ability to execute multiple tasks at the same time is especially useful in scenarios where programs need to handle a variety of operations such as user interactions, I/O operations, or computational tasks.

Java provides robust support for multithreading, both through the Thread class and the Runnable interface. These tools allow developers to create and manage threads with ease, making multithreading a core feature for applications that demand high performance, responsiveness, or parallel task execution.

This exploration of multithreading in Java covers various aspects such as:

  • Creating Threads: We will examine the two main approaches for creating threads in Java—extending the Thread class and implementing the Runnable interface.

  • Managing Thread Execution: We’ll delve into how threads are executed, including how to start them and manage their life cycle, as well as when to use start() versus run().

  • Thread Naming: Understanding how to name and identify threads is crucial for debugging multithreaded programs. We will look at how to retrieve and set thread names dynamically.

  • Best Practices: Best practices for using multithreading efficiently and effectively in real-world applications will also be discussed.

Multithreading offers powerful ways to handle concurrent tasks, improve program efficiency, and create responsive, high-performing applications. However, it also introduces complexity—race conditions, deadlocks, and other concurrency issues—that developers need to manage carefully. This guide aims to provide a comprehensive understanding of how to work with threads in Java and build robust multithreaded applications.


Table of Contents

  1. Case 5: Overloading the run() Method

    • Explanation of run() method behavior.

    • Examples of overloading with code and output analysis.

  2. Case 6: Overriding the start() Method

    • Why overriding start() is discouraged.

    • Examples showcasing behavior with overridden start().

  3. Case 7: Using super.start() in Overridden start()

    • How to combine custom logic with the parent class’s start() method.
  4. Case 8: Life Cycle of a Thread

    • Thread states: Born, Ready, Running, Dead.

    • Detailed flowchart and code examples.

  5. Best Practices in Multithreading

    • When to use thread overloading, overriding, and explicit calls.

    • Common pitfalls and debugging tips.

  6. Conclusion and Key Takeaways


Case 5: Overloading the run() Method

In Java, we can overload the run() method of the Thread class. This allows us to define multiple run() methods with different parameters. However, it’s essential to understand the behavior when overloading the run() method.

Key Point:

Although we can overload the run() method, the start() method of the Thread class will always invoke the no-argument version of the run() method. If we overload run() with arguments, the thread will still call the zero-argument run() method unless we explicitly call the overloaded run() method within the thread.

Here’s an example:

class MyThread extends Thread {
    // No-argument run method
    public void run() {
        System.out.println("No-argument method");
    }

    // Overloaded run method with an argument
    public void run(int i) {
        System.out.println("Method with integer argument: " + i);
    }
}

public class ThreadDemo {
    public static void main(String... args) {
        MyThread t = new MyThread();
        t.start();  // Invokes run() with zero arguments
    }
}

Output:

No-argument method

Which run() method is called by JVM?

When we invoke t.start(), the thread calls the run() method with zero arguments, even though we have overloaded the method. This is because start() invokes the run() method with no parameters. The overloaded run(int i) method is not called automatically.

Understanding the Code Behavior

Let's break down the flow of the program when we call t.start():

  1. The start() method of the Thread class is invoked.

  2. This method internally calls the run() method with no arguments.

  3. The run() method executes the task defined in it and prints the message "No-argument method".

After the thread completes its execution, the main thread proceeds to execute the rest of the code, which could include further processing or printing messages.

class MyThread extends Thread {
    public void run() {
        System.out.println("No-argument run method");
        run(5);  // Explicitly calling the overloaded run method
    }

    public void run(int i) {
        System.out.println("Int argument run method: " + i);
    }
}

public class Multithreading8 {
    public static void main(String[] args) {
        MyThread t = new MyThread();
        t.start();  // Start the thread
        for (int i = 0; i < 5; i++) {
            System.out.println("Main thread");
        }
    }
}

Output:

Main thread
Main thread
Main thread
Main thread
Main thread
No-argument run method
Int argument run method: 5

Code Explanation:

  1. Initially, the start() method is called, and the thread's run() method with zero parameters is invoked. This prints "No-argument run method".

  2. The run() method with an integer argument is explicitly called within the run() method itself, printing "Int argument run method: 5".

  3. Meanwhile, the main thread executes its own task and prints "Main thread" five times.

This demonstrates how threads work concurrently, with context switching happening between the threads managed by the Java Virtual Machine (JVM) and the thread scheduler.

How to Call run() with Parameters?

If you want to call the run() method with parameters, you can do so by invoking it explicitly within the run() method. Here's an example where we explicitly call the overloaded run(int i) method from within the run() method:

class MyThread extends Thread {
    public void run() {
        System.out.println("No-argument run method");
        run(5);  // Explicitly calling the overloaded run method
    }

    public void run(int i) {
        System.out.println("Int argument run method: " + i);
    }
}

public class Multithreading8 {
    public static void main(String[] args) {
        MyThread t = new MyThread();
        t.start();  // Start the thread
        for (int i = 0; i < 5; i++) {
            System.out.println("Main thread");
        }
    }
}

Output Explanation:

  • The run() method with zero arguments is called by the JVM through the start() method.

  • The zero-argument run() method calls the overloaded run(int i) method explicitly.

  • The main thread continues to execute its own tasks.

Explicitly Calling the Overloaded run() Method from Main Thread

You can also explicitly call the overloaded run() method from the main thread (or any other thread) without using the start() method. However, this will not start a new thread; it will simply call the method as a normal method of the class.

class MyThread extends Thread {
    public void run() {
        System.out.println("No-argument run method");
        run(5);  // Calling overloaded run method
    }

    public void run(int i) {
        System.out.println("Int argument run method");
    }
}

public class Multithreading8 {
    public static void main(String[] args) {
        MyThread t = new MyThread();
        t.start();  // This calls run() with zero arguments
        t.run(5);   // This explicitly calls the overloaded run() method
    }
}

Output:

No-argument run method
Int argument run method

In this case, after t.start() calls the run() method with zero arguments, we explicitly call the overloaded run(int i) method from the main thread. Note that this doesn’t invoke multi-threading behavior since it’s executed in the main thread.

Context Switching in Multi-threading

When multiple threads are running concurrently, the JVM manages the switching between threads using a concept known as context switching. The JVM and the underlying operating system handle thread scheduling and switching between threads. This is critical to achieving true multi-threading.

For example:

  • Thread t1 starts executing, and before it finishes, the JVM may suspend it and start executing thread t2.

  • The JVM switches between the two threads, ensuring they execute in parallel (depending on the number of processors and the OS scheduler).

In this example, context switching happens between the main thread and the new thread t. The main thread prints "Main thread" multiple times while the new thread executes its run() method.

Possible Outputs in Multi-threading

Due to the non-deterministic nature of thread scheduling, the output of a multi-threaded program can vary. Here are a few possible outputs:

Output 1:

Int argument run method
No-argument run method
Main thread
Main thread
Main thread
Main thread
Main thread

Output 2:

No-argument run method
Int argument run method
Main thread
Main thread
Main thread
Main thread
Main thread

Case-6: Overriding of start() Method

Concept:

  • Overriding start(): If we override the start() method in the Thread class, our start() method will be executed like a normal method, but no new thread will be created, and no new thread will be started. This prevents the creation of a new thread and blocks the thread scheduler from informing about it.

  • Multithreading is not implemented in this case because overriding the start() method stops the creation of a new thread.

  • Recommendation: It is never recommended to override the start() method. The primary duty is to override the run() method, which is the task-defining method for threads.


How Many Threads Are There in the Code and What Is the Output?

  • Thread Behavior:

    • Here, the start() method present in the MyThread class is called, but there is no possibility of creating a new thread and informing the thread scheduler about it.

    • Since the overridden start() method in MyThread is called, no new thread is created.

    • The start() method in MyThread will be executed by the main thread only, and hence multithreading is not implemented in this case.

    • Multithreading is only implemented when the start() method in the Thread class is called. In this case, we are overriding the start() method, which prevents the creation of a new thread.

    • Overriding the run() method is our prime duty to define the task for the thread, but overriding start() should be avoided.


Code Explanation:

class MyThread extends Thread {
    @Override
    public void start() {
        // Custom start method, but no new thread is created
        System.out.println("start method is called");
    }

    public void run() {
        // Run method that would be executed in the thread, but it will not run in a new thread here
        System.out.println("No argument run method");
    }    
}

public class Multithreading9 {
    public static void main(String[] args) {
        MyThread t = new MyThread();

        // Here, since our class's start() method is called, no new thread is created.
        t.start(); 

        // Task for the main thread (this will execute in the main thread)
        for (int i = 0; i < 5; i++) {
            System.out.println("Main thread");
        }
    }
}

Output:

start method is called
Main thread
Main thread
Main thread
Main thread
Main thread

Code Breakdown:

  1. Overridden start() Method:

    • The start() method in MyThread is called by the main thread, which prints "start method is called".

    • No new thread is created, and the start() method does not initiate multithreading.

  2. run() Method:

    • The run() method in MyThread is defined, but it is not called by the thread scheduler because no new thread is created by calling the overridden start() method.

    • The run() method would normally be executed in a new thread, but since there is no new thread, it doesn't run.

  3. Main Thread:

    • The main method proceeds to execute the task intended for the main thread.

    • The loop prints "Main thread" five times as part of the main thread’s execution.


Summary of the Behavior:

  • Overridden start() method: When the start() method is overridden, the task for multithreading is skipped. The start() method is executed like any normal method and does not create a new thread.

  • No New Thread Created: Since the overridden start() method does not create a new thread, multithreading is not achieved in this case.

  • Execution: The overridden start() method runs in the main thread, and the main thread continues to execute its task.

  • No Context Switching: There is no context switching between threads since there is only the main thread.


Key Takeaways:

  1. Overriding start(): When you override the start() method, no new thread is created, and it behaves like a regular method.

  2. Multithreading: Multithreading is not implemented if start() is overridden.

  3. Best Practice: The best practice is to override the run() method for defining the thread's task, but do not override the start() method.

  4. Recommendation: Always rely on the start() method of the Thread class to initiate multithreading properly.


Case-7: Overriding start() Method and Using super.start() to Call the Parent Class's start() Method

Concept:

  • Overriding the start() Method: In this case, the start() method is overridden in the user-defined MyThread class. The super.start() is used to invoke the start() method from the parent class Thread, which is responsible for starting a new thread.

  • How Many Threads Will Be Created?

    • 2 threads will be created in this case:

      1. Main Thread: The main thread calls the overridden start() method and executes its task.

      2. User-Defined Thread: The new thread, created by the Thread class's start() method, executes the run() method in the MyThread class.

Code Explanation:

  1. Line 1 (t.start()):

    • In the main() method, we call the start() method of the MyThread object t. This triggers the overridden start() method in MyThread.
  2. Line 2 (public void start()):

    • We override the start() method in the MyThread class.

    • The first action inside the overridden start() method is to call super.start().

      • super.start() calls the start() method of the parent class (Thread), which handles the creation of a new thread and registers it with the thread scheduler.

      • This starts a new thread and invokes the run() method.

  3. Line 3 (super.start()):

    • The super.start() call creates a new thread, and the run() method in MyThread is executed by this new thread.

    • After that, the control returns to the main() method, and the task in the main thread is executed.

    • Since we are working with two threads, context switching will occur between the threads managed by the JVM's thread scheduler.

  4. Context Switching:

    • Context switching happens between the main thread and the user-defined thread.

    • The JVM schedules the execution of both threads, so both can run concurrently.


Thread Class Implementation Overview:

The Thread class is the heart of multi-threading. The start() method is crucial for creating and managing threads.

class Thread {
    // Heart of Multi-threading
    public void start() {
        // 1. Register the thread with the Thread Scheduler
        // 2. Perform all the mandatory low-level activities (Memory level)
        // 3. Call the run() method
        // The run() method is invoked, which in this case will be in the MyThread class
    }
}

Code Explanation with super.start():

class MyThread extends Thread {
    @Override
    public void start() {   // --> Line 2
        // super.start() calls the parent class's start() method
        super.start();       // --> Line 3
        System.out.println("start method is called");
    }

    @Override
    public void run() {
        System.out.println("No argument run method");
    }
}

public class Multithreading10 {
    public static void main(String[] args) {
        MyThread t = new MyThread();

        t.start();  // --> Line 1

        // Task for main thread
        System.out.println("Main thread");
    }
}

Output:

start method is called
Main thread
No argument run method

Explanation of the Output:

  1. start() Method Call:

    • The main() method calls the start() method of the MyThread object t at Line 1. This triggers the overridden start() method in the MyThread class.
  2. Calling super.start():

    • Inside the overridden start() method, the super.start() method is called. This starts a new thread, and the run() method is executed in that new thread.

    • The output "start method is called" is printed from the start() method.

  3. Main Thread Execution:

    • After calling super.start(), control returns to the main() method, and the main thread continues its execution, printing "Main thread".
  4. User-Defined Thread Execution:

    • The new thread, which was started by super.start(), executes the run() method of MyThread. This prints "No argument run method".

Summary of the Behavior:

  • Two Threads Created:

    • Main thread: Executes the main() method and prints "Main thread".

    • User-defined thread: Created by super.start() and executes the run() method, printing "No argument run method".

  • Context Switching:

    • Context switching occurs between the main thread and the user-defined thread managed by the JVM's thread scheduler. Both threads run concurrently, leading to unpredictable execution order.
  • Best Practice:

    • Overriding start() is generally not recommended, as it's better to just override the run() method and rely on the parent Thread class to handle thread creation.

Key Takeaways:

  1. Two Threads Created: One from the main method and one from the Thread class's start() method.

  2. Use of super.start(): Invoking super.start() in the overridden start() method ensures the creation of a new thread.

  3. Context Switching: The JVM handles the execution of both threads, and context switching happens between them.

  4. Thread Execution Order: Since both threads run concurrently, the output order can vary.


Case-8: Life Cycle of a Thread in Java

In Java, the thread life cycle describes the various stages a thread goes through from creation to completion. Below is a detailed explanation of each state in the thread's life cycle:

1. Born State (New State)

  • Thread Object Creation:
    When a thread is created, it is in the born state or new state. At this point, the thread object has been instantiated but has not yet started execution.

      MyThread t = new MyThread(); // Thread is in born (new) state
    
  • Explanation:
    A thread enters the born state when an instance of the Thread class (or its subclass) is created. However, it does not start executing yet.


2. Ready/Runnable State

  • Calling start() Method:
    After a thread object is created, the start() method is called. This moves the thread to the ready or runnable state, meaning it is now ready to be executed by the Thread Scheduler.

      t.start(); // Thread is in ready/runnable state
    
  • Explanation:
    The start() method doesn't directly run the run() method of the thread. Instead, it tells the Thread Scheduler to begin managing this thread, making it available for execution. At this point, the thread is ready to be allocated CPU time.


3. Running State

  • Thread Scheduler Allocates CPU Time:
    If the Thread Scheduler allocates CPU time to the thread, it enters the running state. Here, the thread begins executing its run() method.

  • Explanation:
    The Thread Scheduler decides which thread gets to run based on various factors such as priority and availability of CPU resources. If the thread is selected by the scheduler, it enters the running state and executes its run() method.


4. Dead State

  • Completion of run() Method:
    Once the thread's run() method has finished executing, the thread enters the dead state. A thread cannot be started again after it reaches the dead state.

  • Explanation:
    The thread finishes its task, and there is no further work to be done by the thread. Once the run() method completes, the thread is considered dead and is no longer usable.


Thread Life Cycle Overview:

The life cycle of a thread follows these stages:

  1. Born State (New):

    • Thread object is created using new keyword, but it has not yet started execution.
  2. Ready/Runnable State:

    • After calling the start() method, the thread enters the ready or runnable state, waiting for CPU time.
  3. Running State:

    • When the Thread Scheduler allocates CPU time, the thread enters the running state and begins executing its run() method.
  4. Dead State:

    • After the run() method completes, the thread enters the dead state, signifying that the thread has finished its execution and cannot be reused.

Visual Representation of Thread Life Cycle:

Born State
   |  
   v
Ready/Runnable State
   |
   v
Running State
   |
   v
Dead State

Explanation of Transitions:

  1. Born State to Ready/Runnable:

    • The thread enters the ready/runnable state when the start() method is called on the thread object.
  2. Ready/Runnable to Running:

    • The Thread Scheduler allocates CPU time to the thread, and it moves into the running state, where the run() method is executed.
  3. Running to Dead:

    • When the thread finishes executing its run() method, it enters the dead state.

Summary of Thread Life Cycle Stages:

  • Born State (New): The thread has been created but not yet started.

  • Ready/Runnable State: The thread is ready to run, waiting for the CPU.

  • Running State: The thread is executing its run() method.

  • Dead State: The thread has finished its execution and cannot be restarted.


This life cycle is essential for understanding how threads work in Java, and it helps in efficiently managing threads to ensure optimal performance in multi-threaded applications.

Case-9: IllegalThreadStateException in Java

In Java, once a thread is started using the start() method, it cannot be started again. If you try to call start() on an already started thread, the JVM throws an IllegalThreadStateException at runtime. This occurs because a thread can only be started once, and attempting to restart it leads to an illegal operation.


Explanation of the Issue

When a thread is created and started, it goes through the states of the thread life cycle. Once the thread enters the running state and completes its task (i.e., the run() method finishes executing), the thread is considered dead. A thread that has already been started cannot be restarted or re-executed.

Attempting to start an already started thread causes the IllegalThreadStateException to be thrown.


Code Example

class MyThread extends Thread {
    public void run() {
        System.out.println("No argument run method");
    }
}

public class Multithreading12 {
    public static void main(String[] args) {
        MyThread t = new MyThread();

        // Calling start() of MyThread - First time
        t.start();  // --> Line-1

        // Calling start() of the same thread - Second time (This will throw IllegalThreadStateException)
        t.start();

        // Task for the main thread
        System.out.println("Main thread");
    }
}

Explanation of the Code

  1. First t.start() Call:

    • The start() method is called for the first time. The thread enters the ready/runnable state and eventually starts executing the run() method. The output is "No argument run method" from the run() method.
  2. Second t.start() Call:

    • The second attempt to call start() on the same thread t causes the IllegalThreadStateException. This is because once a thread is started, it cannot be started again. This leads to a runtime exception being thrown.
  3. Main Thread Task:

    • The main method continues and prints "Main thread", which will be displayed after the exception is thrown, if the exception is not caught.

Output

No argument run method
Exception in thread "main" java.lang.IllegalThreadStateException
        at java.base/java.lang.Thread.start(Thread.java:793)
        at new2.main(new2.java:17)

Cause of the Exception

  • The thread t has already been started once, and attempting to start it again leads to the IllegalThreadStateException. This exception is part of the runtime exceptions in Java, meaning it occurs during the execution of the program.

Key Points to Remember

  • IllegalThreadStateException is thrown if you attempt to start a thread that has already been started.

  • A thread in Java can only be started once; attempting to restart it will result in a runtime exception.

  • The start() method should only be invoked once on a thread. Once a thread has completed its task (i.e., the run() method has finished), it cannot be restarted.

This exception is a common pitfall when working with threads in Java. Understanding the life cycle of a thread and managing thread states properly is crucial to avoid such errors in multi-threaded applications.

Here’s a detailed explanation using each and every word from your text, ensuring clarity and completeness:


7. Creation of Thread Using Runnable Interface

In Java, there are two common ways to create a thread:

  1. Using the Thread Class(extending it directly):

    • Use the start() method from the Thread class to initiate a thread.

    • Override the run() method to define the job or task of the thread.

  2. Using the Runnable Interface(implementing it and passing it to a Thread object):

    • Implement the java.lang.Runnable interface.

    • Provide an implementation for the run() method.

    • Pass the Runnable object to a Thread instance and call start() to execute the thread.


Exploring Runnable

Using javap Command:
javap java.lang.Runnable

The output of this command is:

Compiled from "Runnable.java"
public interface java.lang.Runnable {
    public abstract void run();
}
  • The Runnable interface contains a single abstract method run().

  • A functional interface has exactly one abstract method, enabling the use of lambda expressions in Java.


Why Use the Runnable Interface?

Challenges Addressed by Sun Microsystems (SUNMS):

  • SUNMS designed the Runnable interface as part of their Software Requirement Specifications (SRS) for creating threads in Java.

  • An interface represents the SRS by defining rules that developers must follow to create threads.

  • Directly implementing all the required features would be cumbersome for developers. SUNMS provided the Thread class as an adapter to reduce this burden.

Role of the Thread Class as an Adapter:

  • Performs low-level tasks such as:

    1. Registering the thread with the Thread Scheduler.

    2. Handling memory-level activities.

    3. Invoking the run() method.

  • Developers only need to focus on the job or task for the thread by implementing the run() method.

Example of Thread Class as an Adapter
interface Runnable {
    public abstract void run();
}

class Thread implements Runnable { // Adapter class
    public void start() {
        // 1. Register the thread with the Thread Scheduler
        // 2. Perform low-level activities (memory management)
        // 3. Call the run() method
    }


    public void run() {
        // Define the job for the thread
    }
}

Creating Threads Using the Runnable Interface

Example Code

class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("Task executed by Runnable thread.");
    }
}

public class RunnableExample {
    public static void main(String[] args) {
        MyRunnable task = new MyRunnable(); // Task defined
        Thread t = new Thread(task);        // Pass task to Thread
        t.start();                          // Start the thread
    }
}

Functional Interface and Lambda Expressions

  • Since Runnable is a functional interface, it allows lambda expressions for concise thread creation.

Example with Lambda Expression

public class LambdaExample {
    public static void main(String[] args) {
        Runnable task = () -> System.out.println("Task executed with Lambda!");
        Thread t = new Thread(task);
        t.start();
    }
}

1. Creating a Thread Using java.lang.Thread Class

  • Step 1: You can create a thread by extending the Thread class.

  • Step 2: Override the run() method to define the task or job the thread will perform.

  • Step 3: Call the start() method to begin the thread's execution.


Code Example (Using Thread Class)

class MyThread extends Thread {
    @Override
    public void run() {
        // This is the task for the thread
        System.out.println("Thread is running.");
    }
}

public class ThreadExample {
    public static void main(String[] args) {
        MyThread t = new MyThread();
        t.start();  // Starts the thread, which will execute the run() method
    }
}

2. Creating a Thread Using the Runnable Interface

Why Use the Runnable Interface?

  • SRS (Software Requirement Specifications): Sun Microsystems introduced the Runnable interface to provide a standardized way of creating threads. Instead of forcing developers to extend the Thread class, which can lead to limitations (since Java supports single inheritance), the Runnable interface allows developers to separate the thread’s task from the thread's management.

  • Flexibility: The Runnable interface offers a more flexible approach for creating threads, as it allows any class to implement the interface, while still extending another class.

  • Functional Interface: The Runnable interface contains only one abstract method run(), which makes it a functional interface. This allows it to be used with lambda expressions in Java 8 and beyond.

Runnable Interface Code Example

 MyRunnable implements Runnable {
    @Override
    public void run() {
        // This is the task for the thread
        System.out.println("Runnable thread is running.");
    }
}

public class RunnableExample {
    public static void main(String[] args) {
        MyRunnable myRunnable = new MyRunnable();
        Thread t = new Thread(myRunnable);  // Pass Runnable to Thread
        t.start();  // Starts the thread
    }
}

Explanation of Key Concepts

  • Runnable Interface:
    The Runnable interface in Java defines a single method: run(). The run() method contains the code that will be executed when the thread starts. The interface makes the thread task independent of the thread management, allowing the same task to be used across multiple threads.

  • Thread Class:
    The Thread class in Java implements the Runnable interface, meaning that it can accept any Runnable object and execute its run() method. The Thread class provides higher-level management features like starting the thread, controlling the thread’s state, etc.


How Java Creates a Thread Using Runnable Interface

  1. Thread Management:
    The Thread class is often referred to as an adapter class because it helps reduce the burden on developers. It does the low-level work (e.g., memory management, thread scheduling) that would otherwise be cumbersome for developers.

  2. Adapter Class:
    The Thread class can be thought of as an adapter because it provides default implementations for thread-related tasks, while the developer is only responsible for implementing the task (i.e., the run() method).

  3. Separation of Task and Thread Management:
    With the Runnable interface, the thread’s task is decoupled from the thread management. The developer can focus on the task itself, while the Thread class takes care of managing the thread.


Using javap to Explore Classes

  • Command 1:
    javap java.lang.Runnable
    This command shows the definition of the Runnable interface. It confirms that Runnable is a functional interface with one abstract method, run().

      public interface java.lang.Runnable {
          public abstract void run();
      }
    
  • Command 2:
    javap java.lang.Thread
    This reveals that Thread implements the Runnable interface, so it is capable of handling tasks defined in the run() method.

       Thread implements Runnable {
          public void start() {
              // Registers the thread with the thread scheduler
              // Other low-level activities
              // Calls run() method
          }
    
          public void run() {
              // Job for the thread
          }
      }
    

Eclipse Shortcuts for Thread Exploration

  • Ctrl + Shift + T:
    Opens the definition of any class. Use this to view the definition of the Thread class or any other class.

  • Ctrl + O:
    Lists all the methods inside the class, allowing you to easily navigate through the methods of the Thread class or any other class.


Key Points to Remember

  • Functional Interface:
    Runnable is a functional interface, which makes it compatible with Java’s lambda expressions, offering a more concise way to create thread tasks.

  • Advantages of Runnable:

    • Allows any class to implement Runnable without having to extend Thread.

    • Enables a thread's task to be reused across different threads.

    • Increases flexibility by separating thread creation from task definition.

  • Thread as an Adapter Class:

    • The Thread class simplifies thread management by providing default implementations for creating and starting threads, leaving only the task (i.e., the run() method) for the developer to implement.

Code-1: Thread Creation Using Runnable Interface

Code Explanation

  1. Object Creation:

    • A MyThread object is created in the main method.
  2. start() Method and Multithreading:

    • The start() method is considered the heart of multithreading.

    • However, in this case, the start() method is not defined in the MyThread class.

  3. JVM Checks for start() Method:

    • The JVM first checks for the start() method in the MyThread class.

    • Since the start() method is not present in MyThread, the JVM checks in its parent class, which is Object.

    • The start() method is not found in the Object class either.

  4. Parent Class Hierarchy:

    • Class Level:

      • Object is the ultimate parent of MyThread, and as mentioned, it doesn't have a start() method.

      • The JVM checks MyThread and Object for the method but doesn't find it.

    • Interface Level:

      • The JVM also checks the Runnable interface for the start() method, but it’s not defined in the Runnable interface either.

      • Since the start() method is not found in either Object or Runnable, it throws a ClassNotFoundException.

  5. Where start() Method Is Found:

    • The start() method is actually present in the Thread class, which is designed to handle multithreading.

Code for Thread Creation Using Runnable Interface

// Runnable Interface
interface Runnable {
    public abstract void run();  // This is the method that will be executed in a new thread
}

// Thread Class Implementing Runnable
class Thread implements Runnable {  // Adapter class for Runnable interface
    public void start() {
        // 1. Register the thread with ThreadScheduler
        // 2. Perform mandatory low-level memory activities
        // 3. Invoke or call run() method
    }

    public void run() {
        // Define the task for the thread to execute
    }
}

// MyThread Class Implementing Runnable
class MyThread implements Runnable {
    @Override
    public void run() {
        for (int i = 1; i < 10; i++) {
            System.out.println("Child Thread");
        }
    }
}

public class Multithreading13 {
    public static void main(String[] args) {
        MyThread t = new MyThread(); // Create the thread object

        // Start the thread (incorrect usage)
        t.start(); // This will throw an error because 'start()' is not defined in MyThread

        // Task for the main thread
        for (int i = 1; i <= 10; i++) {
            System.out.println("Main Thread");
        }
    }
}

Explanation of the Error:

  • Error: Could not find or load main class Multithreading13

  • Caused by: java.lang.ClassNotFoundException: Multithreading13

The error occurs because of the incorrect usage of the start() method in MyThread. MyThread implements Runnable, but it does not extend Thread, so it cannot directly call start(). The start() method is defined in the Thread class, not the Runnable interface.


How to Fix the Code:

To fix this issue, we need to ensure that the MyThread class is either extended from Thread or that we create a Thread object with MyThread as its Runnable task.

Fixed Code Example:
// MyThread Class Implementing Runnable
class MyThread implements Runnable {
    @Override
    public void run() {
        for (int i = 1; i < 10; i++) {
            System.out.println("Child Thread");
        }
    }
}

public class Multithreading13 {
    public static void main(String[] args) {
        MyThread t = new MyThread();  // Create the Runnable task

        Thread thread = new Thread(t);  // Pass the task to a Thread object
        thread.start();  // Start the thread

        // Task for the main thread
        for (int i = 1; i <= 10; i++) {
            System.out.println("Main Thread");
        }
    }
}

Output:

With the corrected code, the output will be:

Main Thread
Main Thread
Main Thread
Main Thread
Main Thread
Main Thread
Main Thread
Main Thread
Main Thread
Main Thread
Child Thread
Child Thread
Child Thread
Child Thread
Child Thread
Child Thread
Child Thread
Child Thread
Child Thread
Child Thread
  • The main thread and the child thread will print their respective tasks.

  • The start() method works correctly here because the MyThread task is wrapped in a Thread object, which is responsible for managing the multithreading.


Key Points to Remember:

  1. start() Method is the heart of multithreading and is defined in the Thread class.

  2. Runnable Interface: It only defines the run() method, which represents the task to be executed by a thread.

  3. Thread Class: It is responsible for managing threads and has the start() method, which initiates the execution of the run() method.

  4. Common Mistake: You cannot call start() directly on a class that only implements Runnable. You must create a Thread object and pass the Runnable task to it.


Code-2: Using public java.lang.Thread() Constructor

Explanation:

  1. Thread Creation Using Zero-Parameter Constructor:

    • By using the Thread() constructor with zero parameters, the start() method of the Thread class is internally called.

    • This start() method invokes the run() method from the Thread class. However, the run() method in the Thread class has no implementation—it is essentially an empty method.

    • As a result, when you create a new thread using this constructor, it will invoke the empty run() method of the Thread class, and no actual task will be executed by that thread.

  2. How to Create a Thread:

    • To create a thread, an object of the Thread class is instantiated.

    • The start() method is called on this object to initiate the thread.

    • Once the start() method is invoked, it internally calls the run() method of the Thread class, which does nothing because it has no implementation.

  3. Issue: Calling the run() Method from Thread Class:

    • When a thread is created using the zero-parameter Thread() constructor, it will call the run() method from the Thread class. But, since this run() method has no meaningful implementation, no task is executed.

    • What we want instead is for the run() method from the MyRunnable class to be invoked when the thread starts.

  4. How to Resolve This Issue?:

    • To make the thread execute the run() method from MyRunnable, we need to pass an instance of MyRunnable to the Thread constructor.

    • By doing this, we tell the Thread class to invoke the run() method from the MyRunnable class, which contains the actual logic for the task to be executed by the thread.

  5. How Does Java Handle This?:

    • The solution is to pass a Runnable implementation to the Thread constructor.

    • This way, the Thread class will invoke the run() method of the Runnable implementation passed to it instead of the empty run() method in the Thread class.

Understanding Thread Constructors in Java

  • Constructor 1: public java.lang.Thread()

    • This constructor creates a new thread but invokes the run() method from the Thread class, which has no implementation.

    • If you use this constructor and call start(), it will call the empty run() method in Thread class, and no task will be executed.

  • Constructor 2: public java.lang.Thread(java.lang.Runnable)

    • This constructor allows you to pass a Runnable object to the Thread constructor.

    • When start() is called, it will invoke the run() method from the Runnable object (e.g., MyRunnable), which contains the task you want to execute.


Code Example: Using the Thread() Constructor

Code Explanation:

// MyRunnable class implements Runnable interface
class MyRunnable implements Runnable {
    @Override
    public void run() {
        for (int i = 1; i < 5; i++) {
            System.out.println("Child Thread");
        }
    }
}

public class Multithreading15 {
    public static void main(String[] args) {
        MyRunnable r = new MyRunnable();  // Create an instance of MyRunnable

        // Create a Thread object using the zero-parameter constructor
        Thread t = new Thread();  // No Runnable passed
        t.start();  // Start the thread, but run method of Thread class will be called

        // Main thread task
        for (int i = 1; i < 5; i++) {
            System.out.println("Main Thread");
        }
    }
}

Output:

Main Thread
Main Thread
Main Thread
Main Thread
  • In this code:

    • Thread t = new Thread(); creates a Thread object using the constructor that doesn't take any parameters.

    • When t.start(); is called, the run() method from the Thread class is executed, but it has no implementation, so it does nothing.

    • The main thread runs its own task and prints "Main Thread" four times.

Fix: Using Runnable Interface with Thread

To ensure that the run() method from MyRunnable is executed, we need to pass MyRunnable to the Thread constructor. Here's how you can correct the code:

public class Multithreading15 {
    public static void main(String[] args) {
        MyRunnable r = new MyRunnable();  // Create an instance of MyRunnable

        // Create a Thread object using the constructor that takes Runnable
        Thread t = new Thread(r);  // Pass MyRunnable as the task
        t.start();  // Start the thread, which will execute MyRunnable's run()

        // Main thread task
        for (int i = 1; i < 5; i++) {
            System.out.println("Main Thread");
        }
    }
}

Output (Fixed Code):

Main Thread
Main Thread
Main Thread
Main Thread
Child Thread
Child Thread
Child Thread
Child Thread
  • Now, both the main thread and the child thread print their respective messages.

  • The child thread prints "Child Thread" as defined in the run() method of MyRunnable.

  • The main thread prints "Main Thread".


Key Takeaways:

  1. Thread Constructor:

    • The zero-parameter constructor of the Thread class does not allow any task execution unless overridden.

    • The task to be executed by the thread should be passed via Runnable when creating the Thread object.

  2. Thread vs. Runnable:

    • Thread class: It can be used directly to create a thread, but by default, its run() method does nothing.

    • Runnable interface: It allows you to define a task (via run() method) that can be executed by a thread.

  3. Proper Thread Creation:

    • To create a thread that performs useful work, always pass a Runnable implementation (like MyRunnable) to the Thread constructor.

Code-3: Creating a Thread Using the Runnable Interface

Explanation:

In this code, we use the public java.lang.Thread(java.lang.Runnable) constructor to create a thread using the Runnable interface. Here's a step-by-step breakdown:

  1. The Runnable Interface:

    • The Runnable interface defines a single method, run(), which contains the task that should be executed by a thread.

    • The run() method has no return type and is intended to be overridden by a class that implements Runnable to specify the task for the thread.

  2. Thread Creation Using Runnable:

    • The Thread constructor that takes a Runnable object as a parameter allows the start() method to be called from the Thread class.

    • However, instead of invoking the empty run() method from the Thread class (like in the previous case with the zero-parameter constructor), this constructor invokes the run() method from the MyRunnable class.

  3. How This Works:

    • MyRunnable object is created first. This class implements the Runnable interface and defines the run() method, where the actual task for the thread is specified.

    • A new Thread object is then created with the MyRunnable object passed to it.

    • t.start() is called to start the thread. The start() method in the Thread class registers the thread with the Thread Scheduler and triggers the run() method in the MyRunnable class.

  4. Multithreading:

    • At this point, there are two threads running concurrently:

      1. The main thread: It runs the main method and prints "Main Thread" four times.

      2. The user-defined thread: This thread executes the run() method in MyRunnable, printing "Child Thread" four times.

    • Since both threads are running simultaneously, context switching happens between them, meaning the CPU alternates between executing the main thread and the user-defined thread.


Code Explanation:

// MyRunnable implements Runnable interface
class MyRunnable implements Runnable {
    @Override
    public void run() {
        for (int i = 1; i < 5; i++) {
            System.out.println("Child Thread"); // Prints "Child Thread"
        }
    }
}

public class Multithreading16 {
    public static void main(String[] args) {
        MyRunnable r = new MyRunnable(); // Line-1: Create MyRunnable object

        // Create a new thread and pass MyRunnable to it
        Thread t = new Thread(r); // Creates a new thread for MyRunnable
        t.start(); // Start the user-defined thread

        // Main thread executes this code
        for (int i = 1; i < 5; i++) {
            System.out.println("Main Thread"); // Prints "Main Thread"
        }
    }
}

Step-by-Step Execution:

  1. Line 1: A new MyRunnable object is created.

    • MyRunnable r = new MyRunnable();

    • This object contains the run() method that will execute the task for the user-defined thread.

  2. Creating and Starting the Thread:

    • Thread t = new Thread(r);

    • A new Thread object t is created, passing the MyRunnable object as a parameter.

    • t.start(); starts the thread and calls the run() method of MyRunnable.

  3. Thread Execution:

    • The main thread (the one running the main method) continues its execution and prints "Main Thread" four times.

    • Meanwhile, the user-defined thread starts executing the run() method of MyRunnable and prints "Child Thread" four times.

    • Context switching happens here, where the CPU alternates between the two threads, allowing them to execute concurrently.


Output:

Main Thread
Child Thread
Main Thread
Main Thread
Main Thread
Child Thread
Child Thread
Child Thread
  • The output alternates between "Main Thread" and "Child Thread", demonstrating the concept of multithreading.

  • The main thread prints "Main Thread" four times, and the user-defined thread prints "Child Thread" four times.


Key Takeaways:

  1. Runnable Interface:

    • The Runnable interface provides a way to define the task to be executed by a thread by implementing the run() method.
  2. Thread Constructor with Runnable:

    • By passing a Runnable object to the Thread constructor (public Thread(Runnable target)), the Thread class invokes the run() method of the Runnable implementation when the start() method is called.
  3. Multithreading:

    • In this example, two threads are executing concurrently: the main thread and the user-defined thread (created using the Runnable interface).

    • Context switching occurs between the two threads, meaning the CPU alternates between executing the code from the main thread and the user-defined thread.

  4. Using start() vs. run():

    • The start() method in Thread class causes the thread to begin executing, and it invokes the run() method where the actual task is defined.

    • If you directly call the run() method, it will simply execute in the current thread (main thread), not creating a new thread.


Improvement Note:

To ensure that both threads work as expected, avoid calling run() directly. Always use start() to initiate a new thread and allow for concurrent execution.

Case Study: Thread Creation and Output Analysis

Given Code:

class MyRunnable implements Runnable {
    @Override
    public void run() {
        // Task-2: This task is executed by the thread associated with MyRunnable
        for (int i = 1; i < 5; i++) {
            System.out.println("Child Thread");
        }
    }
}

public class Multithreading17 {
    public static void main(String[] args) {
        MyRunnable r = new MyRunnable();
        Thread t1 = new Thread(); // Thread t1 created without Runnable
        Thread t2 = new Thread(r); // Thread t2 created with Runnable

        t1.start(); // Starts t1, but it executes the run method of Thread class
        for (int i = 1; i < 5; i++) {
            System.out.println("Main Thread"); // Task-1: Executed by the main thread
        }
    }
}

Analysis:

In this case, two threads are involved:

  • t1: Created using the Thread constructor without a Runnable object.

  • t2: Created using the Thread(Runnable) constructor, passing the MyRunnable object.

However, the code only calls start() on t1, not on t2. Let's break down what happens when the program runs:

  1. Thread t1:

    • Thread t1 = new Thread(); creates a thread without a Runnable.

    • When t1.start() is called, the start() method from the Thread class is invoked.

    • Since t1 was not provided with a Runnable, it will invoke the run() method defined in the Thread class itself.

    • The Thread.run() method has no implementation (empty), so no task is executed by t1.

  2. Main Thread:

    • The main thread will execute the for loop and print "Main Thread" four times.
  3. Thread t2:

    • Thread t2 = new Thread(r); creates a thread with MyRunnable.

    • However, t2.start() is never called in this code, so the run() method in MyRunnable is never executed.

    • Therefore, Child Thread is not printed by t2.

How many threads?

  • 2 threads will be created in this case:

    1. Main thread: The default thread running the main method.

    2. Thread t1: A new thread created but it doesn't execute any task due to the empty run() method in the Thread class.

Since t2.start() is not called, Thread t2 does not actually run.

Output:

Since only t1.start() is invoked, and t1 does nothing (because the run() method in Thread is empty), the main thread will execute the for loop and print "Main Thread" four times.

The output will be:

Main Thread
Main Thread
Main Thread
Main Thread

Summary:

  • Number of threads: 2 (main thread and t1)

  • What happened?

    • t1 was started, but it doesn't do anything because it uses the run() method from the Thread class (which is empty).

    • t2 was created but not started, so it never executes.

  • Output: The main thread prints "Main Thread" four times.

Case Study: Thread Creation and Output Analysis (Case-2)

Given Code:

class MyRunnable implements Runnable {
    @Override
    public void run() {
        // Task-2: This task is executed by the thread associated with MyRunnable
        for (int i = 1; i < 5; i++) {
            System.out.println("Child Thread");
        }
    }
}

public class Multithreading18 {
    public static void main(String[] args) {
        MyRunnable r = new MyRunnable();
        Thread t1 = new Thread(); // Thread t1 created without Runnable
        Thread t2 = new Thread(r); // Thread t2 created with Runnable

        t2.start(); // Starts t2, which executes the run method of MyRunnable

        // Task-1: Executed by the main thread
        for (int i = 1; i < 5; i++) {
            System.out.println("Main Thread");
        }
    }
}

Analysis:

In this case, two threads are involved:

  • t1: Created using the Thread constructor without a Runnable object.

  • t2: Created using the Thread(Runnable) constructor, passing the MyRunnable object.

The t2.start() method is called, which starts t2 and invokes the run() method of MyRunnable.

Let's break down what happens when the program runs:

  1. Thread t1:

    • Thread t1 = new Thread(); creates a thread without a Runnable.

    • t1.start() is not called, so t1 does not perform any task.

  2. Thread t2:

    • Thread t2 = new Thread(r); creates a thread with MyRunnable.

    • When t2.start() is called, it invokes the run() method of MyRunnable, printing "Child Thread" four times.

  3. Main Thread:

    • The main thread executes the for loop and prints "Main Thread" four times.

How many threads?

  • 2 threads will be created in this case:

    1. Main thread: The default thread running the main method.

    2. Thread t2: A new thread created with MyRunnable that executes the run() method from MyRunnable.

Since t1.start() is not called, t1 does not execute.

Output:

The output will be as follows:

  1. The main thread starts and prints "Main Thread" four times.

  2. The user-defined thread (t2) starts after t2.start() is called, executing the run() method of MyRunnable, printing "Child Thread" four times.

Since t2.start() is called after the loop in the main thread, the exact order of output can vary due to thread scheduling. Typically, the output could be:

Main Thread
Main Thread
Main Thread
Main Thread
Child Thread
Child Thread
Child Thread
Child Thread

However, due to context switching and multithreading, the output can interleave in other ways. For example:

Main Thread
Child Thread
Main Thread
Child Thread
Main Thread
Child Thread
Main Thread
Child Thread

Summary:

  • Number of threads: 2 (main thread and t2).

  • What happens?:

    • Main thread executes the for loop printing "Main Thread".

    • Thread t2 executes the run() method from MyRunnable, printing "Child Thread".

  • Output: The main thread prints "Main Thread" four times, and the user-defined thread prints "Child Thread" four times. The exact order may vary depending on thread scheduling.

Case Study: Calling t1.run() Instead of t1.start()

Given Code:

class MyRunnable implements Runnable {
    @Override
    public void run() {
        // Task-2: This task is executed by the thread associated with MyRunnable
        for (int i = 1; i < 5; i++) {
            System.out.println("Child Thread");
        }
    }
}

public class Multithreading19 {
    public static void main(String[] args) {
        MyRunnable r = new MyRunnable();
        Thread t1 = new Thread();  // t1 is created without Runnable
        Thread t2 = new Thread(r); // t2 is created with MyRunnable

        t1.run();  // Calling run() directly, instead of start()

        // Task-1: Executed by the main thread
        for (int i = 1; i < 5; i++) {
            System.out.println("Main Thread");
        }
    }
}

Analysis:

In this case, we are calling the run() method directly instead of start() for t1. Let's break down the behavior and expected output:

  1. Thread t1:

    • Thread t1 = new Thread(); creates a Thread object.

    • t1.run(); calls the run() method directly instead of starting a new thread.

    • Since run() is called directly, it behaves like a normal method call and does not create a new thread.

    • It will simply execute the empty implementation of the run() method from the Thread class, which doesn't do anything.

  2. Thread t2:

    • Thread t2 = new Thread(r); creates a Thread object with the MyRunnable object passed as a Runnable.

    • However, t2.start() is never called, so the run() method of MyRunnable is never invoked. The task in MyRunnable is not executed in this case.

  3. Main Thread:

    • The main thread executes the for loop and prints "Main Thread" four times.

    • No new threads are created or executed.

How many threads?

  • In this case, only one thread (the main thread) will be active because:

    • t1.run() does not create a new thread (it simply calls run() as a normal method).

    • t2.start() is not called, so t2 will not execute the run() method of MyRunnable.

Thus, only the main thread is responsible for the output.

Output:

The output will be as follows, as only the main thread is executing:

Main Thread
Main Thread
Main Thread
Main Thread
  • Explanation:

    • The main thread executes the for loop and prints "Main Thread" four times.

    • The run() method of t1 does not get executed because it was called directly and it has no implementation in this case.

    • The run() method of MyRunnable is never invoked because t2.start() is not called.

Summary:

  • Number of threads: 1 (main thread).

  • What happens?:

    • t1.run() is called directly, but it does not create a new thread. It simply calls the run() method like any normal method.

    • t2.start() is not called, so t2 does not execute any task.

    • The main thread prints "Main Thread" four times.

Case Study: Calling t2.run() Directly

Given Code:

class MyRunnable implements Runnable {
    @Override
    public void run() {
        // Task-2: This task is executed by the thread associated with MyRunnable
        for (int i = 1; i < 5; i++) {
            System.out.println("Child Thread");
        }
    }
}

public class Multithreading20 {
    public static void main(String[] args) {
        MyRunnable r = new MyRunnable();
        Thread t1 = new Thread();   // t1 is created without Runnable
        Thread t2 = new Thread(r);  // t2 is created with MyRunnable

        t2.run();  // Calling run() directly, instead of start()

        // Task-1: Executed by the main thread
        for (int i = 1; i < 5; i++) {
            System.out.println("Main Thread");
        }
    }
}

Analysis:

In this case, t2.run() is called directly, instead of t2.start(). This impacts how the threads behave, as run() behaves like a normal method call, and start() would have created a new thread.

  1. Thread t1:

    • Thread t1 = new Thread(); creates a Thread object, but no action is performed on t1 in this case.

    • No thread is started for t1, so no output is produced by t1.

  2. Thread t2:

    • Thread t2 = new Thread(r); creates a Thread object with MyRunnable.

    • t2.run() is called directly (instead of t2.start()), meaning the run() method of MyRunnable is executed on the main thread as a normal method call, not on a separate thread.

    • Since run() is executed directly, it behaves like a regular method call and does not create a new thread.

  3. Main Thread:

    • The main thread will also execute its for loop and print "Main Thread" four times.

How many threads?

  • Only one thread is active here: the main thread.

    • t2.run() does not create a new thread, it just executes the code in MyRunnable on the main thread.

    • t1 is not used, so it does not impact the output.

Output:

  • The output will show "Child Thread" printed four times by the main thread, as run() is executed in the main thread.

  • After that, the main thread will continue to execute and print "Main Thread" four times.

Child Thread
Child Thread
Child Thread
Child Thread
Main Thread
Main Thread
Main Thread
Main Thread

Explanation:

  • The main thread executes both the task in MyRunnable (because run() is called directly) and the main thread's task (printing "Main Thread").

  • No new threads are created because t2.run() is just a normal method call, and not a thread start.

  • Thus, the main thread is responsible for both tasks.

Summary:

  • Number of threads: 1 (main thread).

  • What happens?:

    • t2.run() executes the run() method of MyRunnable directly on the main thread.

    • The main thread prints "Child Thread" and "Main Thread".

Case Study: Calling r.start()

Given Code:

class MyRunnable implements Runnable {
    @Override
    public void run() {
        // Task-2: This task is executed by the thread associated with MyRunnable
        for (int i = 1; i < 5; i++) {
            System.out.println("Child Thread");
        }
    }
}

public class Multithreading21 {
    public static void main(String[] args) {
        MyRunnable r = new MyRunnable();
        Thread t1 = new Thread();   // t1 is created without Runnable
        Thread t2 = new Thread(r);  // t2 is created with MyRunnable

        r.start();  // Error: MyRunnable does not have a start() method

        // Task-1: Executed by the main thread
        for (int i = 1; i < 5; i++) {
            System.out.println("Main Thread");
        }
    }
}

Analysis of the Code:

In this case, r.start() is called, where r is an object of type MyRunnable. However, there is no start() method in the MyRunnable class. This leads to a compile-time error, because the start() method is defined in the Thread class, not the Runnable interface or its implementing classes.

  1. r.start():

    • r is an instance of MyRunnable, which implements Runnable.

    • The Runnable interface only has the run() method, not the start() method.

    • The start() method is part of the Thread class and is used to begin a new thread, but start() can only be called on instances of the Thread class (or its subclasses).

    • Since r is not a Thread object, calling start() on r results in a compile-time error.

  2. Thread t2 = new Thread(r);:

    • t2 is a Thread object that is constructed with r (which is of type MyRunnable).

    • In this case, the Thread object t2 would be used to execute run() (through the Runnable interface). But, since r.start() is invoked on r (which is a MyRunnable object), the program will not reach this part due to the compilation error.

Error in the Code:

The code will fail to compile because of this line:

r.start();  // Error: MyRunnable does not have a start() method

The specific error message will be something like:

Exception in thread "main" java.lang.Error: Unresolved compilation problem: 
    The method start() is undefined for the type MyRunnable

Key Points:

  • The start() method belongs to the Thread class, not the Runnable interface or its implementing classes.

  • You can only call start() on instances of the Thread class or its subclasses.

  • The Runnable interface provides the run() method, but not the start() method.

  • To create and start a thread, you would use new Thread(r).start(), where r is a Runnable instance.

Correct Usage:

To avoid the error and correctly implement multithreading, you should call start() on the Thread object, not the Runnable object.

Here’s how you can fix the code:

public class Multithreading21 {
    public static void main(String[] args) {
        MyRunnable r = new MyRunnable();
        Thread t1 = new Thread();  // Thread t1 is not needed in this case
        Thread t2 = new Thread(r); // t2 is created with MyRunnable

        t2.start();  // Correct way to start the thread

        // Task-1: Executed by the main thread
        for (int i = 1; i < 5; i++) {
            System.out.println("Main Thread");
        }
    }
}

Output after the correction:

Main Thread
Main Thread
Main Thread
Main Thread
Child Thread
Child Thread
Child Thread
Child Thread

Explanation:

  1. Thread t2 will execute the run() method of MyRunnable, printing "Child Thread" four times.

  2. The main thread will execute its own loop, printing "Main Thread" four times.

Conclusion:

  • Number of threads: 2

    • Main thread: Executes the task in the main method.

    • User-defined thread (t2): Executes the task in the run() method of MyRunnable.

  • What happens?:

    • If you call r.start(), it will result in a compile-time error because the start() method is not defined in the MyRunnable class.

    • To fix this, you should call t2.start() where t2 is a Thread object.

Case Study: Calling r.run()

Given Code:

class MyRunnable implements Runnable {
    @Override
    public void run() {
        // Task-2: This task is executed by the thread associated with MyRunnable
        for (int i = 1; i < 5; i++) {
            System.out.println("Child Thread");
        }
    }
}

public class Multithreading22 {
    public static void main(String[] args) {
        MyRunnable r = new MyRunnable();
        Thread t1 = new Thread();  // t1 is created without Runnable
        Thread t2 = new Thread(r); // t2 is created with MyRunnable

        r.run();  // Calling run() directly

        // Task-1: Executed by the main thread
        for (int i = 1; i < 5; i++) {
            System.out.println("Main Thread");
        }
    }
}

Analysis of the Code:

In Case-6, the code calls r.run() instead of r.start(). This approach does not create a new thread, and it runs the run() method of MyRunnable directly in the main thread. Let's break it down step by step:

  1. r.run():

    • run() is a normal method in the MyRunnable class.

    • Calling r.run() directly does not create a new thread. Instead, it executes the run() method on the main thread just like any regular method.

    • The run() method in MyRunnable prints "Child Thread" four times.

  2. Main Thread Execution:

    • After r.run() is completed, the main thread continues executing its own loop, printing "Main Thread" four times.

Key Points:

  • No new thread is created in this case. The run() method of MyRunnable is executed just like a regular method, but it's executed on the main thread.

  • This is different from calling start() on a Thread object, which would result in the creation of a new thread to execute the run() method concurrently with the main thread.

  • The output will be sequential because both the run() method of MyRunnable and the main thread’s loop are executed on the same thread (the main thread).

Number of Threads:

  • There is only 1 thread in this case: the main thread.

  • Even though t2 is created, it is not used, and t1 is also unused. Only the main thread executes the tasks.

Output:

The output will be:

Child Thread
Child Thread
Child Thread
Child Thread
Main Thread
Main Thread
Main Thread
Main Thread

Explanation of Output:

  1. r.run() prints "Child Thread" four times because the run() method of MyRunnable is executed directly in the main thread.

  2. After that, the main thread continues to execute its own loop, printing "Main Thread" four times.

Conclusion:

  • Number of threads: 1 (the main thread)

  • Output:

      Child Thread
      Child Thread
      Child Thread
      Child Thread
      Main Thread
      Main Thread
      Main Thread
      Main Thread
    
  • Reason: Calling r.run() does not start a new thread. The run() method is executed on the main thread, followed by the main thread printing "Main Thread" four times.

Analysis of the Cases and Questions

Let's break down each case and answer the questions accordingly. We are specifically looking at how threads behave in each scenario.

Code Setup:

class MyRunnable implements Runnable {
    @Override
    public void run() {
        for (int i = 1; i < 5; i++) {
            System.out.println("Child Thread");
        }
    }
}

public class MultithreadingExample {
    public static void main(String[] args) {
        MyRunnable r = new MyRunnable();
        Thread t1 = new Thread();      // t1: A thread without MyRunnable
        Thread t2 = new Thread(r);     // t2: A thread with MyRunnable

        // Code with different cases
    }
}

1. In which of the above cases a new Thread will be created which is responsible for the execution of MyRunnable's run() method?

  • Case 1: t1.start()

    • No new thread for MyRunnable's run():

    • t1 is created without a Runnable, so it will not execute the MyRunnable's run() method. It will call the run() method of the Thread class, but this method is empty, so nothing will happen. No new thread for MyRunnable.

  • Case 2: t2.start()

    • A new thread for MyRunnable's run():

    • When t2.start() is called, a new thread is created and started. The start() method of the Thread class will internally invoke MyRunnable's run() method in a new thread. So, a new thread for MyRunnable.run() will be created.

  • Case 3: t2.run()

    • No new thread for MyRunnable's run():

    • t2.run() does not start a new thread. It simply calls the run() method directly in the main thread. So, no new thread is created, and MyRunnable.run() will execute in the main thread.

  • Case 4: t1.run()

    • No new thread for MyRunnable's run():

    • Like t2.run(), calling t1.run() also does not create a new thread. It simply invokes the run() method of Thread, which is empty. Hence, no thread is created for MyRunnable.run().

  • Case 5: r.start()

    • Compilation Error:

    • This case results in a compile-time error because r is an instance of MyRunnable and does not have a start() method. The start() method belongs to the Thread class. This would result in a compilation error and hence will not create a new thread.

  • Case 6: r.run()

    • No new thread for MyRunnable's run():

    • r.run() calls the run() method directly within the main thread. It does not create a new thread, it simply executes the run() method as a normal method call in the main thread.

Answer to Question 1:

A new thread will be created and responsible for the execution of MyRunnable's run() method only in Case 2: t2.start().


2. In which of the above cases a new Thread will be created?

  • Case 1: t1.start()

    • No new thread:

    • t1 is created without a Runnable and calls the run() method of the Thread class, which is empty. So no new thread is created.

  • Case 2: t2.start()

    • New thread is created:

    • t2.start() creates a new thread and invokes MyRunnable.run() in that thread.

  • Case 3: t2.run()

    • No new thread:

    • t2.run() simply calls the run() method on the main thread, not creating a new thread.

  • Case 4: t1.run()

    • No new thread:

    • t1.run() simply calls run() in the main thread (without creating any new thread).

  • Case 5: r.start()

    • Compilation Error:

    • The start() method is not defined in the MyRunnable class, so this case results in a compile-time error, and no thread is created.

  • Case 6: r.run()

    • No new thread:

    • Calling r.run() simply invokes the run() method on the main thread, without creating any new threads.

Answer to Question 2:

A new thread will be created only in Case 2: t2.start().


3. In which of the above cases MyRunnable's run() method will be executed?

  • Case 1: t1.start()

    • MyRunnable's run() will not be executed:

    • t1 does not have a Runnable, so it does not invoke MyRunnable.run(). The run() method of Thread (which is empty) is invoked instead.

  • Case 2: t2.start()

    • MyRunnable's run() will be executed:

    • t2.start() creates a new thread and the run() method of MyRunnable is invoked in the new thread.

  • Case 3: t2.run()

    • MyRunnable's run() will be executed:

    • Calling t2.run() invokes MyRunnable.run() directly, but it executes in the main thread instead of a new thread.

  • Case 4: t1.run()

    • MyRunnable's run() will not be executed:

    • t1.run() invokes run() of Thread, which is empty. Therefore, MyRunnable's run() is not executed.

  • Case 5: r.start()

    • Compile-time error:

    • r is an instance of MyRunnable, and it does not have a start() method, so this case results in a compile-time error. MyRunnable's run() will not be executed.

  • Case 6: r.run()

    • MyRunnable's run() will be executed:

    • Calling r.run() directly calls MyRunnable.run() in the main thread, not in a new thread.

Answer to Question 3:

MyRunnable.run() will be executed in the following cases:

  • Case 2: t2.start() (in a new thread)

  • Case 3: t2.run() (in the main thread)

  • Case 6: r.run() (in the main thread)


Summary of Answers:

  1. Which case creates a new thread responsible for executing MyRunnable.run()?

    • Case 2: t2.start()
  2. Which case creates a new thread?

    • Case 2: t2.start()
  3. Which cases execute MyRunnable.run()?

Different Approaches for Creating a Thread in Java

Java provides two main ways to create and run threads: extending the Thread class and implementing the Runnable interface. Let's explore both approaches, their advantages and disadvantages, and why implementing the Runnable interface is generally considered the best approach.


This approach involves creating a new class that extends the Thread class and overriding the run() method to define the task that the thread will execute.

Code Example:

// Extending the Thread class
class MyThread extends Thread {
    @Override
    public void run() {
        // Task to be executed by the thread
        for (int i = 1; i <= 5; i++) {
            System.out.println("Child Thread");
        }
    }
}

public class Main {
    public static void main(String[] args) {
        MyThread t = new MyThread();  // Creating an instance of the custom thread
        t.start();  // Starting the thread
    }
}

Issues with Extending the Thread Class:

  1. No Inheritance from Other Classes:

    • Only one class can be inherited in Java, and if we extend the Thread class, our custom class cannot extend any other class because Java does not support multiple inheritance of classes.

    • Inheritance loss: If our class extends Thread, it loses the ability to inherit from other classes, which can be restrictive in some situations.

  2. Limited Reusability:

    • Extending Thread directly limits the reusability of the class. It becomes tightly coupled with the Thread class, and you can't extend other classes if needed.

    • Not flexible: If the class already extends another class, it is not possible to extend Thread because Java allows only single inheritance.

Why this Approach is Not Ideal:

  • The thread-related functionality is tightly coupled to the class, meaning that the class cannot be reused for other purposes (like being part of a larger class hierarchy).

  • Performance and memory considerations: This approach may lead to suboptimal memory usage and performance because each thread object has its own state and methods, even if they are not all needed.


In this approach, we create a class that implements the Runnable interface and overrides the run() method to define the task that the thread will execute. A Thread object is then created, passing the Runnable instance to it.

Code Example:

// Implementing the Runnable interface
class MyRunnable implements Runnable {
    @Override
    public void run() {
        // Task to be executed by the thread
        for (int i = 1; i <= 5; i++) {
            System.out.println("Child Thread");
        }
    }
}

public class Main {
    public static void main(String[] args) {
        MyRunnable r = new MyRunnable();   // Creating a Runnable object
        Thread t = new Thread(r);           // Creating a thread object and passing Runnable
        t.start();                          // Starting the thread
    }
}

Advantages of Implementing Runnable:

  1. Allows Inheritance from Other Classes:

    • The Runnable interface allows you to implement multiple interfaces and extend other classes, which is not possible with the Thread class (since Java does not support multiple inheritance for classes).

    • This makes your class more flexible and reusable.

  2. Better Performance and Memory Usage:

    • By separating the task (Runnable) from the Thread object, this approach leads to better performance and memory management. The Runnable interface is lightweight compared to extending the Thread class.

    • You can reuse the same Runnable object across multiple Thread objects, which reduces memory overhead.

  3. Separation of Concerns:

    • In this approach, task and thread management are separate. The Runnable defines the task, and the Thread class is responsible for managing the thread execution. This separation improves code modularity and maintainability.

    • You can have a Runnable task that can be used with different thread objects.

  4. Better in Real-World Applications:

    • In real-world applications, you often need to manage more complex tasks, and using Runnable makes it easier to handle such tasks. For example, when working with thread pools, Runnable is preferred because it decouples the task definition from the thread management logic.
  • Reusability: You can reuse the same Runnable class with different thread objects, and your class can still extend other classes.

  • Performance: The approach results in better memory management and thread management.

  • Separation of concerns: The Runnable interface handles the task, and the Thread class manages the thread execution.


Conclusion:

  • Best Approach: Implementing the Runnable interface is the recommended approach for creating threads in Java because it allows your class to implement multiple interfaces and extend other classes, making it more flexible and reusable.

  • Why not extend Thread?: Extending Thread leads to tight coupling with the Thread class, preventing inheritance from other classes and limiting the reusability of the class.

Real-World Usage: In most real-world multi-threaded applications, implementing Runnable is preferred, especially when tasks are independent and should be executed concurrently.

Constructors Available in the Thread Class

The Thread class in Java has several constructors, which allow you to create a thread in different ways depending on how you want to initialize it. Below are the various constructors available, as you would find by running javap java.lang.Thread in the command prompt:

1. Thread t = new Thread()

This constructor creates a new thread without associating it with any task or a specific name. This is the simplest constructor that just creates a thread.

  • Usage: It creates a thread with a default name such as "Thread-0", "Thread-1", etc.

  • Example:

      Thread t = new Thread();
      t.start();
    

2. Thread t = new Thread(Runnable r)

This constructor creates a new thread that will execute the task defined in the Runnable interface. The Runnable task is passed to the thread, and when the thread is started, it will execute the run() method of the Runnable.

  • Usage: This is the constructor you'd use when you want to create a thread and pass a Runnable task to it.

  • Example:

      Runnable task = new Runnable() {
          public void run() {
              System.out.println("Task is running in a new thread!");
          }
      };
    
      Thread t = new Thread(task);
      t.start();
    

3. Thread t = new Thread(String name)

This constructor creates a new thread and sets its name. This is useful when you want to specify a name for the thread for easier identification during debugging or logging.

  • Usage: You use this constructor when you want to create a thread with a specific name, rather than the default one like "Thread-0".

  • Example:

      Thread t = new Thread("MyCustomThread");
      t.start();
    

4. Thread t = new Thread(Runnable r, String name)

This constructor creates a new thread with a specified Runnable task and a custom name. This is one of the most commonly used constructors, as it combines both functionality: the task to be executed and a name for the thread.

  • Usage: This is often used in real-world applications when you need both a specific task and a custom thread name.

  • Example:

      Runnable task = new Runnable() {
          public void run() {
              System.out.println("Task is running in a new thread with a custom name!");
          }
      };
    
      Thread t = new Thread(task, "CustomThreadName");
      t.start();
    

5. Thread t = new Thread(ThreadGroup g, String name)

This constructor creates a new thread and associates it with a specific ThreadGroup. A ThreadGroup is used to group multiple threads for management purposes.

  • Usage: This is used when you want to create a thread that belongs to a particular ThreadGroup.

  • Example:

      ThreadGroup group = new ThreadGroup("MyThreadGroup");
      Thread t = new Thread(group, "GroupThread");
      t.start();
    

6. Thread t = new Thread(ThreadGroup g, Runnable r)

This constructor creates a new thread that will execute the task defined in the Runnable interface and belongs to a specified ThreadGroup.

  • Usage: Used when you need both a Runnable task and a specific ThreadGroup.

  • Example:

      ThreadGroup group = new ThreadGroup("Group1");
      Runnable task = new Runnable() {
          public void run() {
              System.out.println("Task in specific thread group");
          }
      };
    
      Thread t = new Thread(group, task);
      t.start();
    

7. Thread t = new Thread(ThreadGroup g, Runnable r, String name)

This constructor creates a new thread that executes a Runnable task, belongs to a ThreadGroup, and has a custom thread name.

  • Usage: This is useful when you need all three: a Runnable task, a specific ThreadGroup, and a custom name for the thread.

  • Example:

      ThreadGroup group = new ThreadGroup("Group2");
      Runnable task = new Runnable() {
          public void run() {
              System.out.println("Task with custom name in specific group");
          }
      };
    
      Thread t = new Thread(group, task, "CustomThreadInGroup");
      t.start();
    

8. Thread t = new Thread(ThreadGroup g, Runnable r, String name, long stackSize)

This constructor creates a new thread with a specified Runnable, ThreadGroup, custom name, and a stack size. The stackSize specifies the size of the stack for the new thread, which is useful for applications that require fine control over memory usage.

  • Usage: Used when you need all three components and also need to specify the stack size for the thread.

  • Example:

      ThreadGroup group = new ThreadGroup("Group3");
      Runnable task = new Runnable() {
          public void run() {
              System.out.println("Task with custom name, group, and stack size");
          }
      };
    
      long stackSize = 1024 * 1024; // 1MB stack size
      Thread t = new Thread(group, task, "CustomThreadWithStackSize", stackSize);
      t.start();
    

Summary of Common Constructors:

In real-time coding, the top 4 constructors are most commonly used. They provide the flexibility to specify both the task and the thread's name, while still allowing you to use the Runnable interface, which is a better practice for multi-threaded applications. Here's a quick rundown:

  1. Thread() — Default constructor, rarely used in practice.

  2. Thread(Runnable r) — Used to pass a Runnable task to the thread.

  3. Thread(String name) — Used to give the thread a custom name.

  4. Thread(Runnable r, String name) — Used to pass a Runnable task and a custom name to the thread, which is the most common constructor in real-world applications.

The other constructors that involve ThreadGroup and stackSize are typically used in more specialized situations where you need better control over thread grouping and memory management.

Best Practices:

  • Use Runnable with Thread: Prefer Runnable for defining the task that a thread will execute, because it allows for better design and reusability.

  • Custom thread names: Set a custom thread name for easier debugging, especially when multiple threads are involved.

  • ThreadGroups: Use ThreadGroup if you need to manage multiple threads together. However, thread groups are less commonly used in modern Java applications.

How Many Threads are Created and What is the Output?

In this case, two threads are created:

  1. Main Thread: The thread in which the main() method runs.

  2. User-Defined Thread: A custom thread defined by extending the Thread class, which will execute the overridden run() method in MyThread.

// Defining the MyThread class that extends the Thread class
class MyThread extends Thread {
    @Override
    public void run() {
        // Task in the run method of MyThread
        for (int i = 1; i < 5; i++) {
            System.out.println("run method is called");
        }
    }
}

public class Multithreading26 {
    public static void main(String[] args) {
        // Creating an instance of MyThread
        MyThread t = new MyThread();

        // Creating an instance of Thread
        Thread t1 = new Thread();

        // Starting the t1 thread
        t1.start();  // Starts a new thread (Main Thread will execute run() method, but it has no output)

        // Task in main thread
        for (int i = 1; i < 5; i++) {
            System.out.println("Main Thread");
        }
    }
}

Explanation of Code:

  • Step 1: The MyThread class extends the Thread class and overrides the run() method. When the start() method is called on an instance of MyThread, the thread begins execution, which internally invokes the run() method.

  • Step 2: In the main() method:

    • A new instance of MyThread is created, but it is not started immediately.

    • The Thread class's start() method is called on t1, which starts the Main Thread execution and also triggers the run() method of the Thread class (although it's empty, so it doesn't print anything).

  • Step 3: The Main Thread continues executing the main() method, and it prints "Main Thread" four times.

Threads Created:

  • Main Thread: This thread is already running when the main() method starts. It prints "Main Thread" four times.

  • User-Defined Thread (MyThread): This is the thread that starts when t1.start() is called. This thread will run the run() method of MyThread and print "run method is called" four times.

Important Note: The output will not strictly follow the order of execution of threads, as Java thread scheduling is handled by the operating system's thread scheduler. Hence, the output may interleave between the "Main Thread" and "run method is called" messages. The order may vary each time you run the program.

Expected Output (One Possible Order):

Main Thread
Main Thread
Main Thread
Main Thread
run method is called
run method is called
run method is called
run method is called

Detailed Breakdown of Execution Flow:

  1. Main Thread starts executing from the main() method.

  2. A new instance of MyThread is created but not started yet.

  3. t1.start() is called, which:

    • Invokes the start() method of the Thread class.

    • Registers t1 with the Thread Scheduler and starts a new thread (the Main Thread).

  4. Both the Main Thread and the User-Defined Thread (MyThread) are running concurrently.

    • The Main Thread prints "Main Thread" 4 times.

    • The User-Defined Thread (MyThread) prints "run method is called" 4 times.

Since the threads are running concurrently, their output may be interleaved in different ways each time you run the program.

Here's the combined code for defining a thread using an alternative approach, where a class implements the Runnable interface, and the Runnable object is passed to the Thread constructor:

Code Explanation:

  1. Runnable Interface:

    • The Runnable interface has a single abstract method run() that represents the task to be executed by a thread.
  2. Thread Class:

    • The Thread class can be passed an object that implements the Runnable interface, and when we call start() on the Thread object, it will invoke the run() method of the Runnable object.
  3. MyThread Class:

    • MyThread extends Thread, and the run() method is overridden to define the task that this thread will perform (in this case, it just prints "run method is called").
  4. In the Multithreading27 Class:

    • We create a MyThread object t, then pass this object as a parameter to the Thread constructor. After that, we start the thread using t1.start(), which will invoke the run() method in the MyThread class.

    • Meanwhile, the Main Thread executes the task in the main method, which prints "Main Thread".

Code:

// Runnable interface
public interface Runnable {
    public abstract void run();
}

// Thread class (pre-defined class in Java)
public class Thread implements Runnable {
    public void start() {
        // Register Thread with ThreadScheduler
        // Perform other low-level activities
        // Invoke run()
    }

    public void run() {
        // Empty implementation
    }
}

// MyThread class that extends Thread and implements Runnable
class MyThread extends Thread {
    @Override
    public void run() {
        // Task in the run method of MyThread
        System.out.println("run method is called");
    }
}

public class Multithreading27 {
    public static void main(String[] args) {
        // Creating an instance of MyThread
        MyThread t = new MyThread();

        // Creating a new Thread object and passing MyThread instance as the Runnable task
        Thread t1 = new Thread(t);

        // Starting the thread (this invokes the run() method of MyThread)
        t1.start();

        // Task in the main thread
        System.out.println("Main Thread");
    }
}

Explanation:

  • MyThread Class:

    • MyThread extends the Thread class, which already implements the Runnable interface. This allows MyThread to run as a separate thread by overriding the run() method.
  • Thread t1 = new Thread(t):

    • Here, the MyThread object t is passed to the Thread constructor as the Runnable task. The Thread object t1 is responsible for starting the MyThread task in a separate thread by calling t1.start(), which will invoke the run() method of MyThread.
  • t1.start():

    • This starts the Thread, and the run() method of MyThread is invoked in a separate thread.
  • Output:

    • The main thread will print "Main Thread", and the MyThread thread will print "run method is called".

Expected Output:

Main Thread
run method is called

In this case, since threads execute concurrently, the order may vary between runs, but typically, the Main Thread will print first, followed by the output of the MyThread thread.

Key Takeaway:

  • The approach of passing a Runnable object to the Thread constructor allows us to separate the task from the thread itself, giving more flexibility in terms of multiple tasks and thread reuse. This is often considered a better practice over directly extending the Thread class.

Explanation of Thread Naming in Java:

In Java, each thread has a name, and you can get or set the name of a thread. By default, threads are named automatically (e.g., Thread-0, Thread-1, etc.), but you can change the name of a thread if needed. Let's look at examples of how to get the name of the main thread and child threads using Thread.currentThread().getName() and the getName() method.

Example 1: Getting the Name of the Main Thread and Child Thread

// Thread class is a part of Java, no need to write it from scratch.

class MyThread extends Thread {
    // This class extends Thread, so the run() method is overridden here
    @Override
    public void run() {
        // This will get the name of the thread that runs this method
        String name = Thread.currentThread().getName();
        System.out.println("run() is executed by - " + name);
    }
}

public class Multithreading28 {
    public static void main(String[] args) {
        // Getting the name of the main thread (the thread that runs the main method)
        String name = Thread.currentThread().getName();
        System.out.println("Main method is executed by - " + name);

        // Creating a thread object
        MyThread t = new MyThread();

        // Printing the default name of the thread object
        System.out.println(t);  // Outputs something like: Thread[Thread-0,5,main]

        // Starting the thread
        t.start();

        // Getting the name of the newly created child thread (Thread-0 by default)
        System.out.println("Name of child thread is: " + t.getName());  // Outputs: Thread-0
    }
}

Explanation:

  • Thread.currentThread().getName():

    • This method gets the name of the thread that is currently executing the code. In the case of main, it refers to the main thread that is running the main method.
  • t.getName():

    • This method returns the name of the thread t. Since MyThread extends Thread, it is a child thread. When start() is called on t, it creates a new thread with a default name (e.g., Thread-0).
  • Output:

    • Main method is executed by-main tells you that the main method is executed by the main thread.

    • Thread[Thread-0,5,main] gives details about the Thread-0 (name, priority, and thread group).

    • Name of child thread is: Thread-0 confirms the name of the child thread (Thread-0), which is created when t.start() is called.

Expected Output:

Main method is executed by - main
Thread[Thread-0,5,main]  // Default name, priority, and group
Name of child thread is: Thread-0

Example 2: Using run() Method in a Custom Thread

class MyThread extends Thread {
    @Override
    public void run() {
        // Get the name of the thread executing the run method
        String name = Thread.currentThread().getName();
        System.out.println("run() is executed by - " + name);
    }
}

public class Multithreading28 {
    public static void main(String[] args) {
        // Getting the name of the main thread executing the main method
        String name = Thread.currentThread().getName();
        System.out.println("Main method is executed by - " + name);

        // Creating an instance of MyThread
        MyThread t = new MyThread();

        // Starting the thread
        t.start();
    }
}

Explanation:

  • In this example, MyThread extends the Thread class, and we override the run() method. Inside the run() method, Thread.currentThread().getName() is used to get the name of the thread that is executing the run() method.

  • We print the name of the main thread in the main() method and the name of the thread executing run() inside MyThread.

  • Output:

    • Main method is executed by-main indicates that the main method is executed by the main thread.

    • run() is executed by - Thread-0 shows the name of the child thread that is executing the run() method (in this case, it's Thread-0).

Expected Output:

Main method is executed by - main
run() is executed by - Thread-0

Key Takeaways:

  • The Thread.currentThread().getName() method gives the name of the thread that is executing the current block of code.

  • The Thread.getName() method gives the name of a specific thread object, such as the main thread or a child thread.

  • By default, when you create a new thread (using new Thread() or extending Thread), Java assigns the thread a default name like Thread-0, Thread-1, etc., which can be accessed and modified using getName() and setName() methods.

Setting and Changing Thread Names in Java

In Java, you can set and change the names of threads using the setName() method. By default, each thread has a name like Thread-0, Thread-1, etc., but you can customize it to make your program more readable.

Let's go through examples where the name of the main thread and child threads is set and changed dynamically.

Example 3: Changing the Name of the Main Thread

class MyThread extends Thread {
    @Override
    public void run() {
        // Running code for child thread (not changing name here)
    }
}

public class Multithreading29 {
    public static void main(String[] args) {
        // Get the name of the main thread
        String name = Thread.currentThread().getName();
        System.out.println("Main method is executed by - " + name);

        // Creating a new thread
        MyThread t = new MyThread();
        t.start(); // Starting the child thread

        // Changing the name of the main thread
        Thread.currentThread().setName("My thread");
        System.out.println("Name of main thread changed to: " + Thread.currentThread().getName());
    }
}

Explanation:

  • Thread.currentThread().getName(): This method returns the name of the current thread, which, in this case, is the main thread when running the main() method.

  • Thread.currentThread().setName("My thread"): Changes the name of the current thread (main thread in this case) to "My thread".

  • Output:

    • The name of the main thread is initially "main", but it is changed to "My thread" after calling setName().

Expected Output:

Main method is executed by - main
Name of main thread changed to: My thread

Example 4: Exception in Main Thread

class MyThread extends Thread {
    @Override
    public void run() {
        // Running code for child thread
    }
}

public class Multithreading29 {
    public static void main(String[] args) {
        // Getting the name of the main thread
        String name = Thread.currentThread().getName();
        System.out.println("Main method is executed by - " + name);

        // Creating the thread
        MyThread t = new MyThread();

        // Simulating an exception in the main thread
        System.out.println(10 / 0);  // This will throw ArithmeticException
    }
}

Explanation:

  • The code simulates an exception (ArithmeticException) by attempting to divide by zero in the main thread.

  • This demonstrates that the exception is thrown in the main thread, which will be displayed along with the thread's name (main).

  • Output:

    • The exception occurs in the main thread, which is visible in the exception message.

Expected Output:

Main method is executed by - main
Exception in thread "main" java.lang.ArithmeticException: / by zero
    at Multithreading29.main(Multithreading29.java:14)

Example 5: Changing the Name of the Main Thread After Exception

class MyThread extends Thread {
    @Override
    public void run() {
        // Running code for child thread
    }
}

public class Multithreading29 {
    public static void main(String[] args) {
        // Getting the name of the main thread
        String name = Thread.currentThread().getName();
        System.out.println("Main method is executed by - " + name);

        // Changing the name of the main thread before exception
        Thread.currentThread().setName("ashish");

        // Simulating an exception in the main thread
        System.out.println(10 / 0);  // This will throw ArithmeticException
    }
}

Explanation:

  • Before the exception occurs, the name of the main thread is changed from "main" to "ashish" using Thread.currentThread().setName("ashish").

  • When the exception occurs (ArithmeticException), the name of the thread that caused the exception is displayed as "ashish", not "main".

Expected Output:

Main method is executed by - main
Exception in thread "ashish" java.lang.ArithmeticException: / by zero
    at Multithreading29.main(Multithreading29.java:15)

Example 6: Changing the Name of Two Threads (Main and Child Thread)

class MyThread extends Thread {
    @Override
    public void run() {
        // Changing the name of the child thread inside the run method
        Thread.currentThread().setName("second thread");
        System.out.println("Child thread name changed to " + Thread.currentThread().getName());
    }
}

public class Multithreading29 {
    public static void main(String[] args) {
        // Creating and starting the child thread
        MyThread t = new MyThread();
        t.start();

        // Changing the name of the main thread
        Thread.currentThread().setName("First thread");
        System.out.println("Main method changed to " + Thread.currentThread().getName());
    }
}

Explanation:

  • Child Thread: Inside the run() method of MyThread, the name of the child thread is changed using Thread.currentThread().setName("second thread").

  • Main Thread: The name of the main thread is changed to "First thread" using Thread.currentThread().setName("First thread").

  • The output shows that both the main thread and the child thread have had their names changed.

Expected Output:

Main method changed to First thread
Child thread name changed to second thread

Key Takeaways:

  • setName(): You can change the name of the current thread using Thread.currentThread().setName(). This is useful for debugging or identifying threads in a multithreaded environment.

  • Thread Names: By default, Java names threads automatically (e.g., Thread-0, Thread-1, etc.), but you can change these names for better clarity.

  • Thread Names in Output: When exceptions occur, the thread name is shown as part of the exception message, which can be helpful for tracking where the exception occurred in a multi-threaded environment.

Conclusion: Multithreading in Java: Thread Creation, Naming, and Handling

In this discussion, we explored various concepts and techniques related to multithreading in Java, focusing on thread creation, managing thread names, and best practices for using threads effectively. Let's summarize the key points from the top:


1. Thread Creation: Extending Thread vs Implementing Runnable

  • Extending Thread:

    • The traditional way of creating threads involves subclassing the Thread class and overriding the run() method. However, this approach has limitations such as:

      • No inheritance: Once you extend Thread, your class can no longer extend any other class, limiting flexibility.

      • Not a recommended approach in real-world applications, as it limits the ability to use inheritance effectively.

  • Implementing Runnable:

    • A better approach is to implement the Runnable interface. This allows for:

      • Inheritance flexibility: Your class can still extend other classes while implementing Runnable.

      • Code reuse: The Runnable interface defines the run() method, which can be passed to a Thread instance for execution.

      • Scalability: In more complex systems, it's more common to use this approach because it supports better resource management and cleaner code.

2. Thread Constructors

  • The Thread class in Java has multiple constructors that allow for flexibility in creating threads:

    • No-argument constructor (Thread t = new Thread()).

    • Runnable constructor (Thread t = new Thread(Runnable r)).

    • Named thread constructors (Thread t = new Thread(String name)).

    • Thread with group and stack size for advanced use cases.

In real-time applications, the most common constructors are those that take a Runnable object or allow naming the thread for easier debugging.

3. Thread Execution Process

  • When creating a new thread, the start() method must be called to invoke the run() method.

  • Important to note: The run() method is where the code for the thread’s task is defined. By default, if the run() method is not overridden, it has an empty implementation.

  • The start() method registers the thread with the Thread Scheduler, which decides when the thread should be executed.

4. Thread Naming and Accessing Thread Information

  • Threads in Java are given default names such as "Thread-0", "Thread-1", etc., but you can change these names using the setName() method for better clarity, especially during debugging.

  • Accessing the name of the current thread can be done using Thread.currentThread().getName(), which is useful for identifying which thread is executing a certain block of code.

    • Example: String name = Thread.currentThread().getName(); provides the name of the current thread, which can be printed for debugging.

5. Changing Thread Names Dynamically

  • The thread name can be set dynamically using Thread.currentThread().setName(), allowing you to track and identify threads more clearly in complex multithreaded programs.

    • Example 1: Changing the main thread’s name.

    • Example 2: Changing the name of a child thread inside the run() method.

    • This becomes especially useful when debugging multithreaded applications where you need to keep track of thread execution.

6. Handling Exceptions in Threads

  • As we saw in Example-4 and Example-6, exceptions thrown in the main thread or in child threads are visible with the thread's name. This helps in tracing where the exception occurred.

  • Handling exceptions within threads should be done carefully to prevent one thread's exception from affecting other threads in the application.

7. Conclusion and Best Practices

  • Recommended Thread Creation Approach: Implementing the Runnable interface is generally the preferred approach, as it allows for cleaner, more maintainable, and flexible code.

  • Thread Naming: Always name your threads appropriately, especially in multithreaded environments, to make debugging easier. You can set the thread name dynamically using setName().

  • Handling Threads and Exceptions: Ensure that exceptions are caught and managed properly in each thread to avoid unexpected crashes and to provide useful debug information.

  • Performance Consideration: Threads should be used carefully, as improper use can lead to performance bottlenecks, race conditions, or excessive context switching. Always analyze your thread usage in relation to the system's resource constraints.


Overall Takeaway

Java's multithreading capabilities offer great power and flexibility, but they must be used wisely. Creating threads using Runnable interfaces, managing thread names, and carefully handling exceptions are key techniques to ensure that your multithreaded programs are efficient, maintainable, and easy to debug. By understanding these concepts, you can implement complex multithreaded solutions that perform well under real-world conditions.