× back
Next Topic → ← Previous Topic

Multithreaded Programming

Multithreading in Java

Before delving into multithreading, it's essential to understand the concept of multitasking in computing.

Now, let's explore the concept of a "thread" in more detail:

Threads in Computing

A thread is the smallest unit of execution within a program. It represents a sequence of instructions that can be scheduled and executed independently by the computer's CPU. Threads within a process share the same memory space and resources, allowing them to work together to accomplish tasks concurrently.

Threads are commonly used to perform multiple operations simultaneously, enhance program responsiveness, and efficiently utilize modern multi-core processors.

Java provides extensive support for thread-based multitasking through its multithreading capabilities. Understanding threads and multithreading is crucial for building responsive and efficient Java applications that can perform multiple tasks concurrently.

Java provides extensive support for thread-based multitasking through its multithreading capabilities. Understanding multithreading is crucial for building responsive and efficient Java applications that can perform multiple tasks concurrently.

What is Multithreading?

Multithreading is a concurrent execution process where multiple threads run independently at the same time without dependency on each other. In Java, it allows you to execute multiple threads within a single process, enhancing program performance and responsiveness by making efficient use of the CPU.

  • Multithreading Saves Time and Enhances Performance:
  • Multithreading is used to save time and increase the performance of Java applications. By dividing tasks into multiple threads, a program can execute multiple operations simultaneously, taking advantage of modern multi-core processors.

  • Applications in Animation and Game Development:
  • Java is commonly used in animation and game development, where multithreading allows multiple animated characters or game elements to appear on the screen simultaneously. Each character or element can be controlled by a separate thread, enabling concurrent and independent actions.

What is a Thread?

A thread is a fundamental unit of execution in a program. It represents a sequence of instructions that can run independently and concurrently with other threads. In Java, threads are instances of the pre-defined `Thread` class, available in the `java.lang` package.

  • Basic Unit of CPU and Independent Execution:
  • Threads are often referred to as the basic units of a CPU because they can be scheduled and executed independently. Each thread shares the same memory space and resources within a process, enabling them to perform tasks simultaneously.

How to Create Threads in Java?

In Java, you can create threads in two main ways:

  1. By Extending the Thread Class:
  2. You can create a new thread by extending the `Thread` class and overriding its `run()` method. This approach allows you to define the thread's behavior within the `run()` method.

  3. By Implementing the Runnable Interface:
  4. Another way to create threads is by implementing the `Runnable` interface. This approach separates the thread's behavior from the thread object itself. You need to provide the implementation of the `run()` method in a separate class that implements `Runnable` and then create a `Thread` object that executes the `Runnable` instance.

Multitasking vs. Multithreading

It's important to distinguish between multitasking and multithreading:

  • Multitasking:
  • Multitasking refers to the concurrent execution of multiple processes or applications by the operating system. These processes can be entirely independent and may or may not share resources.

  • Multithreading:
  • Multithreading, on the other hand, involves concurrent execution within a single process. Multiple threads within the same process share the same memory space and resources, allowing them to work together to achieve parallelism and perform tasks simultaneously.

Defining Threads by Extending the Thread Class

  • Introduction:
  • In Java, you can create threads by extending the `Thread` class. This approach allows you to define your thread's behavior by overriding the `run()` method. Threads created using this method are also known as "child threads," and they run concurrently with the main thread of the program.

                    
class A extends Thread {
    @Override
    public void run() {
        for (int i = 1; i <= 5; i++) {
            System.out.println("Hello");
        }
    }
}

class B {
    public static void main(String[] args) {
        A t = new A();
        t.start(); // Starting the child thread

        // Code below is part of the main thread, while the code above is part of the child thread
        for (int i = 1; i <= 5; i++) {
            System.out.println("Hello");
        }
    }
}
                    
                
  • The `run` Method in Thread Class:
  • The `run` method is already defined in the `Thread` class, but you can override it to provide your custom implementation. When you start a thread, the JVM automatically calls its `run` method.

  • Concurrency:
  • When you run the program, the order in which the threads run is not guaranteed. Each time you execute the program, the output sequence may differ. However, what is certain is that both the main thread and the child thread run concurrently.

If you want to introduce a delay between the output, you can use the `Thread.sleep(1000)` method. However, you need to handle exceptions since `Thread.sleep()` can throw an `InterruptedException`. Here's an example:

                    
class A extends Thread {
    @Override
    public void run() {
        try {
            for (int i = 1; i <= 5; i++) {
                System.out.println("Hello");
                Thread.sleep(1000); // Pause for 1 second
            }
        } catch (InterruptedException e) {
            // Handle the interrupted exception
        }
    }
}

class B {
    public static void main(String[] args) throws InterruptedException {
        A t = new A();
        t.start(); // Starting the child thread

        for (int i = 1; i <= 5; i++) {
            System.out.println("Hello");
        }
    }
}
                    
                

Defining Threads Using the Runnable Interface

  • Introduction:
  • In Java, you can create threads by implementing the `Runnable` interface. This approach provides more flexibility compared to extending the `Thread` class because it allows you to separate the thread's behavior from the thread object. Threads created using the `Runnable` interface are also known as "runnable threads."

Syntax to Define a Thread:

    
class A implements Runnable {
    public void run() {
        // Define the thread's job or task here
    }
}

class B {
    public static void main(String[] args) {
        A obj = new A();
        // obj.start(); // This won't work because we need a Thread object to start a thread
        Thread t = new Thread(obj); // Create a Thread object with obj as a reference
        t.start(); // Now the thread will execute the run() method defined in class A
    }
}
    

Example:

    
class A implements Runnable {
    public void run() {
        for (int i = 1; i <= 5; i++) {
            System.out.println("My child Thread");
        }
    }
}

