Multithreading In Java – Exploring Parallelism

To understand a concept, we should first understand its what, why, and how. Let’s learn about what is Multithreading but before there is an important concept called `Multitasking`, we need to learn before.

What Is Multitasking? 

As the name suggests, multitasking refers to handling or performing multiple tasks at the same time. Imagine you’re in a busy restaurant kitchen preparing multiple dishes at once. You have a stove, a cutting board, and various ingredients spread out on the counter. As orders come in, you need to multitask effectively to ensure everything gets cooked and prepared on time. Multitasking in Java is like efficiently managing multiple cooking tasks simultaneously in the kitchen. Just as you need to handle different dishes simultaneously to keep the restaurant running smoothly, Java programs can handle multiple tasks concurrently.

Multitasking

In the kitchen, you might have one pot simmering on the stove, vegetables being chopped on the cutting board, and meat being grilled in the oven. Each task requires attention and progress, and you switch between them to ensure they’re all taken care of. Similarly, in Java, tasks are like different cooking operations, and the program can manage them concurrently. `Multitasking` in Java enables programs to make the most of available resources, just as you utilize your cooking appliances and tools efficiently. It allows tasks to be executed in parallel, saving time and improving overall performance.

Now multitasking can be done in two ways:

  1. Process-based multitasking 
  2. Thread-based multitasking 

Types Of Multitasking

Multitasking in Java can be categorized into two main types: process-based multitasking and thread-based multitasking.

  1. Process-Based Multitasking:

Process-based multitasking involves running multiple independent processes simultaneously. Each process runs in its own memory space, operates independently, and has its own system resources. It is suitable for tasks that are independent of each other and do not require communication or resource sharing.

  1. Thread-Based Multitasking:

Thread-based multitasking involves dividing the program’s execution into multiple threads. Threads share the same memory space and resources, allowing for efficient communication and coordination. It is useful when tasks are interdependent, require communication, or need to share data. Thread-Based Multitasking is commonly referred to as `Mutlithreading`.

Difference Between Process-Based Multitasking And Thread-Based Multitasking

Multitasking in Java can be achieved through two distinct approaches: process-based multitasking and thread-based multitasking. While both approaches involve concurrent execution of tasks, there are significant differences between them in terms of resource allocation, communication, and coordination.

Process-Based MultitaskingThread-Based Multitasking
Resource AllocationEach process runs independently in its own space. Tasks cannot directly share information.Threads run within a single program and can directly access shared data.
CommunicationProcesses need special communication mechanisms to exchange information, like passing messages through files or network connections.Threads can communicate directly by sharing variables or using synchronization tools provided by Java.
CoordinationProcesses must explicitly coordinate actions through predefined communication channels.Threads can easily coordinate actions and share information within the same program space.
Overhead and EfficiencyCreating and managing separate processes requires more resources and time. Switching between processes is relatively slower.Threads have lower overhead as they share the same program space. Switching between threads is faster and more efficient.
Scalability and FlexibilityIndependent processes offer better isolation and stability. However, managing many processes can be resource-intensive and limit scalability.Threads provide greater scalability and flexibility within a single program. However, shared data requires careful synchronization to avoid conflicts.
Multiprocessing Vs Multithreadind

What is a Thread? 

Now you read the word `thread` and multitasking can be performed using threads, i.e. `Multithreading`. But what is a thread? Let’s explore.

Imagine the same scenario of busy kitchen in which you have multiple tasks to perform concurrently. Each task requires dedicated attention and needs to be performed efficiently to ensure smooth operations. But let’s say you hire workers for each specific task. Now, think of each worker in the kitchen as a thread. Each worker focuses on a specific task but operates concurrently with others. For example, one worker might be responsible for cooking, another for washing dishes, and another for serving food. Each worker follows their own sequence of instructions to accomplish their assigned task.

Similarly, in Java, a thread is like a virtual worker that performs a specific job within your program. Each thread focuses on a particular task, such as calculating a result, processing data, or updating a user interface. Threads work together, just like multiple workers are working together, to accomplish multiple orders.

