Multithreading in Java

Multithreading is a programming paradigm that allows multiple threads (smaller units of a process) to execute concurrently. It is a vital concept in modern programming, enabling efficient CPU utilization and improving the responsiveness of applications. This article will delve into the fundamentals of multithreading, its advantages, and practical examples, particularly in the context of Java programming.



Understanding Multithreading in Java

Multithreading is a programming paradigm that allows multiple threads (smaller units of a process) to execute concurrently. It is a vital concept in modern programming, enabling efficient CPU utilization and improving the responsiveness of applications. This article will delve into the fundamentals of multithreading, its advantages, and practical examples, particularly in the context of Java programming.

What is Multithreading?

Multithreading refers to the ability of a CPU, or a single core in a multicore processor, to provide multiple threads of execution concurrently. This enables a program to perform various tasks at the same time. Multithreading is crucial in modern operating systems, allowing several applications or processes to run simultaneously.

In our daily lives, we naturally engage in multitasking; for example, while driving, we might listen to music, follow traffic rules, and engage in conversation. Similarly, in programming, especially in Java, the goal is to manage multiple threads to enhance the efficiency of the application.


Multitasking vs. Multithreading

Multitasking is the capability of an operating system to manage multiple tasks at once. For instance, in a Windows environment, users can print a document while simultaneously downloading files and checking emails. This is made possible through different processes executing concurrently in memory.

Multithreading, on the other hand, is a subset of multitasking. It involves executing different parts of the same program simultaneously. For example, when copying a file on a computer, one thread might handle the file transfer, while another manages the display of a progress bar.

The fundamental difference is that multitasking involves multiple processes running independently, while multithreading consists of multiple threads executing within a single process, sharing the same memory space.

How Multithreading Works in Java

In Java, every application begins with a single thread called the “main” thread. This thread can create additional threads, and each thread runs independently but shares the same memory space. The Java Virtual Machine (JVM) uses a thread scheduler to manage the execution of threads.

When multiple threads are executed on a single-core processor, they do not run simultaneously. Instead, the CPU switches between threads in a manner that gives the illusion of parallelism. This switching occurs so quickly that it seems as if the threads are running concurrently.

Modern multicore processors can execute multiple threads at once, which significantly improves performance. Each core can handle a different thread, thereby genuinely performing several tasks simultaneously.


Advantages of Multithreading

  1. Responsiveness: Applications can remain responsive to user inputs while performing background operations. For instance, in a word processor like MS Word, the spell checker runs in a separate thread, allowing users to continue typing without interruption.
  2. Simplified Program Organization: Multithreading allows developers to break down complex problems into simpler, concurrent tasks. This separation helps avoid cluttering code with intermingled logic, making it easier to maintain and understand.
  3. Improved Performance: In many cases, applications need to wait for input/output (I/O) operations, which can be slower than processing tasks. By utilizing separate threads for I/O operations, the CPU can allocate its time to other threads, enhancing overall performance.

Creating Threads in Java

Java provides two primary methods for creating threads:

  1. Extending the Thread Class: By creating a new class that extends the Thread class and overriding the run() method, you can define the code that will execute in that thread.
  2. Implementing the Runnable Interface: This method involves implementing the Runnable interface and defining the run() method. You can then create a Thread object, passing an instance of the implementing class.

Here’s a brief look at both methods:

Example 1: Extending the Thread Class

class MyThread extends Thread {
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println("Child Thread: " + i);
        }
    }
}

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

Example 2: Implementing the Runnable Interface

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

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

Managing Thread Execution

Java offers several methods to manage threads, such as sleep(), join(), and isAlive().

  • sleep(long millis): This static method pauses the execution of the current thread for the specified duration. It is useful for creating timed delays in execution.
  • join(): This method allows one thread to wait for another thread to finish its execution. It is useful when you want the main thread to wait until other threads complete their tasks.
  • isAlive(): This method checks if a thread is still running. It returns true if the thread is alive and false otherwise.

Practical Example of Multithreading

Consider a scenario where we need to read multiple files and count the number of lines in each. Doing this sequentially would result in a longer processing time. Instead, we can use multithreading to read files concurrently.

Single-Threaded Approach

import java.io.*;

public class SingleThread {
    public static void main(String[] args) throws Exception {
        System.out.println("Starting Time: " + System.currentTimeMillis());
        for (String fileName : args) {
            BufferedReader reader = new BufferedReader(new FileReader(fileName));
            int lineCount = 0;
            while (reader.readLine() != null) {
                lineCount++;
            }
            System.out.println("Lines in " + fileName + ": " + lineCount);
        }
        System.out.println("Ending Time: " + System.currentTimeMillis());
    }
}

Multi-Threaded Approach

import java.io.*;

class FileReadThread extends Thread {
    private String fileName;

    public FileReadThread(String fileName) {
        this.fileName = fileName;
    }