class B {
    public static void main(String[] args) {
        A r = new A(); // Create an instance of class A, which implements Runnable

        Thread t = new Thread(r); // Create a Thread object with the Runnable instance r
        t.start(); // Start the child thread

        // The main thread is responsible for running the following code
        for (int i = 1; i <= 5; i++) {
            System.out.println("My main Thread");
        }
    }
}
    

What is a Thread Scheduler?

  • A Thread Scheduler is a crucial part of the Java Virtual Machine (JVM) responsible for executing multiple threads on a single processor in a seemingly random manner. It manages when and how much time each thread will be allocated for execution.
  • Within the Thread Scheduler, various algorithms are employed to make these decisions. The scheduler determines which thread to execute first and how much time each thread will receive. For instance, if there are three threads that each require 5 units of execution time, the Thread Scheduler may select one thread for execution, allowing it to run only 4 times. The remaining execution time is then allocated to other threads. This decision-making process is governed by algorithms such as First-Come-First-Serve (FCFS), Shortest Job First (SJF), and Round Robin.

Here's an example to demonstrate the use of a Thread Scheduler:

                        
class A extends Thread {
    public void run() {
        String name = Thread.currentThread().getName();
        for (int i = 1; i <= 3; i++) {
            System.out.println(name);
        }
    }
}

class Test {
    public static void main(String[] args) {
        A t1 = new A();
        A t2 = new A();
        A t3 = new A();

        t1.setName("Thread 1");
        t2.setName("Thread 2");
        t3.setName("Thread 3");

        t1.start();
        t2.start();
        t3.start();

        String mainThreadName = Thread.currentThread().getName();
        for (int i = 1; i <= 3; i++) {
            System.out.println(mainThreadName);
        }
    }
}
                    
                

Explanation of functions used in the program:

  • Thread.currentThread().getName(): This function retrieves the name of the current thread.
  • Thread.start(): This method is used to start the execution of a thread. When called, it invokes the run() method of the thread.
  • run(): This method is overridden in the A class and defines the task that the thread will perform when started.
  • setName(String name): This method sets the name of a thread, making it easier to identify when multiple threads are running concurrently.
  • System.out.println(): This function is used to print messages to the console.

The program demonstrates the use of multiple threads (t1, t2, t3) running concurrently and independently alongside the main thread. Each thread prints its name multiple times, and the main thread also prints its name, showing the concurrent execution of threads.

When to Use Multithreading and Why?

  • Multithreading is employed when there are independent tasks that can execute concurrently. For example, in the program above, threads t1, t2, and t3 are assigned the independent task of setting their respective names. The order in which they set their names is not critical, and they can work concurrently. Thus, when multiple independent tasks exist, multithreading is a suitable choice.
  • In the program, there are a total of four threads: t1, t2, t3, and the main thread. Each of these threads can execute independently and concurrently.
  • Advantages of the Main Thread:
    • The main thread serves as the entry point for the Java program. It's responsible for initiating the program's execution.
    • It can be used for performing tasks that need to be coordinated with the program's start and end.
    • The main thread is essential for tasks like user interface initialization, resource allocation, and program termination.

What is Thread Life Cycle?

A thread has a well-defined life cycle during which it can transition through different states, each serving a specific purpose in the execution process:

  • New State (Born): In this initial state, a thread is created but has not yet started its execution. To create a new thread, we use the 'new' keyword and create an instance of a thread class.
  • Runnable State (Ready): After the thread is created, it transitions to the runnable state when we invoke the t.start() method. In this state, the thread is ready to be executed but is waiting for the thread scheduler to assign it CPU time.
  • Running State (Execution): When the thread is selected by the thread scheduler, it moves into the running state. During this phase, the thread's code is actively being executed, and it performs its designated tasks.
  • Waiting State (Blocked): Threads can enter the waiting state when certain conditions are met. For instance, a thread can transition to the waiting state if we use methods like t.join(), t.sleep(), t.wait(), or t.suspend(). These methods cause the thread to temporarily pause its execution and move to the waiting state. It can later return to the ready state if picked by the thread scheduler.
    • When using join, sleep, or wait methods, the thread moves directly from the blocked state back to the ready state after a specific time period, which is usually specified in milliseconds. For example, sleep(1000) would make the thread sleep for 1 second before returning to the ready state.
  • Dead State (Exit): The final state in a thread's life cycle is the dead state. A thread reaches this state when it has completed its execution. To explicitly terminate a thread and send it to the dead state, we can use the t.stop() method, although it's important to note that this method is not recommended due to its potential for abrupt termination and resource leaks.

The thread life cycle provides a structured model for understanding the various phases a thread goes through during its execution, from creation to completion.

Thread Transitions in Multithreading

Understanding Thread Transitions

In multithreading, thread transitions refer to the various states that a thread can move through during its lifecycle. Threads in a multithreaded program can transition between different states as they are created, started, paused, resumed, and terminated. Understanding these thread states is crucial for managing and controlling the execution of threads effectively.

Thread States:

Threads in Java can exist in several states, including:

  1. New: The thread has been created but has not yet started executing.
  2. Runnable: The thread is ready to run and waiting for its turn to be scheduled by the thread scheduler.
  3. Running: The thread is currently executing its code.
  4. Blocked: The thread is temporarily suspended because it's waiting for a particular condition, such as acquiring a lock or input/output operation.
  5. Waiting: The thread is in a waiting state and will remain so until another thread notifies it to resume its execution.
  6. Timed Waiting: Similar to the waiting state, but the thread will automatically transition out of this state after a specified time interval.
  7. Terminated: The thread has completed its execution and has terminated. It cannot be restarted.

Thread Transition Example:

Here's a simplified example illustrating thread transitions:

                
public class ThreadTransitionsExample {
    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(() -> {
            System.out.println("Thread 1: Running");
        });

        Thread thread2 = new Thread(() -> {
            System.out.println("Thread 2: Running");
        });