In Java, a thread is a lightweight unit of execution that runs concurrently with other threads within a program. Threads in Java allow for multitasking, where different tasks can be performed at the same time. They share the program’s resources, like memory and processing power, and can communicate with each other to coordinate their work. 

OS

As shown in the above figure, a thread is executed inside the process. There is context-switching between the threads. There can be multiple processes inside the OS, and one process can have multiple threads.

Now you know the meaning of threads, now multithreading should be crystal clear to you.

What Is Multithreading?

Multithreading refers to the concurrent execution of multiple threads within a single program. It involves running multiple threads simultaneously to achieve multitasking and parallelism. Each thread represents an independent flow of execution, allowing for efficient utilization of system resources and improved program performance.

In simple terms, multithreading allows different parts of a program to execute concurrently, similar to how multiple tasks can be performed simultaneously by different individuals. It enables tasks to be divided into smaller units that can be executed independently and in parallel.

Life Cycle Of A Thread 

The life cycle of a thread refers to the different stages a thread goes through from its creation until its termination. Understanding the thread life cycle is crucial for effective thread management and synchronization. In Java, a thread can exist in various states, each representing a specific phase in its life cycle.

  1. New State: In the new state, a thread has been created but has not yet started. It is ready to be scheduled for execution.
  2. Active State: When a thread is in the runnable/active state, it is eligible to run, and the scheduler has assigned CPU time to it. The thread may be executing, or it may be waiting for its turn to run on the CPU.
  3. Waiting State: A thread enters the waiting/blocked state when it needs to wait for a specific notification or condition to occur. This can happen when the thread is waiting for I/O operations, synchronization locks, or other blocking operations.
  4. Timed Waiting State: Similar to the waiting/blocked state, a thread can enter the timed waiting state by invoking methods like sleep() or join(), specifying a time duration to wait for. The thread remains in this state until the specified time elapses or until it receives a relevant notification.
  5. Terminated State: A thread enters the terminated state when it completes its execution or when an unhandled exception occurs. Once a thread is terminated, it cannot be restarted or run again.

This is a short explanation of the lifecycle and states of a thread. To read it in more detail, refer to the blog “LifeCycle and States of a Thread”.

Creating Threads In Java

In Java, creating threads allows you to execute multiple tasks concurrently, enhancing the performance and responsiveness of your applications. There are two main mechanisms for creating threads: `extending the Thread class` and implementing the Runnable interface.

  1. Extending the Thread Class:

One way to create a thread is by extending the Thread class. Here’s how you can do it:

public class MyThread extends Thread {
    public void run() {
        // Code to be executed by the thread
    }
}

To create a thread using this mechanism, you need to follow these steps:

a. Create a new class that extends the Thread class.

b. Override the run() method in the subclass. This method contains the code that will be executed when the thread starts.

c. Instantiate an object of the subclass.

d. Call the start() method on the object to start the execution of the thread. The start() method internally calls the run() method.

For Example:

public class MyThread extends Thread {
    private int threadId;


    public MyThread(int threadId) {
        this.threadId = threadId;
    }

    public void run() {
        System.out.println("Thread " + threadId + " is running");
        for (int i = 1; i <= 3; i++) {
            System.out.println("Thread " + threadId + ": Count " + i);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("Thread " + threadId + " finished execution");
    }
}

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

        MyThread thread1 = new MyThread(1);
        MyThread thread2 = new MyThread(2);

        thread1.start();
        thread2.start();

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

Output:

Main thread started
Thread 1 is running
Thread 2 is running
Main thread finished
Thread 1: Count 1
Thread 2: Count 1
Thread 1: Count 2
Thread 2: Count 2
Thread 1: Count 3
Thread 2: Count 3
Thread 1 finished execution
Thread 2 finished execution

Explanation:

In this example, we create two threads of the MyThread class, each representing a unique thread. The MyThread class takes an int parameter threadId to distinguish between the threads.

Within the run() method, each thread starts by printing its respective thread ID and enters a loop. In each iteration of the loop, the thread prints its current count and pauses for 1 second using the Thread.sleep() method to simulate some processing time.

The main thread starts both thread1 and thread2 using the start() method, allowing them to execute concurrently. After starting the threads, the main thread continues its execution and prints “Main thread finished”.

