Mastering Multithreading in Java: A Comprehensive Guide Part 1

Table of Contents

In the realm of software development, multithreading emerges as a powerful tool for building applications that can execute multiple tasks seemingly concurrently, enhancing responsiveness and performance. Java, a widely used language, offers robust support for multithreading through its Thread class and Runnable interface. Multithreading in Java is a powerful feature that allows concurrent execution of multiple threads within the same process. It’s crucial for building scalable, responsive, and efficient applications, especially in today’s world where multi-core processors are prevalent. In this comprehensive guide, we will delve into the depths of Java multithreading, covering everything from the basics to advanced concepts.

What is Multithreading in Java?

Before understanding multithreading, we first need to understand multitasking. Let’s see what it is.

Multitasking: Executing several tasks simultaneously is the concept of multitasking. There are two types of multitasking:

  1. Process-based: Executing several tasks simultaneously, where each task is a separate independent program (Process), is called process-based multitasking. For example, while typing a Java program in an editor, we can listen to audio songs from the same system, simultaneously download a file from the internet. All these tasks will be executed simultaneously and independently of each other; hence, it is process-based multitasking. Process-based multitasking is best suited at the OS level.
  2. Thread-based: Executing several tasks simultaneously, where each task is a separate independent part of the same program, is called thread-based multitasking. Each independent part is called a thread. Thread-based multitasking is best suited at the program level. For example, developing multimedia graphics, animation, video games, web server, and application server, etc.

Whether it is process-based or thread-based, the main objective of multitasking is to reduce the response time of the system and improve performance.

Now, What is Multithreading?

Multithreading refers to the concurrent execution of multiple threads within a single process. Each thread represents an independent flow of control, allowing programs to perform multiple tasks simultaneously.

BTW, What is Concurrency: The ability of tasks to appear to execute simultaneously, even on single-core processors.

Why Multithreading?

Multithreading enables applications to utilize the available CPU resources efficiently, especially on multi-core processors. It improves responsiveness by allowing tasks to run in the background while the main application thread remains responsive.

Threads vs. Processes

A thread is a lightweight process, and multiple threads can exist within a single process. Threads share the same memory space, making communication between them more efficient compared to processes, which have separate memory spaces.

Defining a Thread

We can define a thread by:

  1. By extending the Thread class
  2. By implementing the Runnable Interface

Let’s take a closer look at each one.

By Extending the Thread class

To define a thread by extending the Thread class, a new class can be created that extends the Thread class. Here’s an example:

Java
class MyThread extends Thread   // This way we can define a thread by extending the Thread class 
{
   public void run()
   {
     for(int i=0; i<10; i++)   // Inside the run method, whatever is there is the job of the thread. Here, this for loop is the job of this child thread and it is executed independently.
     {
       System.out.println("child thread");
     }
   }
}            

In this method, the class MyThread extends the Thread class, enabling it to define a thread. The run() method within this class signifies the task to be executed by the thread. In this example, the loop within the run() method represents the specific job to be performed by the child thread.

Thread Execution and Thread Scheduler

Let’s see below example first.

Java
class ThreadDemo {
  public static void main(String[] args) {
    // At this point, only one thread is executing, and that is the main thread.
  
    MyThread t = new MyThread();  // Thread instantiation
    t.start();   // Starting the thread
   
    // At this point, two threads are executing simultaneously: one is the main thread, and the other is the child thread (t - MyThread).
    for(int i=0; i<10; i++)   // This for loop is executed by the main thread and not by its child thread.
    {
      System.out.println("main thread");
    }
  }
}

The behavior of thread execution is influenced by the thread scheduler, a component of the JVM responsible for managing and scheduling threads.

Thread Scheduler

It is a part of JVM and is responsible for scheduling threads. If multiple threads are waiting to get a chance for execution, the order in which threads will be executed is decided by the thread scheduler. We can’t expect the exact algorithm followed by the thread scheduler as it varies from JVM to JVM. Hence, we can’t expect the thread execution order and exact output. Whenever the situation involves multithreading, there is no guarantee of exact output, but we can provide several possible outputs. The following are various possible outputs for the above program:

Possible Outputs:

Java
Output 1:

Main thread
Main thread
...
Child thread
Child thread
...
Java
Output 2:

Child Thread
Child Thread
...
Main Thread
Main Thread
...
Java
Output 3:

Main Thread
Child Thread
Main Thread
Child Thread
...
Java
Output 4:

Child Thread
Main Thread
Child Thread
...

Due to the unpredictable nature of thread scheduling, any of these scenarios (or variations thereof) could occur during program execution. It’s important to note that while we cannot guarantee a specific output, understanding the behavior of thread scheduling enables us to anticipate various possible outcomes in multithreading scenarios.

Difference between t.start() and t.run()

In the case of t.start(), a new thread will be created which is responsible for the execution of the run method. However, in the case of t.run(), a new thread won’t be created, and the run method will be executed just like a normal method call by the main thread. Hence, in the above program, if we replace t.start() with t.run(), then the output is:

Java
Child Thread 
Child Thread 
.
.
.
Main Thread 
Main Thread 
Main Thread

This total output is produced only by the main thread.

Importance of Thread class start method

The start method of the Thread class is responsible for registering the thread with the thread scheduler and performing all other mandatory activities. Without executing the start method of the Thread class, there is no chance of starting a new thread in Java. Due to this, the start method of the Thread class is considered the heart of multithreading.

Java
start()
{
  1. Register this thread with the Thread Scheduler
  2. Perform all other mandatory activities
  3. Invoke run()
}

Each step in the start() method plays a critical role in setting up and launching a new thread, making it indispensable for effective multithreading in Java.

Overloading of the run method

Overloading of the run method is always possible, but the start method of the Thread class will always invoke the no-argument run method. The other overloaded methods must be called explicitly like a normal method call.

For instance:

Java
class MyThread extends Thread {
    public void run() {
        System.out.println("no arg run");
    }
 
    public void run(int i) {
        System.out.println("int arg run");
    }
}

class ThreadDemo {
    public static void main(String[] args) {
        MyThread t = new MyThread();
        t.start(); // This will invoke the no argument run method implicitly.
    }
}



Output: no arg run

Here, although the MyThread class has an overloaded run method that accepts an integer argument, it won’t be invoked implicitly when the thread starts. To execute the overloaded run method with an integer argument, it must be called explicitly like any other method.

Without overriding the run method

If we are not overriding the run method, then the Thread class’s run method will be executed, which has an empty implementation. Hence, we won’t get any output.

Means, If the run method is not overridden in a subclass of Thread, then the default run method provided by the Thread class will be executed. This default run method has an empty implementation, resulting in no output.

Java
class MyThread extends Thread {
    // No run method overridden
}

class Test {
    public static void main(String[] args) {
        MyThread t = new MyThread();
        t.start();
    }
}


//No output

Note: It is highly recommended to override the run method; otherwise, don’t proceed with the multithreading concept.

Overriding start method

If we override the start method, then our overridden start method will be executed just like a normal method call, and a new thread won’t be created.

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

class Test {
  public static void main(String[] args) {
    MyThread t = new MyThread();
    t.start();
    System.out.println("main method");
  }
}


////////////////////////////////

output

start method
main method


// Note: This output is produced only by the main thread.

Here, the output is produced solely by the main thread since no new thread is created. The overridden start method in the MyThread class behaves like a regular method call, printing “start method” before executing the main method. It’s important to note that overriding the start method is not recommended in general, especially when dealing with multithreading concepts.

Note: It is not recommended to override the start method; otherwise, don’t proceed with the multithreading concept.

If we made one small change in the above program, then the output will change as follows:
Java
class MyThread extends Thread {
  public void start() {
    super.start();           // due to this line two threads run simultaneously that is child and main thread so o/p also vary
    System.out.println("Start method");
  }

  public void run() {
    System.out.println("run method");
  }
}


class Test {
  public static void main(String[] args) {
    MyThread t = new MyThread();
    t.start();
    System.out.println("main Thread");
  }
}

Here, due to the invocation of super.start() within the overridden start method of the MyThread class, two threads, the child thread, and the main thread, run simultaneously, thereby causing variations in the output. Here are the possible outputs:

Output 1

Java
run method 
Start method 
main Thread

In this case, the child thread executes its run method first, followed by the completion of the start method in the child thread, and then the main thread executes its main method.

Output 2

Java
Start method 
main Thread
run method

Here, the start method in the child thread executes first, followed by the completion of the main method in the main thread, and then the run method in the child thread executes.

Output 3

Java
Start method
run method
main Thread 

In this case, the start method in the child thread executes first, followed by the execution of the run method in the child thread, and finally, the main method in the main thread executes.