        thread1.start(); // Thread 1 transitions from "New" to "Runnable" to "Running."
        thread2.start(); // Thread 2 transitions from "New" to "Runnable" to "Running."

        Thread.sleep(1000);

        // Note: The following methods (suspend, resume, and stop) are deprecated and not recommended for modern multithreaded programming.
        // Instead, use interrupt and join for better control over thread execution.
        
        thread1.suspend(); // Thread 1 transitions to "Blocked." (Deprecated)
        thread2.resume(); // Thread 2 transitions to "Running."

        Thread.sleep(1000);

        thread1.resume(); // Thread 1 transitions back to "Running." (Deprecated)
        thread1.stop();   // Thread 1 transitions to "Terminated." (Deprecated)

        System.out.println("Thread 1 state: " + thread1.getState());
        System.out.println("Thread 2 state: " + thread2.getState());
    }
}
            
            

In this example, two threads (thread1 and thread2) are created and go through various thread state transitions, including starting, suspending (deprecated), resuming (deprecated), and stopping (deprecated).

Understanding thread transitions is essential for designing reliable multithreaded applications and ensuring that threads behave as expected throughout their lifecycles.

The following code has been updated to remove the use of deprecated methods:

                        
public class Test {
    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(() -> {
            System.out.println("Thread 1: Running");
        });

        Thread thread2 = new Thread(() -> {
            System.out.println("Thread 2: Running");
        });

        thread1.start(); // Thread 1 transitions from "New" to "Runnable" to "Running."
        thread2.start(); // Thread 2 transitions from "New" to "Runnable" to "Running."

        Thread.sleep(1000);

        thread1.interrupt(); // Thread 1 transitions to "Blocked" if asleep, or "Runnable" if not interrupted.
        Thread.sleep(1000);

        thread1.join(); // Wait for Thread 1 to finish its execution.
        thread2.join(); // Wait for Thread 2 to finish its execution.

        System.out.println("Thread 1 state: " + thread1.getState());
        System.out.println("Thread 2 state: " + thread2.getState());
    }
}
                        
                    

What is the Sleep Method?

  • The `sleep` method is a static method of the `Thread` class in Java, and it can throw a checked exception known as `InterruptedException`.
  • Its primary purpose is to temporarily pause or put a thread into a waiting state for a specified duration.

Syntax:

                    
Thread.sleep(milliseconds);  <!-- Example syntax -->
// OR
Thread t = new Thread();
t.sleep(milliseconds);
                    
                

The `milliseconds` represent the duration in milliseconds for which the thread should sleep.

Example Program:

                    
class A extends Thread {
    public void run() {
        String threadName = Thread.currentThread().getName();
        try {
            for (int i = 1; i <= 3; i++) {
                System.out.println("From: " + threadName + " -- " + i);
                Thread.sleep(1000); // Sleep for 1 second
            }
        } catch (InterruptedException e) {
            System.out.println(e);
        }
    }
}

