Chapter 38: Multithreading in Java - Part 1

Chapter 38: Multithreading in Java - Part 1

Table of contents

Introduction to Chapter 38

Welcome to Chapter 38 of the Full Stack Java Development Series, where we delve into one of the most powerful features of Java: Multithreading. This chapter lays the foundation for understanding how Java enables concurrent execution of tasks, enhancing the responsiveness and performance of applications. By the end of this chapter, you'll have a solid understanding of the theoretical and practical aspects of multithreading, preparing you to write efficient, concurrent programs in Java.


Syllabus Overview

The topics we'll cover in this chapter are:

  1. Introduction to Multithreading

    • What is Multithreading?

    • Types of Multitasking

      • Process-Based Multitasking

      • Thread-Based Multitasking

  2. Defining and Starting Threads

    • Extending the Thread class

    • Implementing the Runnable interface

  3. Thread Management

    • Thread constructors

    • Setting thread priorities

    • Naming threads

  4. Controlling Thread Execution

    • Methods like yield(), join(), and sleep()
  5. Synchronization in Threads

  6. Inter-Thread Communication

  7. Understanding Deadlock

  8. Daemon Threads

  9. Advanced Topics

    • Thread Suspension and Resumption

    • Thread Groups

    • Green Threads

    • Thread Local Variables

  10. Thread Lifecycle


Multitasking in Computers

Before diving into threads, let's explore the broader concept of multitasking:

What is Multitasking?

Multitasking refers to executing multiple tasks simultaneously. This is made possible by the Operating System (OS), which manages multiple applications and tasks in the computer's RAM. For instance:

  • Typing a Java program while listening to music and downloading a file.

  • Watching a YouTube video while editing documents and attending a Zoom call.

The OS enables these simultaneous activities by efficiently managing system resources, ensuring tasks don’t interfere with each other.


Types of Multitasking

Process-Based Multitasking:

Definition
Process-based multitasking refers to the execution of multiple tasks simultaneously, where each task runs as an independent process. The operating system (OS) handles the management and execution of these processes, ensuring smooth transitions between them.


Key Characteristics

  1. Independence:
    Each task operates as a separate process, isolated from others. They don't share memory unless explicitly designed to do so.

  2. Context Switching:

    • The OS facilitates transitions between processes through context switching.

    • This involves saving the state of the current process and loading the state of the next.

    • While effective, context switching can create a computational burden on the OS.

  3. Heavyweight Operation:

    • Processes require more resources like memory and CPU time.

    • High resource demand can slow down the system, especially with limited RAM.


Real-Life Examples

  1. Typing a Java program in a text editor.

  2. Listening to music on an MP3 player.

  3. Downloading a file from the internet.

  4. Browsing the web using a browser.


OS-Level Role

  • The OS is pivotal in managing process-based multitasking.

  • It enables users to switch seamlessly between applications (e.g., from a text editor to a music player).

  • All processes run on RAM, coordinated by the OS.


Mechanisms

1. Context Switching

  • Definition: Transitioning the CPU from executing one process to another.

  • Purpose: Ensures smooth user experience without disruptions.

  • Process:

    • Saves the current process state.

    • Loads the state of the next process.

  • Impact:

    • Utilizes hardware components like CPU registers and RAM.

    • Imposes a computational load, making it a "heavyweight" operation.

2. Resource Management

  • Heavyweight multitasking requires the OS to manage RAM effectively.

  • Overloaded systems may experience slowdowns or crashes.


Advantages

  1. True Parallelism: Independent tasks run concurrently.

  2. OS Management: The operating system handles complexities, abstracting it from the user.

  3. User-Friendly Navigation: Seamless switching between applications enhances productivity.


Disadvantages

  1. High Resource Consumption: Requires significant memory and CPU resources.

  2. System Lag: Overburdened systems may experience reduced performance.

  3. Complex Implementation: Context switching increases the complexity of OS design.


Visualization

Consider three processes:

  1. Process 1: Editing code in EditPlus.

  2. Process 2: Watching a video on YouTube.

  3. Process 3: Listening to an MP3.