    public void run() {
        try (BufferedReader reader = new BufferedReader(new FileReader(fileName))) {
            int lineCount = 0;
            while (reader.readLine() != null) {
                lineCount++;
            }
            System.out.println("Lines in " + fileName + ": " + lineCount);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

public class MultiThread {
    public static void main(String[] args) {
        System.out.println("Starting Time: " + System.currentTimeMillis());
        for (String fileName : args) {
            new FileReadThread(fileName).start();
        }
        System.out.println("Ending Time: " + System.currentTimeMillis());
    }
}
package sample;

public class Sample {
    public static void main(String[] args) throws Exception {
        Output c = new Output();
        Ex t1 = new Ex(c, "KICIT");
        t1.start();
        Ex t2 = new Ex(c, "Nagpur");
        t2.start();
        Ex t3 = new Ex(c, "India");
        t3.start();

        t1.join();
        t2.join();
        t3.join();
    }
}

class Ex extends Thread {
    private Output o;
    private String message;

    public Ex(Output c, String msg) {
        o = c;
        message = msg;
    }

    public void run() {
        o.display(message);
    }
}

class Output {
    synchronized void display(String msg) {
        System.out.print("[" + msg);
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            // Handle exception
        }
        System.out.println("]");
    }
}

In the multi-threaded approach, each file is processed in its own thread, significantly reducing the total time taken to read multiple files.


Understanding Thread Synchronization in Java

Multithreading is a powerful feature in programming that allows multiple threads to run concurrently, enhancing application performance and responsiveness. However, when threads share resources, synchronization becomes crucial to prevent data inconsistency and erratic behavior. This article explores the necessity of synchronization, provides a practical example, and discusses inter-thread communication and thread priorities in Java.


The Need for Synchronization

In a multithreaded environment, if multiple threads attempt to access and modify shared resources simultaneously without coordination, the outcome can be unpredictable. For instance, consider a simple scenario where two threads modify the same variable concurrently. Without proper synchronization, the final value of the variable may not reflect the intended changes, leading to unreliable and erratic behavior.


A Practical Example of Synchronization

Let’s illustrate the need for synchronization using a method that displays messages with brackets. The goal is to ensure that messages are printed in an orderly manner, maintaining their formatting.

void display(String msg) {
    System.out.print("[");
    System.out.print(msg);
    Thread.sleep(1000);
    System.out.println("]");
}

Suppose three different threads call this method to display messages: “KICIT”, “Nagpur”, and “India”. Without synchronization, the output could be jumbled:

[India[Nagpur[KICIT]
]
]

To rectify this, we can synchronize the display method, ensuring that only one thread can execute it at a time. Here’s how we can implement this using Java:

Explanation of the Code

  1. Classes Defined:
  • Sample: Contains the main method to start the application.
  • Ex: A thread class that extends Thread to process messages.
  • Output: Contains the synchronized display method to print messages.
  1. Synchronized Method: The display method is marked with the synchronized keyword. This means that once a thread enters this method, other threads attempting to access it will be blocked until the executing thread exits.
  2. Thread Creation: In the main method, we create an instance of Output and three Ex threads, each with a different message. Each thread calls the synchronized display method, ensuring orderly output.
  3. Joining Threads: The join method ensures that the main thread waits for all child threads to complete before proceeding, allowing us to see the final output.
  4. Desired Output:
   [KICIT]
   [Nagpur]
   [India]

This implementation ensures that the messages are printed correctly, demonstrating the need for synchronization.


The Synchronized Block

In some cases, you may not have access to the source code of a class, or it may not have been designed with synchronization in mind. In such situations, you can use a synchronized block to control access to the method without modifying it directly.

Here’s how you can use a synchronized block in the run method:

public void run() {
    synchronized (o) {
        o.display(message);
    }
}

This approach ensures that while one thread is executing the display method, no other thread can enter the synchronized block that accesses the same resource.


Inter-thread Communication

While synchronization prevents concurrent access issues, it may also lead to inefficiencies. To improve performance, Java provides mechanisms for threads to communicate with each other using wait(), notify(), and notifyAll() methods. These methods allow one thread to notify another thread when it can resume execution, facilitating better resource management.

  • wait(): Causes the current thread to wait until another thread invokes the notify() or notifyAll() methods on the same object.
  • notify(): Wakes up a single thread that is waiting on the object’s monitor.
  • notifyAll(): Wakes up all threads that are waiting on the object’s monitor.

A common application of these methods is in the Producer-Consumer problem, where one thread produces data while another consumes it.


Thread Priorities

Java allows you to assign priorities to threads, which can influence the order in which they are executed by the thread scheduler. Higher priority threads are allocated processor time before lower priority ones.

Java defines three standard priorities:

  • MIN_PRIORITY: A value of 1.
  • NORM_PRIORITY: A value of 5 (the default).
  • MAX_PRIORITY: A value of 10.

You can set a thread’s priority using the setPriority(int level) method and retrieve it with getPriority().

Thread thread1 = new Thread();
thread1.setPriority(Thread.MAX_PRIORITY); // Set maximum priority

While thread priority can influence scheduling, it does not guarantee execution order, as it ultimately depends on the thread scheduler of the operating system.