class Test {
    public static void main(String[] args) {
        A t1 = new A();
        A t2 = new A();
        A t3 = new A();

        t1.setName("Thread 1");
        t2.setName("Thread 2");
        t3.setName("Thread 3");

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

In this example, the `sleep` method is used to introduce delays while printing messages from multiple threads.

Handling InterruptedException:

  • The `try-catch` block is used to catch `InterruptedException` because the `sleep` method can throw this exception if the thread is interrupted by another thread.
  • This exception allows the sleeping thread to handle interruptions gracefully in multi-threaded programs.

join() Method in Java

  • The join() method in Java is a fundamental method for managing threads. It is used to ensure that a thread, in which the join() method is called, completes its execution before other threads can proceed. This method is particularly useful when you need to coordinate the execution of multiple threads.
  • The main purpose of the join() method is to put the calling thread into a temporary waiting state until the thread on which join() is called finishes its execution. This ensures that the calling thread waits for the specified thread to complete.
  • Additionally, the join() method can throw a checked exception, namely InterruptedException. This exception typically occurs if the thread waiting with join() is interrupted while waiting for the target thread to complete. Proper exception handling is essential when using join().

Here is an example of how the join() method can be used:

                    
class A extends Thread {
    public void run() {
        String threadName = Thread.currentThread().getName();
        for (int i = 1; i <= 3; i++) {
            System.out.println("From " + threadName + ": " + i);
        }
    }
}

class Test {
    public static void main(String[] args) {
        A t1 = new A();
        A t2 = new A();
        A t3 = new A();

        t1.setName("Thread 1");
        t2.setName("Thread 2");
        t3.setName("Thread 3");

        t2.start(); // Start Thread 2
        try {
            t2.join(); // Wait for Thread 2 to complete
        } catch (InterruptedException e) {
            System.out.println(e);
        }
        t1.start(); // Start Thread 1
        t3.start(); // Start Thread 3
    }
}
                
            

In this example, we create three threads (t1, t2, and t3). We start t2 and then call t2.join(), which causes the main thread to wait until t2 completes its execution. This ensures that the output from t2 is printed before t1 and t3 start running.

Difference between sleep() and join() methods in Java

Java provides two methods, sleep() and join(), for managing the execution of threads in a multi-threaded application. While both methods involve temporarily suspending the execution of a thread, they serve different purposes and are used in distinct scenarios:

  • Sleep(): The sleep() method is used to put a thread into a temporary waiting state for a specified amount of time. During this waiting period, the thread releases the CPU, allowing other threads to execute. After the specified time elapses, the thread re-enters the ready state and competes for CPU time with other threads.
  • Join(): The join() method is used to put the parent thread into a temporary waiting state until the completion of a specified child thread. When a parent thread invokes join() on a child thread, it waits for the child thread to finish its execution before proceeding further. This is particularly useful when you want to coordinate the execution order of threads or ensure that certain tasks are completed before moving on.

Here's an example to demonstrate the use of both sleep() and join():

                    
class MyRunnable implements Runnable {
    @Override
    public void run() {
        try {
            // Simulate some work in the child thread
            Thread.sleep(2000); // Sleep for 2 seconds
            System.out.println("Child thread has completed.");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

public class ThreadExample {
    public static void main(String[] args) {
        // Create an instance of the Runnable implementation
        Runnable myRunnable = new MyRunnable();

        // Create a Thread and pass the Runnable to it
        Thread childThread = new Thread(myRunnable);

        // Start the child thread
        childThread.start();

        try {
            // Parent thread waits for the child thread to complete
            childThread.join(); // Wait for the child thread to finish
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("Parent thread continues.");
    }
}
                    
                

In this example, the parent thread starts a child thread, which simulates some work using `sleep()`. The parent thread then uses `join()` to wait for the child thread to finish. As a result, the "Child thread has completed." message is guaranteed to be printed before the "Parent thread continues." message, demonstrating the coordination achieved with the `join()` method.

Suspend() and Resume() Methods in Java (Deprecated)

In Java, the suspend() and resume() methods were historically used to control the execution of threads. However, it is crucial to understand that these methods are deprecated since Java 2 and are strongly discouraged for use in modern multithreaded applications. The reason for deprecation is their inherent shortcomings, including the potential for thread deadlock and other synchronization issues. Instead, Java offers more reliable and safer mechanisms for thread control and coordination, such as the wait() and notify() methods, or higher-level concurrency utilities provided by the Java concurrency framework.

Below, we'll briefly discuss the deprecated suspend() and resume() methods and provide an example:

Suspend() Method (Deprecated)

  • The main purpose of the suspend() method was to put a thread from the running state to the waiting state. When a thread was suspended, it was temporarily halted in its current state.

Example:

                    
A t1 = new A();
A t2 = new A();
A t3 = new A();

t1.start();
t2.start();
t3.start();

t2.suspend(); // Thread t2 is suspended and will not be executed.
                    
                
  • In the example above, t2 is suspended using the suspend() method, which caused it to stop executing.
  • To resume a suspended thread, you would typically use the resume() method (deprecated).

Resume() Method (Deprecated)

  • The resume() method (deprecated) was used to resume a suspended thread, transitioning it from the waiting state back to the runnable state, allowing it to continue execution.
                    
t2.resume(); // Thread t2 is resumed and can continue executing (deprecated).
                    
                

Given the issues associated with the suspend() and resume() methods, it is strongly recommended that you avoid using them in your code. Instead, consider using more modern and safer synchronization mechanisms and concurrency utilities provided by Java, such as the wait() and notify() methods or the higher-level java.util.concurrent classes, for effective thread control and coordination.

Wait() and Notify() Methods in Java

In Java, the wait() and notify() methods are essential for thread synchronization and coordination. These methods are considered fundamental in multithreaded programming and are recommended for managing thread execution.

wait() Method

  • The wait() method is used to make a thread temporarily release its lock on an object and enter a waiting state. This is typically used when a thread needs to wait for a specific condition to be met before proceeding.

Example:

                    
class SharedResource {
    private boolean condition = false;

    synchronized void waitForCondition() throws InterruptedException {
        while (!condition) {
            wait(); // Release the lock and wait for the condition to be true.
        }
    }

    synchronized void setConditionTrue() {
        condition = true;
        notify(); // Notify a waiting thread that the condition has been met.
    }
}
                    
                

notify() Method

  • The notify() method is used to notify a waiting thread that the condition it was waiting for has been met. It allows one waiting thread to continue its execution.

Example:

                    
class ExampleThread extends Thread {
    private SharedResource resource;

    public ExampleThread(SharedResource resource) {
        this.resource = resource;
    }

    public void run() {
        try {
            Thread.sleep(1000); // Simulate some work.
            resource.setConditionTrue(); // Set the condition to true and notify waiting threads.
        } catch (InterruptedException e) {
            System.out.println(e);
        }
    }
}

public class Test {
    public static void main(String[] args) {
        SharedResource resource = new SharedResource();

        ExampleThread thread = new ExampleThread(resource);

        thread.start();

        try {
            resource.waitForCondition(); // Wait for the condition to be true.
            System.out.println("Condition is now true. Continuing execution.");
        } catch (InterruptedException e) {
            System.out.println(e);
        }
    }
}
                    
                

In the example above, the wait() and notify() methods are used to coordinate the execution of two threads. The main thread waits for the condition to be set to true by the ExampleThread, and when the condition is met, it continues its execution.

Using wait() and notify() methods, you can create more robust and predictable multithreaded applications by allowing threads to communicate and synchronize their actions effectively.

What is Thread Priority in Java?

In Java, thread priority is a way to influence the order in which threads are scheduled to run by the thread scheduler. Each thread has a priority assigned to it, and threads with higher priority values have a better chance of being executed before threads with lower priority values. However, it's important to note that thread priority should be used judiciously, as it doesn't guarantee precise execution order and can vary depending on the underlying system.

Here are some key points about thread priority:

  • If you don't explicitly set the priority of a thread, it will be assigned a default priority by the JVM, which is typically 5.
  • You can manually set the priority of a thread using the methods setPriority() and retrieve its priority using getPriority().
  • The Thread class provides three predefined final static variables for priority, with values ranging from 1 to 10:
    • Thread.MIN_PRIORITY with a value of 1, representing the lowest priority.
    • Thread.NORM_PRIORITY with a value of 5, representing the default or normal priority.
    • Thread.MAX_PRIORITY with a value of 10, representing the highest priority.

It's important to use thread priority carefully, as relying solely on priority for thread synchronization and coordination can lead to unpredictable behavior. Thread priority should be considered as a hint to the thread scheduler, and other synchronization mechanisms should be used for precise control over thread execution.

Example:

                    
class PriorityExample extends Thread {
    public void run() {
        System.out.println("Thread " + Thread.currentThread().getName() + " with priority " + Thread.currentThread().getPriority() + " is running.");
    }

    public static void main(String[] args) {
        PriorityExample thread1 = new PriorityExample();
        PriorityExample thread2 = new PriorityExample();

        thread1.setPriority(Thread.MAX_PRIORITY);
        thread2.setPriority(Thread.MIN_PRIORITY);

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

In this example, we create two threads with different priorities using the setPriority() method. Thread 1 is set to MAX_PRIORITY, and Thread 2 is set to MIN_PRIORITY. The output may vary depending on the thread scheduler, but Thread 1 has a higher chance of running before Thread 2 due to its higher priority.

yield() Method in Java

In Java, the yield() method is a part of the Thread class and is used to allow other threads with equal or higher priority to have a chance to run. It essentially temporarily pauses the execution of the current thread, giving way to other threads in the same priority range. However, it's important to understand that the effectiveness of the yield() method may vary depending on the operating system and the thread scheduler.

Here's how the yield() method works:

  • When a thread calls yield(), it voluntarily relinquishes the CPU, indicating that it has no objection to other threads running.
  • The scheduler then determines which thread to execute next, considering factors like thread priority and the specific scheduling algorithm.
  • Threads with higher priority may be favored, but it ultimately depends on the underlying scheduling policy.

Example:

                    
class A extends Thread {
    public void run() {
        String threadName = Thread.currentThread().getName();
        for (int i = 1; i <= 3; i++) {
            System.out.println("From " + threadName + ": " + i);
        }
    }
}

class Test {
    public static void main(String[] args) {
        A t1 = new A();
        A t2 = new A();
        A t3 = new A();

        t1.setName("Thread 1");
        t2.setName("Thread 2");
        t3.setName("Thread 3");

        t1.setPriority(Thread.MIN_PRIORITY); // Lower priority
        t2.setPriority(Thread.MAX_PRIORITY); // Highest priority
        t3.setPriority(Thread.NORM_PRIORITY); // Default priority

        t1.start();
        t2.start();
        t3.start();

        t2.yield(); // Thread 2 yields, allowing other threads to run.
    }
                }
                    
                
  • In this example, we create three threads with different priorities: t1 (lowest), t2 (highest), and t3 (default).
  • Thread t2 calls yield(), indicating that it's willing to let other threads run. The scheduler then decides which thread to execute next.
  • The effect of yield() depends on the thread priorities and the specific behavior of the thread scheduler.

While the yield() method can be used for thread control, it is often better to rely on other synchronization mechanisms and thread management techniques to achieve predictable and robust multithreaded behavior.

stop() and interrupt() Methods in Java (Deprecated and Preferred)

The stop() method is a deprecated way of terminating a thread in Java. It forcefully stops a thread's execution without allowing it to finish its current work or clean up resources. However, it's strongly discouraged due to potential issues like thread instability, data corruption, and resource leaks. Instead, Java offers a safer and preferred way of thread termination using the interrupt() method.

stop() Method (Deprecated)

  • The stop() method was used to terminate a thread immediately, which often left the thread in an inconsistent state, leading to data corruption and application instability.

interrupt() Method (Preferred)

  • The interrupt() method is the recommended way to gracefully terminate a thread in Java.
  • When a thread is interrupted, it receives an interrupt status but continues to execute unless it periodically checks its interrupt status using isInterrupted() or encounters a blocking operation that throws an InterruptedException.
  • To terminate a thread, you set its interrupt status using interrupt() and handle the interruption within the thread's run() method by checking the interrupt status and exiting gracefully when appropriate.

Example:

            
class A extends Thread {
    public void run() {
        String threadName = Thread.currentThread().getName();
        for (int i = 1; i <= 3; i++) {
            System.out.println("From " + threadName + ": " + i);
        }
        if (Thread.interrupted()) { // Check for interruption.
            System.out.println("Thread " + threadName + " is interrupted. Exiting gracefully.");
            return;
        }
    }
}

class Test {
    public static void main(String[] args) {
        A t1 = new A();
        A t2 = new A();
        A t3 = new A();

        t1.start();
        t2.start();

        t2.interrupt(); // Interrupt Thread t2.

        t3.start();
    }
}
                
            

In this example, we use the interrupt() method to gracefully terminate t2. The thread periodically checks its interrupt status using Thread.interrupted() and exits when interrupted, allowing for proper resource cleanup.

It's essential to use the interrupt() method and handle thread interruption correctly to ensure that your multithreaded Java applications are stable and reliable.

isAlive() Method in Java

The isAlive() method in Java is used to check the current status of a thread. It returns true if the thread is currently running (i.e., it has been started and has not yet completed its execution), and it returns false if the thread has terminated, either by naturally completing its run() method or due to an unhandled exception.

Here's a brief overview of how the isAlive() method works:

  • When you call isAlive() on a thread object, it provides information about whether the thread is actively executing.
  • This method is useful for checking the status of a thread and making decisions based on whether it's still running.

Example:

                    
class ExampleThread extends Thread {
    public void run() {
        // Simulate some work.
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            System.out.println(e);
        }
    }

    public static void main(String[] args) {
        ExampleThread thread = new ExampleThread();
        System.out.println("Before starting the thread, isAlive: " + thread.isAlive());

        thread.start();
        System.out.println("After starting the thread, isAlive: " + thread.isAlive());

        try {
            thread.join();
        } catch (InterruptedException e) {
            System.out.println(e);
        }

        System.out.println("After the thread has finished, isAlive: " + thread.isAlive());
    }
}
                    
                

In this example, we create a thread, start it, and use isAlive() to check its status at different points in the program. The method correctly reflects whether the thread is currently running or has completed its execution.

The isAlive() method is a valuable tool for managing and monitoring the status of threads in a multithreaded Java application.

Understanding Synchronization in Multithreading

Why Synchronization is Needed?

Multithreading is a powerful technique for concurrent execution, allowing tasks to be completed efficiently. However, it introduces challenges when multiple threads attempt to access shared resources simultaneously. This can lead to unexpected and incorrect results due to data corruption or race conditions. To address these issues, synchronization mechanisms are essential to ensure orderly and safe access to shared resources in multithreaded programs.

Challenges in Multithreading

  • In multithreaded environments, multiple threads may execute concurrently and access shared resources, such as variables or objects, simultaneously.
  • Without proper synchronization, threads can interfere with each other's execution, leading to data inconsistencies and unexpected program behavior.

Example

Consider a simple example to illustrate the need for synchronization:

  • Imagine a bus with only one available seat, and there are three passengers who want to occupy that seat simultaneously.
  • In a multithreading conMultithreaded Programming, this situation parallels when multiple threads attempt to execute the same function or access the same data without synchronization.
  • Without synchronization, all threads might concurrently modify shared data, resulting in data corruption or unpredictable outcomes.

Here is a basic analogy to the bus example:

                    
class Bus {
    private int availableSeats = 1;

    public void bookSeat() {
        if (availableSeats > 0) {
            // Simulate some booking process.
            availableSeats--;
            System.out.println("Seat booked by " + Thread.currentThread().getName());
        } else {
            System.out.println("No seats available.");
        }
    }
}

class PassengerThread extends Thread {
    private Bus bus;

    public PassengerThread(Bus bus) {
        this.bus = bus;
    }

    public void run() {
        bus.bookSeat();
    }
}

public class BusBookingApp {
    public static void main(String[] args) {
        Bus bus = new Bus();
        PassengerThread passenger1 = new PassengerThread(bus);
        PassengerThread passenger2 = new PassengerThread(bus);
        PassengerThread passenger3 = new PassengerThread(bus);

        passenger1.start();
        passenger2.start();
        passenger3.start();
    }
}
                
            

In this example, multiple passenger threads try to book a seat in the bus simultaneously. Without synchronization, it's possible for more than one passenger to book the same seat, leading to incorrect results.

To address these issues and ensure orderly access to shared resources, synchronization mechanisms like locks, mutexes, and synchronized blocks are used in multithreaded programming.

What is Synchronization?

Synchronization is a crucial technique in multithreading that enables controlled access to shared resources among multiple threads. It ensures that only one thread can enter a synchronized block or method at a time, preventing concurrent and potentially problematic access to shared data.

Purpose of Synchronization

The primary purpose of synchronization is to address the challenges that arise in multithreading when multiple threads simultaneously attempt to access the same shared resource. Without synchronization, these situations can lead to undesirable outcomes, including data corruption and incorrect program behavior.

Types of Synchronization

Synchronization in Java can be broadly classified into two categories:

  1. Method-Level Synchronization: In method-level synchronization, the entire method is synchronized using the synchronized keyword. Only one thread can execute the synchronized method at a time.
  2. Block-Level Synchronization: Block-level synchronization allows for finer control. Specific blocks of code within a method can be synchronized using synchronized blocks, denoted by synchronized (object). This provides better granularity and performance in some scenarios.

Method-Level Synchronization

Method-level synchronization in Java is achieved by using the synchronized keyword to declare a method. When a method is marked as synchronized, only one thread can execute that method on the same object instance at a time. This ensures that the critical section of code within the synchronized method is protected from concurrent access by multiple threads, preventing race conditions and data corruption.

Here's an example of method-level synchronization:

                    
class Hotel {
    private int guests = 0;

    public synchronized void checkIn() {
        // Synchronized method.
        guests++;
        System.out.println("Guest checked in. Total guests: " + guests);
    }

    public synchronized void checkOut() {
        // Synchronized method.
        guests--;
        System.out.println("Guest checked out. Total guests: " + guests);
    }
}
                    
                

In this example, the checkIn() and checkOut() methods are declared as synchronized. This means that only one guest can check in or check out at a time, preventing conflicts and ensuring the guests variable is accessed safely.

Method-level synchronization is suitable when you want to protect an entire method or when the entire method needs to be atomic and thread-safe. However, it can lead to contention if multiple threads frequently access the synchronized methods concurrently, potentially causing performance bottlenecks.

It's important to choose the appropriate level of synchronization based on your application's requirements. In some cases, block-level synchronization may offer better performance and flexibility.

Block-Level Synchronization

Block-level synchronization in Java allows for more fine-grained control over synchronization compared to method-level synchronization. Instead of synchronizing entire methods, you can synchronize specific blocks of code using the synchronized keyword with an object as a monitor. This approach provides greater flexibility and can lead to improved performance by reducing contention in multithreaded applications.

Here's an example of block-level synchronization:

                    
class Account {
    private double balance = 1000;
    private Object lock = new Object(); // Monitor object for synchronization.

    public void withdraw(double amount) {
        synchronized (lock) { // Synchronized block.
            if (balance >= amount) {
                balance -= amount;
                System.out.println("Withdrawal of $" + amount + " successful. New balance: $" + balance);
            } else {
                System.out.println("Insufficient funds for withdrawal.");
            }
        }
    }

    public void deposit(double amount) {
        synchronized (lock) { // Synchronized block.
            balance += amount;
            System.out.println("Deposit of $" + amount + " successful. New balance: $" + balance);
        }
    }
}
                    
                

In this example, we have an Account class with withdraw() and deposit() methods. Instead of synchronizing the entire methods, we use synchronized blocks to protect critical sections of code within those methods. The lock object serves as a monitor for synchronization.

Block-level synchronization is preferred in scenarios where you want to synchronize specific code sections, allowing other parts of the methods to execute concurrently. It provides better granularity and can lead to reduced contention and improved performance.

When choosing between method-level and block-level synchronization, consider the synchronization requirements of your application to ensure both correctness and efficiency in your multithreaded code.

Static Synchronization in Java

Introduction to Static Synchronization

In Java, static synchronization is a synchronization mechanism that allows you to control access to static methods or static data members of a class by multiple threads. It ensures that only one thread can execute a synchronized static method or access synchronized static data at a time, preventing data corruption and race conditions in a multithreaded environment.

The Need for Static Synchronization

Static synchronization was introduced to address a specific problem associated with synchronization in Java:

  • In multithreaded applications, it's common for multiple threads to need access to shared resources, including static methods and static data members of a class.
  • Without synchronization, concurrent access to shared static resources can lead to race conditions and unpredictable program behavior.

Static synchronization was introduced to provide a mechanism for coordinating access to static resources across multiple threads, ensuring data integrity and preventing issues associated with unsynchronized access.

Example:

Here's an example of static synchronization:

            
class SharedResource {
    private static int counter = 0;

    public static synchronized void incrementCounter() {
        // Synchronized static method.
        counter++;
    }
}

public class StaticSynchronizationExample {
    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                SharedResource.incrementCounter();
            }
        });

        Thread thread2 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                SharedResource.incrementCounter();
            }
        });

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

        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            System.out.println(e);
        }