All these processes coexist in RAM, switching control as directed by the OS.


Summary

Process-based multitasking is fundamental to modern computing, allowing multiple independent processes to run concurrently. While resource-intensive, it is essential for OS-level multitasking, making it a cornerstone of efficient system design and user experience.


2.Thread-based Multi-tasking in Java

What is Thread-based Multi-tasking?

Executing several tasks simultaneously, where each task is a separate independent part of the same program, is called Thread-based Multi-tasking.

  • Each independent part is technically referred to as a thread.

  • Java provides inbuilt support for working with threads through APIs such as Thread, Runnable, ThreadGroup, ThreadLocal, and others.

Advantages of Thread-based Multi-tasking

  1. Reduced Response Time: Multi-tasking helps decrease the waiting time of tasks, improving application performance.

  2. Improved Performance: Assigning threads to tasks ensures efficient CPU utilization.

Key Application Areas

Thread-based multi-tasking is widely used in:

  • Developing multimedia graphics.

  • Building web application servers (a concept learned in JEE).

  • Designing video games.

  • Creating animations.


How Java Handles Multi-threading

  1. Developer Effort: Java developers write only 10% of the logic for multi-threading.
    The remaining 90% is managed by Java's API, making multi-threading implementation easier.

  2. Execution Management:

    • Writing a few lines of code allows developers to handle different tasks in different ways.

    • Java's JVM (Java Virtual Machine) manages all task execution.


Thread Basics

Thread

A thread is technically referred to as a line of execution.
Threads can either execute tasks sequentially (single-threading) or simultaneously (multi-threading).

Single-threaded Execution

In single-threaded execution:

  • Execution starts at Task-1.

  • Tasks 2 and 3 wait until Task-1 finishes.

  • This increases waiting time, which decreases the performance of the application.

Characteristics:

  • There is only one line of execution, referred to as a single thread.

  • Sequential execution results in increased waiting time and reduced performance.

Example Code:

// Single-threaded execution
public class SingleThreadExample {
    public static void main(String[] args) {
        System.out.println("Task-1 is executing...");
        System.out.println("Task-2 is executing...");
        System.out.println("Task-3 is executing...");
    }
}

In this example:

  • Task-2 and Task-3 will only execute after Task-1 completes.

Disadvantages:

  • Waiting time increases significantly.

  • Performance decreases due to the sequential execution of tasks.


Multi-threading Execution

How Does Multi-threading Work?

  1. Tasks are categorized to ensure they are independent of each other.
    For instance:

    • Task-1, Task-2, and Task-3 are independent tasks.
  2. Each independent task is assigned its own line of execution (thread).

Steps to Enable Multi-threading

  1. Categorize Tasks: Ensure each task is independent.

  2. Create Threads: Assign a separate thread to each task.

Example Code:

// Multi-threaded execution
public class MultiThreadExample {
    public static void main(String[] args) {
        Thread task1 = new Thread(() -> System.out.println("Task-1 is executing..."));
        Thread task2 = new Thread(() -> System.out.println("Task-2 is executing..."));
        Thread task3 = new Thread(() -> System.out.println("Task-3 is executing..."));

        task1.start();
        task2.start();
        task3.start();
    }
}

Here:

  • Each task runs on its independent thread, allowing for simultaneous execution.

  • Waiting time is reduced, and performance improves.

Lightweight Nature of Threads

  • In Java, JVM manages threads, making them lightweight.

  • JVM handles switching between threads at the thread level, not at the memory level.
    Since this operation is not directly controlled by hardware, threads are considered lightweight.


Advantages of Multi-threading

  1. Effective CPU Utilization:
    By promoting multi-tasking, CPU time is utilized effectively.

  2. Improved Performance:
    The response time of applications is reduced, enhancing performance.

  3. Context Switching:

    • In thread-based applications, JVM handles context switching with the support of a thread scheduler.

    • In process-based applications, context switching is managed by the Operating System (OS).


Thread APIs in Java