  1. Implementing the Runnable Interface:

Another way to create a thread is by implementing the Runnable interface. Here’s how you can do it:

public class MyRunnable implements Runnable {
    public void run() {
        // Code to be executed by the thread
    }
}

To create a thread using this mechanism, follow these steps:

a. Create a new class that implements the Runnable interface.

b. Implement the run() method in the class. This method contains the code that will be executed when the thread starts.

c. Create an instance of the class that implements Runnable.

d. Pass the instance to the constructor of the Thread class.

e. Call the start() method on the Thread object to start the execution of the thread.

For Example,

public class MyRunnable implements Runnable {
    private int threadId;


    public MyRunnable(int threadId) {
        this.threadId = threadId;
    }

    public void run() {
        System.out.println("Thread " + threadId + " is running");
        for (int i = 1; i <= 3; i++) {
            System.out.println("Thread " + threadId + ": Count " + i);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("Thread " + threadId + " finished execution");
    }
}

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

        MyRunnable runnable1 = new MyRunnable(1);
        MyRunnable runnable2 = new MyRunnable(2);

        Thread thread1 = new Thread(runnable1);
        Thread thread2 = new Thread(runnable2);

        thread1.start();
        thread2.start();

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

Output:

Main thread started
Thread 1 is running
Thread 2 is running
Main thread finished
Thread 1: Count 1
Thread 2: Count 1
Thread 1: Count 2
Thread 2: Count 2
Thread 1: Count 3
Thread 2: Count 3
Thread 1 finished execution
Thread 2 finished execution

Explanation:

In this example, we create two threads by implementing the Runnable interface. The MyRunnable class represents the runnable object and takes an int parameter threadId to differentiate between the threads.

Inside the run() method, each thread starts by printing its respective thread ID and enters a loop. In each iteration of the loop, the thread prints its current count and pauses for 1 second using the Thread.sleep() method to simulate some processing time. In the Main class, we create instances of MyRunnable (runnable1 and runnable2). Then, we create Thread objects (thread1 and thread2) by passing the corresponding MyRunnable instances as arguments.

We start both threads using the start() method, allowing them to execute concurrently. After starting the threads, the main thread continues its execution and prints “Main thread finished”.

Both mechanisms allow you to create threads in Java and execute tasks concurrently. Extending the Thread class is useful when you want the thread to be the main focus and have direct control over its behavior. Implementing the Runnable interface is beneficial when you want to separate the task’s behavior from the thread itself, promoting better code organization and reusability.

Java Thread Class

In Java, the Thread class is a fundamental building block for creating and managing threads. It provides a rich set of `methods` and `functionalities` that facilitate thread creation, control, and coordination. The Thread class is part of the `java.lang package, and it serves as a foundation for working with threads in Java.

Thread Methods  

In Java, the Thread class provides a variety of methods that allow you to work with Java threads and control their behavior. These methods enable you to manage thread execution, synchronization, and communication. Here are some important thread methods in Java:

Return TypeMethodDescription
voidstart()Starts the execution of a thread. It’s like pressing the play button for the thread to begin doing its work.
voidrun()Contains the code that will be executed by the thread. You define what the thread should do in this method.
voidsleep(long millis)Pauses the thread’s execution for a specified time, allowing other threads to run. It’s like taking a short nap before continuing work.
voidyield()Suggests that the thread willingly gives up its turn to execute, allowing other threads to run instead. It’s like saying, “You can go ahead, I’ll wait.”
voidjoin()Waits for another thread to finish its work before continuing. It’s like waiting for a friend to catch up before moving on together.
voidinterrupt()Interrupts the thread’s execution, like tapping someone on the shoulder to get their attention. It signals the thread to stop what it’s doing.
booleanisInterrupted()Checks if the thread has been interrupted. It returns true if the thread has been interrupted, and false otherwise.
booleanisAlive()Checks if the thread is still active and running. It returns true if the thread is running, and false otherwise.
voidsetName(String name)Sets a name for the thread, like giving it a unique identifier or label. It helps in identifying threads during debugging or monitoring.
intgetPriority()Retrieves the priority of the thread. Threads can have different priorities, indicating how important they are compared to others.

These are just a few examples of the thread methods available in the Thread class. Each method serves a specific purpose and helps you control the execution, synchronization, and behavior of threads in your Java programs.

Thread Priority And Scheduling

Thread priority and scheduling in Java determine the order and allocation of CPU time among multiple threads. Thread priority is an integer value ranging from 1 to 10, where higher values indicate higher priority. However, thread priorities are not always strictly followed, and their effectiveness can vary across different systems.

For example,

public class ThreadPriorityExample {

    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {

            // Perform some computation
            for (int i = 0; i < 5; i++) {
                System.out.println("Thread 1 executing");
            }
        });

        Thread thread2 = new Thread(() -> {
            // Perform some I/O operation
            for (int i = 0; i < 5; i++) {
                System.out.println("Thread 2 executing");
            }
        });

        thread1.setPriority(Thread.MAX_PRIORITY);         // Set thread1's priority to maximum (10)
        thread2.setPriority(Thread.MIN_PRIORITY);         // Set thread2's priority to minimum (1)

        thread1.start();
        thread2.start();
    }
}

Output:

Thread 1 executing
Thread 1 executing
Thread 1 executing
Thread 1 executing
Thread 1 executing
Thread 2 executing
Thread 2 executing
Thread 2 executing
Thread 2 executing
Thread 2 executing

In this example, we have two threads: thread1 and thread2. We set thread1 to have the highest priority (10) using Thread.MAX_PRIORITY, and thread2 to have the lowest priority (1) using Thread.MIN_PRIORITY. Then, we start both threads, and they execute concurrently. 

In this output, you can see that thread1 with higher priority (10) gets more CPU time compared to thread2 with lower priority (1). However, please keep in mind that the actual output may vary depending on your system and JVM implementation. The behavior of thread scheduling can be influenced by various factors, and it’s not always deterministic.

Benefits Of Multithreading  

  1. Improved Responsiveness: Multithreading allows concurrent execution of tasks, ensuring a responsive user interface and faster completion of operations.
  2. Enhanced Performance: Multithreading leverages multiple cores for faster execution and efficient resource utilization.
  3. Concurrency and Resource Sharing: Multithreading enables efficient resource sharing and concurrent access to shared resources.
  4. Parallelism and Scalability: Multithreading enables parallel execution and scalability, improving program performance for complex tasks.
  5. Modularity and Maintainability: Multithreading promotes modular design and maintainable code.
  6. Asynchronous Programming: Multithreading enables asynchronous execution and efficient handling of concurrent events.

Conclusion

  • There are two types of multitasking: process-based multitasking and thread-based multitasking.
  • Process-based multitasking involves executing multiple processes simultaneously, while thread-based multitasking enables concurrent execution within a single process.
  • Threads in Java represent separate flows of execution within a program, allowing for concurrent and parallel processing.
  • Understanding the life cycle of a thread helps manage its states and execution flow.
    The life cycle of a thread consists of various states, such as `new`, `active`, `waiting`, `timed-waiting`, and `terminated`.
  • Threads can be created by extending the Thread class or implementing the Runnable interface.
  • The Thread class in Java provides essential methods and functionality for working with threads.
  • Thread methods like start(), join(), sleep(), and interrupt() control thread execution and synchronization.
  • Thread priority and scheduling influence the order and time allocation for thread execution.
  • Multithreading offers benefits such as improved responsiveness, performance, resource utilization, concurrency, and scalability.

Leave a Reply

Your email address will not be published. Required fields are marked *