        System.out.println("Final counter value: " + SharedResource.getCounter());
    }
}
            
            

In this example, the incrementCounter() method of the SharedResource class is synchronized as a static method. This ensures that only one thread can increment the counter at a time, even though multiple threads are accessing it concurrently.

Static synchronization is a valuable tool for managing concurrent access to shared static resources and is commonly used in multithreaded Java applications.

Collection and Generic Framework

Collections Framework

Why do we need collections?

How are collections available?

The Collection class is available in the java.util package. The Collection class also provides static methods for sorting, searching, etc.

Advantage of Collection framework

  • Reusability: Standarized interfaces allow deveopers to use algorithms with different collections.
  • Interporability: Code written with the collections framework can work seamlessly with various types of collections.
  • Performance: Optimized algorithms for common operations on collections.

Generic Framework

  • Generics in Java allow the creation of classes, interfaces, and methods with placeholder types.
  • Introduced in Java 5 to provide stronger type-checking at compile time and eliminate the risk of runtime errors.

Key Concepts:

  • Type parameters:
    • Placeholder types used in the declaration of classes, interfaces, and methods.
    • Enclosed in angle brackets("<>").
  • Advantages:
    • Type safety: Ensures that the code is type-correct at compile time.
    • Code Reusability: Generics enable the creation of reusable components that work with different data types.
  • Generic Collections:
    ArrayList<E>, LinkedList<E>:
    • "E" represents the type of element stored in the list.