Java makes multi-threading development simple and efficient. Here's why:

  1. Java provides extensive API support for threads:

    • APIs like Thread, Runnable, and ThreadLocal are part of the java.lang package.
  2. API Structure:

    • The java.lang package contains .class files for multi-threading.

    • Developers focus on defining what tasks threads need to perform (10% of the logic), while the API handles the rest.

Real-world Applications

Multi-threading is commonly used in:

  1. Gaming Applications: For rendering and user interaction in real time.

  2. Zoom Meetings: For managing video, audio, and chat streams simultaneously.


Comparison: Single-threading vs. Multi-threading

AspectSingle-threadingMulti-threading
ExecutionSequential (Task-1 → Task-2 → Task-3)Parallel (Task-1, Task-2, Task-3 run simultaneously)
PerformanceDecreases with waiting timeIncreases with independent task execution
ControlSingle line of executionMultiple lines of execution
Waiting TimeHighLow
ManagementJVM manages a single threadJVM manages multiple threads

1. Response Time

  • The processor (CPU) is the central control of any computer action.

  • Hardware devices, such as RAM, hard disks, and CPUs, are made operational by the Operating System (OS).

  • The OS is responsible for deciding how much time is allocated to every process.


2. Execution of Tasks

System Workflow Overview:
  • OS manages interactions between Hard Disk, RAM, and Microprocessor (CPU).

    • Files like .java, .mp3, or .mp4 are loaded into RAM before being executed by the CPU.

Task-1: Executing Java Code
  • Java code is loaded into RAM, and this process is termed as loading.

  • The JVM (Java Virtual Machine) handles execution under CPU's guidance.

    • CPU spends time processing instructions as directed by the OS.

    • For example, executing a Java program might take 3 minutes.

Task-2: Listening to Music
  • A music file (.mp3) is moved to RAM.

    • CPU dedicates time for its processing.

    • Example: 2 minutes allocated by the OS.

Task-3: Watching a Video
  • A video file (.mp4) is loaded into RAM upon opening.

    • CPU time: Example: 3 minutes allocated.

3. Key Concepts

  • Time Allocation: The OS decides how much time the CPU spends on each task.

  • Context Switching:

    • OS switches between tasks (processes) to ensure effective utilization of CPU time.

    • This switching is measured in clock cycles (Hz).

  • Multithreading:

    • Using multithreading ensures tasks are completed efficiently, maximizing CPU time.

4. Multitasking at OS Level vs. Thread Level

  • OS Level: Context switching between processes (e.g., Java program, music, video).

  • Thread Level: Context switching between threads (smaller units of a process) is managed by the Thread Scheduler within the JVM.

    • Example: In a 3-minute Java program, three tasks could be allocated 1 minute each.

5. Multithreading in Java

  • Java enables multithreading to maximize performance by using CPU time effectively.

  • 90% of the multithreading logic is implemented in the Java API; developers only need to write the remaining 10%.

  • Java developers are responsible for:

    1. Defining tasks for the threads.

    2. Creating threads for these tasks.

    3. Using thread-related APIs effectively.


6. Challenges in Multithreading

  1. Writing multithreaded code.

  2. Defining tasks for threads.

  3. Creating threads for individual tasks.

  4. Using Java’s thread API correctly.

  5. Ensuring CPU time is effectively utilized.


7. Context Switching in Multithreaded Applications

  • Process-based applications:

    • Context switching is managed by the OS.
  • Thread-based applications:

    • Context switching is managed by the JVM’s Thread Scheduler.

8. Agenda of Multitasking

  • Improve CPU utilization and system performance.

9. Example Allocation by the OS

  • Total time allocated: 3 minutes.

    • Thread-1: Executes Task 1 for 1 minute.

    • Thread-2: Executes Task 2 for 1 minute.

    • Thread-3: Executes Task 3 for 1 minute.


4. What is a Thread?

A thread represents a separate flow of execution within a program. If a program executes in a single flow, it is referred to as Single-Threaded Programming. However, a thread allows multiple independent flows of execution, enabling Multithreading.

  • Thread Characteristics:

    • Each thread performs a separate task (job).

    • In Java, threads can be created in two ways:

      1. By implementing the Runnable interface.

      2. By extending the Thread class.

When is Multithreading Useful?