Thread Lifecycle

Threads in Java go through various states during their lifecycle:

  • New: When a thread is created but not yet started.
  • Runnable: When a thread is ready to run but waiting for CPU time.
  • Blocked: When a thread is waiting for a monitor lock to enter a synchronized block or waiting on I/O operations.
  • Waiting: When a thread is waiting indefinitely for another thread to perform a particular action.
  • Timed Waiting: Similar to waiting, but with a specified timeout period.
  • Terminated: When a thread completes its execution or is terminated prematurely.

The life cycle of a thread in Java can be outlined as follows:

Java
[New/Born] -------- t.start() --------> [Ready/Runnable] 

                                            -------- If Thread Scheduler allocates processor -------->
                                
[Running] -------- If run() method completes --------> [Dead]
  1. New/Born: The thread is created but not yet started. For instance, when you create a new instance of a thread class using MyThread t = new MyThread(), the thread is in the “New” or “Born” state.
  2. Ready/Runnable: After invoking the start() method on the thread object (t.start()), the thread becomes eligible to run, but it’s up to the thread scheduler to allocate processor time. The thread is considered “Ready” or “Runnable” at this stage.
  3. Running: When the thread scheduler allocates processor time to the thread, it enters the “Running” state, and its run() method starts executing. The thread remains in this state until its run() method completes or is paused by the scheduler to allow other threads to run.
  4. Dead: Once the run() method completes execution or the thread is explicitly terminated, the thread enters the “Dead” state. A thread is considered “Dead” when its execution is finished, and it cannot be started again.

What happens if we try to restart an already started thread?

After starting a thread, if we try to restart the same thread once again, then we will get IllegalThreadStateException.

Java
Thread t = new Thread();
t.start();
// Some other code...
t.start(); // This will throw IllegalThreadStateException
  1. You create a new thread object t.
  2. You start the thread using t.start(), which transitions the thread from the new state to the runnable state and invokes its run() method.
  3. If you attempt to start the same thread again using t.start(), you’ll encounter the IllegalThreadStateException because the thread is already in the runnable or running state and cannot be restarted.

Note: Attempting to restart a thread that has already been started violates the threading rules in Java, hence the exception. This exception occurs because a thread cannot be restarted once it has already started running or has been terminated.

Defining a Thread by Implementing the Runnable Interface (I)

When defining a thread by implementing the Runnable interface, you create a class that implements the run() method defined in the Runnable interface. The Runnable interface is present in the java.lang package and it contains only one method: public void run(). Then, you pass an instance of this class to the Thread constructor, and invoke the start() method on the Thread object. This approach allows for better flexibility in Java’s multithreading model.

Java
class MyRunnable implements Runnable { // defining a thread using the second approach, which is implementing the Runnable interface
   public void run() {
     for(int i=0; i<10; i++) { // whatever code inside the run method is the job of the thread and it is executed by the child thread  
       System.out.println("child thread");
     }
   }
}

class ThreadDemo {
  public static void main(String[] args) {
    MyRunnable r = new MyRunnable();
    Thread t = new Thread(r); // r is the target runnable
    t.start();
    
    // At this point, two threads started executing simultaneously: the child and main threads 
    for(int i=0; i<10; i++) { // executed by the main thread 
      System.out.println("main thread");
    }
  }
}

We will get mixed output, and we can’t determine the exact output.

Output

Java
main thread
main thread
child thread
child thread
main thread
main thread
child thread
child thread
main thread
main thread
child thread
child thread
main thread
child thread
main thread
child thread
main thread
child thread
main thread
child thread
  • Here, we defined a class MyRunnable that implements the Runnable interface and overrides its run() method to define the task of the thread.
  • You create an instance of MyRunnable.
  • You create a Thread object t and pass the MyRunnable instance to its constructor.
  • You start the thread using t.start(), which initiates the execution of the thread’s run() method concurrently with the main thread’s execution.
  • Both the main thread and the child thread execute simultaneously, leading to a mixed output.

Due to the concurrent execution of the main thread and the child thread, the output may vary, resulting in a mixed sequence of messages from both threads.

Case Study

Let’s dive into the different outcomes depending on the situation. consider following example:

Java
MyRunnable r = new MyRunnable();
Thread t1 = new Thread();
Thread t2 = new Thread(r);

Case 1:

Java
t1.start(); // A new thread will be created responsible for the Thread class's run method, which has empty implementation.

In this case , a new thread will be created, but since t1 is not associated with any Runnable object, the new thread will execute the run() method of the Thread class, which has an empty implementation.

Case 2:

Java
t1.run(); // No new thread will be created, and the Thread class's run method will be executed like a normal method call.

Here, No new thread will be created, and the run() method of the Thread class will be executed just like a normal method call.

Case 3:

Java
t2.start(); // A new thread will be created responsible for the execution of the MyRunnable class's run method.

A new thread will be created, and it will be responsible for executing the run() method of the MyRunnable class, as t2 is associated with the MyRunnable instance r.

Case 4:

Java
t2.run(); // No new thread will be created, and the MyRunnable class's run method will be executed like a normal method call.

No new thread will be created, and the run() method of the MyRunnable class will be executed just like a normal method call.

Case 5:

Java
r.start(); // Compile-time error: MyRunnable class doesn't have start capability (CE: cannot find symbol | symbol: method start() | location: class MyRunnable)

This will result in a compile-time error because the MyRunnable class does not have a start() method. It’s the Thread class that has the start() method.

Case 6:

Java
r.run(); // No new thread will be created, and the MyRunnable run method will be executed like a normal method call.

No new thread will be created, and the run() method of the MyRunnable class will be executed like a normal method call.

Which approach is best to define a thread?

Among the two ways of defining a thread, the “implements Runnable” approach is recommended.

In the first approach, our class always extends the Thread class, leaving no chance of extending any other class. Hence, we are missing inheritance benefits.

However, in the second approach, by implementing the Runnable interface, we can extend any other class. Therefore, we won’t miss any inheritance benefits.

Because of the above reasons, implementing the Runnable approach is recommended over extending the Thread class.

Another Way of Defining a Thread (Not Recommended)

Here’s another way of defining a thread in Java, valid but not recommended. Let’s see why.

Java
class MyThread extends Thread {
   public void run() {
     System.out.println("Child Thread");
   }
}

class ThreadDemo {
  public static void main(String[] args) {
    MyThread t = new MyThread();
    Thread t1 = new Thread(t);
    t1.start();
    System.out.println("main thread");
  }
}

Output (One of the possible outputs):

Java
Child Thread
main Thread

Or

Java
main Thread
Child Thread

Eample given above is valid but not recommended approach due to several reasons, including inflexibility and violation of good object-oriented design principles.

However, is it valid to pass a reference to another thread to the Thread constructor like this (new Thread(t))?

Java
                       +-------------------+
                       |     java.lang     |
                       |      Object       |
                       +---------+---------+
                                 ^
                                 |
               +-----------------+-----------------+
               |                                   |
   +-----------+-----------+           +-----------+-----------+
   |                       |           |                       |
+--+-------------------+   |       +---+-------------------+   |
|     java.lang      |   |       |     java.lang      |   |
|      Thread       |   |       |      Runnable     |   |
+-----------+-------+   |       +-------------------+   |
            ^           |                   ^           |
            |           |                   |           |
            |           |                   |           |
+-----------+-----------+-----------+       |       +---+-------------------+
|                                   |       |       |                       |
|           MyThread               |<------+       |      YourThread      |
|                                   |               |                       |
+-----------------------------------+               +-----------------------+

Here,

  • java.lang.Thread is shown as extending java.lang.Object and implementing the Runnable interface.
  • java.lang.Runnable is an interface implemented by the Thread class, represented by the dashed line between them.
  • Custom classes like MyThread can extend Thread and, therefore, indirectly implement the Runnable interface.

So, passing either Runnable interface or implementing class reference is acceptable and completely valid in Java, but this hybrid approach is not recommended.

Part 2: Mastering Thread Constructors, Thread Priority, yield(), join(), and sleep() Methods for Concurrent Efficiency

Conclusion

In this comprehensive guide, we’ve covered various aspects of Java multithreading, from the basics of creating and synchronizing threads to advanced concepts like thread safety, concurrency issues, and performance tuning. By mastering multithreading in Java and following best practices, you can develop efficient, scalable, and responsive applications that leverage the full power of modern multicore processors. Keep experimenting, learning, and refining your multithreading skills to build robust and high-performance software.

Skill Up: Software & AI Updates!

Receive our latest insights and updates directly to your inbox

Related Posts

error: Content is protected !!