Integration of Collections and Generics:

  • Benefits:
    • Combining collections and generics provides type-safe and efficient ways to work with different data structures.
    • Enhanced readability and maintainability of code.

Example:

        
ArrayList<String> arrlist = new ArrayList<>();
arrlist.add("Hello");
arrlist.add("World");
        
    
  • In this above example both Collections Framework and Generics are being used.
  • 'ArrayList' is part of the Collection Framework. It is a class that implements the 'List' interface, which is one of the core interfaces in the collections framework.
  • The '<String>' part is an example of Generics in Java. Generics allow you to create classes, interfaces, and methods that operate on types as parameters.
  • In this case, ArrayList<String> indicates that the ArrayList is designed to hold elements of type String. This provides type safety, ensuring that only strings can be added to this specific instance of ArrayList.

ArrayList

  • In Java, the ArrayList class is a part of the java.util package, and it provides a resizable array implementation.
                            
import java.util.ArrayList;

public class ArrayListExample {
    public static void main(String[] args) {
        // Creating an ArrayList
        ArrayList<String> fruits = new ArrayList<>();

        // Adding elements to the ArrayList
        fruits.add("Apple");
        fruits.add("Banana");
        fruits.add("Orange");

        // Displaying the elements of the ArrayList
        System.out.println("ArrayList elements: " + fruits);

        // Getting the size of the ArrayList
        int size = fruits.size();
        System.out.println("Size of ArrayList: " + size);

        // Accessing elements by index
        String firstFruit = fruits.get(0);
        System.out.println("First fruit: " + firstFruit);

        // Checking if the ArrayList contains a specific element
        boolean containsBanana = fruits.contains("Banana");
        System.out.println("Contains Banana? " + containsBanana);

        // Removing an element by value
        fruits.remove("Banana");
        System.out.println("After removing Banana: " + fruits);

        // Removing an element by index
        fruits.remove(0);
        System.out.println("After removing the first element: " + fruits);

        // Checking if the ArrayList is empty
        boolean isEmpty = fruits.isEmpty();
        System.out.println("Is ArrayList empty? " + isEmpty);

        // Clearing all elements from the ArrayList
        fruits.clear();
        System.out.println("After clearing the ArrayList: " + fruits);

        // Checking if the ArrayList is empty again
        isEmpty = fruits.isEmpty();
        System.out.println("Is ArrayList empty now? " + isEmpty);
    }
}
                            
                        