Multithreading is applicable when tasks are independent and can run concurrently. For instance, when there are two or more tasks that do not depend on each other, multithreading improves CPU utilization and program efficiency.


5. How to Define, Instantiate, and Start a Thread?

a. Extending the Thread Class
  • To create a thread by extending the Thread class:

    1. Identify the Task: Define the work a thread needs to perform inside a run() method.

    2. Create a User-Defined Class: Extend the Thread class in this class.

    3. Override the run() Method: Define the thread's specific job inside this method.

Code Example:

class MyThread extends Thread {  // Define a thread by extending Thread class.
    @Override
    public void run() {          // Task of the thread.
        for (int i = 1; i <= 10; i++) {
            System.out.println("Child Thread");
        }
    }
}
public class MultithreadingExample {
    public static void main(String[] args) {
        MyThread t = new MyThread();  // Thread instantiation.
        t.start();                   // Start the thread (calls run() internally).

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

Explanation of Execution:

  1. Line 1: MyThread t = new MyThread();

    • The MyThread object is created in the heap area.

    • The .class file for MyThread is loaded into the Method Area.

  2. Line 2: t.start();

    • The start() method in the Thread class is invoked.

    • A new thread is created and scheduled for execution.

    • The run() method is called to perform the thread's task.

  3. Main Thread:

    • The main thread executes tasks in the main() method.

    • At Line 3, two threads (main and child) start competing for CPU time.


JVM Internals for Thread Execution

  1. Method Area:

    • The .class files (Test.class, MyThread.class) are loaded into the method area.

    • The JVM identifies the main() method in Test.class.

  2. Stack Area:

    • The main thread is automatically created by the JVM.

    • The main() method is loaded into the stack area for execution.

  3. Heap Area:

    • Objects, such as MyThread t, are created in the heap.
  4. Thread Scheduler:

    • The thread scheduler, implemented as part of the JVM, decides which thread gets CPU time based on an algorithm.

    • The scheduling algorithm is vendor-specific and confidential.


Multi-Threading Behavior

  • Task Execution:

    • The main thread executes the logic inside the main() method.

    • The child thread executes the logic inside the run() method.

  • Concurrency:

    • Threads execute independently, competing for CPU time.

    • The exact output depends on the thread scheduler.

Sample Outputs:

  1. Execution 1:

     Main Thread
     Main Thread
     Main Thread
     Main Thread
     Main Thread
     Child Thread
     Child Thread
     Child Thread
     Main Thread
     ...
    
  2. Execution 2:

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

Behind the Scenes

  • Main Thread:

    • Automatically created by the JVM to execute the main() method.
  • Child Thread:

    • Created explicitly using the Thread class or Runnable interface.

    • Execution begins when the start() method is invoked.


Agenda of Multithreading

The primary purpose of multithreading is to use CPU resources efficiently by running multiple independent tasks concurrently. This improves the overall performance of the program.

  • Thread Scheduler:

    • A software component responsible for managing threads' CPU time.

    • Uses confidential algorithms provided by JDK vendors.


Summary of Key Concepts

  1. Defining a Thread:

    • Write a class that extends Thread.

    • Override the run() method to define the thread's task.

  2. Starting a Thread:

    • Instantiate the thread class.

    • Call the start() method to begin execution.

  3. Thread Independence:

    • Threads run independently, and their execution order is determined by the thread scheduler.
  4. Output:

    • Due to context switching, outputs may vary across executions but demonstrate concurrent task execution.

By using threads effectively, we can achieve improved performance and responsiveness in Java applications.

Thread Scheduler in Java

Overview of Thread Scheduler

  • The Thread Scheduler is a component of the JVM (Java Virtual Machine) responsible for deciding which thread will execute first when multiple threads are waiting to execute.

  • The exact order of thread execution cannot be predicted; only possible outcomes can be anticipated.

  • In multi-threading, the priority is on execution of threads rather than their execution order, ensuring improved performance.

  • The Thread Scheduler handles the execution of threads based on internal scheduling policies, which may vary across JVM implementations.


Thread Scheduling with start() Method

Creating a New Thread

When the start() method of a thread object is invoked, it:

  1. Registers the thread with the Thread Scheduler:

    • The scheduler keeps track of the thread's existence.
  2. Handles low-level mandatory activities:

    • This includes memory-level operations essential for thread initialization.
  3. Invokes the run() method:

    • The logic defined in the run() method of the thread class or its subclass is executed.

Code Demonstration

class Thread {
    public void start() {
        // Register thread with the Thread Scheduler
        // Perform low-level memory and setup activities
        // Invoke the run() method
    }
}

Key Points About start() Method

  1. The start() method is part of the Thread class and not the user-defined thread class (e.g., MyThread).

  2. If invoked, the start() method of the Thread class performs all necessary logic and ensures that the thread is ready to execute.

  3. After thread creation and registration, the start() method internally calls the overridden run() method in the subclass.

Code Example

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

public class MultithreadingExample {
    public static void main(String[] args) {
        MyThread t = new MyThread();
        t.start(); // Creates and registers a new thread, then executes the run() method
    }
}

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

Aspectt.start()t.run()
Thread CreationCreates a new thread.Does not create a new thread.
ExecutionExecutes run() on a new thread.Executes run() on the main thread.
Multi-threading BehaviorImplements multi-threading.Single-threaded execution.
Context SwitchingContext switching may occur.No context switching.

Code Example

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

public class MultithreadingExample {
    public static void main(String[] args) {
        MyThread t = new MyThread();
        t.run(); // Directly calls run() without creating a new thread

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

Output of the Above Code

Child Thread
Child Thread
...
Child Thread
Main Thread
Main Thread
...
Main Thread
  • The execution is sequential as there is no multi-threading.

  • The t.run() method behaves like a regular method call, executed by the main thread.


Why start() Method is the Heart of Multi-threading

  1. Registers the Thread:

    • The start() method ensures that the thread is registered with the scheduler, enabling its management and execution.
  2. Handles Low-Level Activities:

    • It performs all necessary preparatory tasks, such as memory allocation and thread initialization.
  3. Invokes the run() Method:

    • Without the start() method, the run() method cannot be executed in a multi-threaded manner.

If the start() method is not executed:

  • The thread will not be registered with the Thread Scheduler.

  • Multi-threading will not occur, leading to inefficient CPU utilization.

Importance of start() Method

The start() method bridges the gap between thread creation and execution, making it an essential component of the multi-threading process in Java.


Case Study: Improper Use of run() Method

If the run() method is called directly instead of using start(), the thread will behave like a regular method in the main thread.

Code

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

public class MultithreadingExample {
    public static void main(String[] args) {
        MyThread t = new MyThread();
        t.run(); // Improper use: behaves like a normal method

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

Output

Child Thread
Child Thread
...
Child Thread
Main Thread
Main Thread
...
Main Thread
  • No multi-threading occurs.

  • Both run() and the main thread logic execute sequentially.


Conclusion

  • The Thread Scheduler is a core component that manages thread execution in a multi-threaded environment.

  • Using the start() method is essential for achieving true multi-threading, as it registers threads, prepares them for execution, and invokes the run() method.

  • Directly calling the run() method bypasses these processes, leading to single-threaded execution and defeating the purpose of multi-threading.

Summary

Java Multithreading simplifies multitasking by leveraging the Java API, enabling developers to focus only on the application's specific logic. The OS ensures efficient process switching, while the Thread Scheduler optimizes thread execution, ensuring CPU time is used effectively.

Multithreading thus aligns with the goal of maximizing CPU performance and improving overall system efficiency.

Thread-based Multi-tasking allows tasks to run simultaneously, reducing response time and improving application performance.

  • Single-threading involves sequential task execution, leading to increased waiting time.

  • Multi-threading assigns tasks to independent threads, enhancing efficiency and utilizing CPU time effectively.

  • With 90% of the work handled by Java APIs, multi-threading is both powerful and developer-friendly, especially for applications like gaming, multimedia, and video conferencing.



By the end of this chapter, you'll master multithreading, a critical skill for modern Java developers. Stay tuned for an in-depth exploration of each topic!