Explanation of basic methods used:

  1. add(E element): Adds the specified element to the end of the list.
  2. size(): Returns the number of elements in the list.
  3. get(int index): Returns the element at the specified position in the list.
  4. contains(Object o): Returns true if the list contains the specified element.
  5. remove(Object o): Removes the first occurrence of the specified element from the list.
  6. remove(int index): Removes the element at the specified position in the list.
  7. isEmpty(): Returns true if the list contains no elements.
  8. clear(): Removes all elements from the list.

LinkedList

  • A LinkedList in Java is a class that implements the List interface.
  • It represents a linked list data structure where each element (node) points to the next one.
  • Each element in a LinkedList is a node containing data and a reference (or link) to the next node.
  • Advantages:
    1. Dynamic size: Can easily grow or shrink.
    2. Efficient insertion and deletion operations.
    3. No need for contiguous memory allocation.
  • Common Operations:
    1. Adding Elements:
      • add(element): Appends the specified element to the end of the list.
      • add(index, element): Inserts the specified element at the specified position.
    2. Removing Elements:
      • remove(): Removes and returns the first element.
      • remove(index): Removes the element at the specified position.
    3. Accessing Elements:
      • get(index): Retrieves the element at the specified position.
      • set(index, element): Replaces the element at the specified position.
                            
import java.util.LinkedList;

public class LinkedListDemo {
    public static void main(String[] args) {
        // Creating a LinkedList of Strings
        LinkedList linkedList = new LinkedList<>();

        // Adding elements
        linkedList.add("Apple");
        linkedList.add("Banana");
        linkedList.add("Cherry");

        // Displaying the LinkedList
        System.out.println("LinkedList: " + linkedList);

        // Adding an element at a specific position
        linkedList.add(1, "Orange");

        // Displaying the updated LinkedList
        System.out.println("Updated LinkedList: " + linkedList);

        // Removing an element
        linkedList.remove("Banana");

        // Displaying the final LinkedList
        System.out.println("Final LinkedList: " + linkedList);

        // Accessing elements by index
        System.out.println("Element at index 2: " + linkedList.get(2));

        // Size of the LinkedList
        System.out.println("Size of the LinkedList: " + linkedList.size());

        // Clearing the LinkedList
        linkedList.clear();

        // Displaying the LinkedList after clearing
        System.out.println("LinkedList after clearing: " + linkedList);
    }
}
                            
                        

Stack

  • A Stack in Java is a class that extends the Vector class and implements the List interface.
  • It represents a Last-In, First-Out (LIFO) data structure where the last element added is the first one to be removed.
  • Common Operations:
    • push(element): Adds an element to the top of the stack.
    • pop(): Removes and returns the element from the top of the stack.
    • peek(): Retrieves the element from the top of the stack without removing it.
    • empty(): Checks if the stack is empty.
    • search(element): Searches for an element and returns its position (distance from the top of the stack).
                            
import java.util.Stack;

public class StackDemo {
    public static void main(String[] args) {
        // Creating a Stack of Integers
        Stack stack = new Stack<>();

        // Pushing elements onto the stack
        stack.push(10);
        stack.push(20);
        stack.push(30);

        // Displaying the Stack
        System.out.println("Stack: " + stack);

        // Popping an element
        int poppedElement = stack.pop();
        System.out.println("Popped Element: " + poppedElement);

        // Displaying the updated Stack
        System.out.println("Updated Stack: " + stack);

        // Peeking at the top element
        int topElement = stack.peek();
        System.out.println("Top Element: " + topElement);

        // Checking if the stack is empty
        System.out.println("Is Stack empty? " + stack.empty());

        // Searching for an element
        int position = stack.search(10);
        System.out.println("Position of 10 in the stack: " + position);

        // Clearing the Stack
        stack.clear();

        // Displaying the Stack after clearing
        System.out.println("Stack after clearing: " + stack);
    }
}
                            
                        

ArrayDeque

  • An ArrayDeque in Java is a class that implements the Deque interface.
  • It represents a resizable, double-ended queue, supporting both FIFO (First-In, First-Out) and LIFO (Last-In, First-Out) operations.
  • Common Operations:
    • add(element): Adds an element to the end of the deque.
    • addFirst(element): Adds an element to the beginning of the deque.
    • addLast(element): Adds an element to the end of the deque.
    • remove(): Removes and returns the first element.
    • removeFirst(): Removes and returns the first element.
    • removeLast(): Removes and returns the last element.
    • peek(): Retrieves the first element without removing it.
    • peekFirst(): Retrieves the first element without removing it.
    • peekLast(): Retrieves the last element without removing it.
    • size(): Returns the number of elements in the deque.
    • isEmpty(): Checks if the deque is empty.
    • clear(): Removes all elements from the deque.
                            
import java.util.ArrayDeque;

public class ArrayDequeDemo {
    public static void main(String[] args) {
        // Creating an ArrayDeque of Strings
        ArrayDeque arrayDeque = new ArrayDeque<>();

        // Adding elements to the end of the deque
        arrayDeque.add("Apple");
        arrayDeque.add("Banana");
        arrayDeque.add("Cherry");

        // Displaying the ArrayDeque
        System.out.println("ArrayDeque: " + arrayDeque);

        // Adding elements at the beginning and end of the deque
        arrayDeque.addFirst("Orange");
        arrayDeque.addLast("Grapes");

        // Displaying the updated ArrayDeque
        System.out.println("Updated ArrayDeque: " + arrayDeque);

        // Removing the first and last elements
        String removedFirst = arrayDeque.removeFirst();
        String removedLast = arrayDeque.removeLast();

        // Displaying the ArrayDeque after removal
        System.out.println("ArrayDeque after removal: " + arrayDeque);

        // Peeking at the first and last elements
        String firstElement = arrayDeque.peekFirst();
        String lastElement = arrayDeque.peekLast();

        System.out.println("First Element: " + firstElement);
        System.out.println("Last Element: " + lastElement);

        // Checking if the deque is empty
        System.out.println("Is ArrayDeque empty? " + arrayDeque.isEmpty());

        // Clearing the ArrayDeque
        arrayDeque.clear();

        // Displaying the ArrayDeque after clearing
        System.out.println("ArrayDeque after clearing: " + arrayDeque);
    }
}
                            
                        

Previous Year Questions

With illustrations explain multithreading, interrupting threads, thread states and thread properties. Write a code snapshot to show the used of yield(), stop() and sleep() methods.

Discuss the following:
(i) Thread Synchronization
(ii) Runnable interface

Define Multithreading in Java? Explain life cycle thread? State the programme which shows the difference between Runnable and Thread class.