Skip to content

Files

Latest commit

 

History

History

Day_59_Threading

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Mastering Multithreading in Java: A Comprehensive Guide

Introduction

Multithreading is a fundamental concept in Java programming that enables you to execute multiple tasks concurrently, improving the efficiency and responsiveness of your applications. In this comprehensive guide, we will explore various aspects of multithreading in Java, providing you with a deeper understanding of this essential topic. We'll also include code examples and their corresponding outputs to illustrate these concepts.

  1. Multithreading

Multithreading is the concurrent execution of multiple threads within a single process. Threads are lightweight processes that allow a program to perform multiple tasks in parallel.

public class MultithreadingExample {
    public static void main(String[] args) {
        // Create and start two threads
        Thread thread1 = new Thread(() -> {
            for (int i = 1; i <= 5; i++) {
                System.out.println("Thread 1 - Count: " + i);
            }
        });
        
        Thread thread2 = new Thread(() -> {
            for (int i = 1; i <= 5; i++) {
                System.out.println("Thread 2 - Count: " + i);
            }
        });
        
        thread1.start();
        thread2.start();
    }
}

Output:

Thread 1 - Count: 1
Thread 2 - Count: 1
Thread 1 - Count: 2
Thread 2 - Count: 2
Thread 1 - Count: 3
Thread 2 - Count: 3
Thread 1 - Count: 4
Thread 2 - Count: 4
Thread 1 - Count: 5
Thread 2 - Count: 5

In this example, we create two threads and execute them concurrently.

  1. Life Cycle of a Thread

Each thread in Java goes through various states in its life cycle, including New, Runnable, Blocked, Waiting, Timed Waiting, and Terminated.

public class ThreadLifecycleExample {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            System.out.println("Thread State: " + Thread.currentThread().getState());
        });
        
        System.out.println("Thread State: " + thread.getState()); // New state
        thread.start();
        
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        System.out.println("Thread State: " + thread.getState()); // Terminated state
    }
}

Output:

Thread State: NEW
Thread State: TERMINATED

Here, we demonstrate the life cycle of a thread from the New state to the Terminated state.

  1. How to Create a Thread

In Java, you can create threads by extending the Thread class or implementing the Runnable interface. We'll explore both methods and discuss when to use each approach.

public class ThreadCreationExample {
    public static void main(String[] args) {
        // Extending Thread class
        Thread thread1 = new MyThread();
        
        // Implementing Runnable interface
        Runnable myRunnable = new MyRunnable();
        Thread thread2 = new Thread(myRunnable);
        
        thread1.start();
        thread2.start();
    }
    
    static class MyThread extends Thread {
        public void run() {
            System.out.println("Thread created by extending Thread class.");
        }
    }
    
    static class MyRunnable implements Runnable {
        public void run() {
            System.out.println("Thread created by implementing Runnable interface.");
        }
    }
}

Output:

Thread created by extending Thread class.
Thread created by implementing Runnable interface.

In this example, we show how to create threads using both the Thread class and the Runnable interface.

  1. Thread Scheduler

The thread scheduler is responsible for managing the execution of threads. It determines the order in which threads run.

public class ThreadSchedulerExample {
    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            for (int i = 1; i <= 5; i++) {
                System.out.println("Thread 1 - Count: " + i);
            }
        });
        
        Thread thread2 = new Thread(() -> {
            for (int i = 1; i <= 5; i++) {
                System.out.println("Thread 2 - Count: " + i);
            }
        });
        
        thread1.setPriority(Thread.MIN_PRIORITY); // Set thread1's priority to the minimum
        thread2.setPriority(Thread.MAX_PRIORITY); // Set thread2's priority to the maximum
        
        thread1.start();
        thread2.start();
    }
}

Output:

Thread 1 - Count: 1
Thread 2 - Count: 1
Thread 1 - Count: 2
Thread 2 - Count: 2
Thread 1 - Count: 3
Thread 2 - Count: 3
Thread 1 - Count: 4
Thread 2 - Count: 4
Thread 1 - Count: 5
Thread 2 - Count: 5

Here, we set the priorities of two threads and observe the thread scheduler's behavior.

  1. Sleeping a Thread

You can make a thread sleep for a specified amount of time using the Thread.sleep() method. We'll demonstrate when and why you might want to use this feature.

public class ThreadSleepExample {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            for (int i = 1; i <= 5; i++) {
                System.out.println("Count: " + i);
                try {
                    Thread.sleep(1000); // Sleep for 1 second
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        
        thread.start();
    }
}

Output:

Count: 1
Count: 2
Count: 3
Count: 4
Count: 5

In this example, we make a thread sleep for one second between each count.

  1. Start a Thread Twice

Attempting to start a thread twice will result in an "IllegalThreadStateException." We'll explain why this happens and how to handle it.

public class StartThreadTwiceExample {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            System.out.println("Thread is running.");
        });
        
        thread.start();
        thread.start(); // Attempt to start the same thread again
    }
}

Output:

Thread is running.
Exception in thread "main" java.lang.IllegalThreadStateException
    at java.base/java.lang.Thread.start(Thread.java:801)
    at StartThreadTwiceExample.main(StartThreadTwiceExample.java:10)

In this example, we demonstrate the error that occurs when trying to start a thread twice.

  1. Calling run() Method

While it is possible to call the run() method directly, doing so will not create a new thread. We'll clarify the distinction between calling run() and start().

public class CallRunMethodExample {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            System.out.println("Thread is running.");
        });
        
       

 thread.run(); // Call run() directly
    }
}

Output:

Thread is running.

In this example, calling the run() method directly executes the code within the current thread.

  1. Joining a Thread

The join() method allows one thread to wait for the completion of another thread. We'll show you how to use it to synchronize thread execution.

public class JoinThreadExample {
    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            for (int i = 1; i <= 5; i++) {
                System.out.println("Thread 1 - Count: " + i);
            }
        });
        
        Thread thread2 = new Thread(() -> {
            for (int i = 1; i <= 5; i++) {
                System.out.println("Thread 2 - Count: " + i);
            }
        });
        
        thread1.start();
        try {
            thread1.join(); // Wait for thread1 to finish
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        thread2.start();
    }
}

Output:

Thread 1 - Count: 1
Thread 1 - Count: 2
Thread 1 - Count: 3
Thread 1 - Count: 4
Thread 1 - Count: 5
Thread 2 - Count: 1
Thread 2 - Count: 2
Thread 2 - Count: 3
Thread 2 - Count: 4
Thread 2 - Count: 5

Here, we use the join() method to make the second thread wait for the first thread to complete.

  1. Naming a Thread

Assigning names to threads is useful for debugging and identifying them within your application. We'll demonstrate how to name your threads.

public class ThreadNamingExample {
    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            System.out.println("Thread 1 - Name: " + Thread.currentThread().getName());
        });
        
        Thread thread2 = new Thread(() -> {
            System.out.println("Thread 2 - Name: " + Thread.currentThread().getName());
        });
        
        thread1.setName("WorkerThread1");
        thread2.setName("WorkerThread2");
        
        thread1.start();
        thread2.start();
    }
}

Output:

Thread 1 - Name: WorkerThread1
Thread 2 - Name: WorkerThread2

In this example, we set custom names for the threads to identify them easily.

  1. Thread Priority

Thread priority is a way to influence the order in which threads are scheduled. We'll explain the priority levels and their implications.

public class ThreadPriorityExample {
    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            for (int i = 1; i <= 5; i++) {
                System.out.println("Thread 1 - Count: " + i);
            }
        });
        
        Thread thread2 = new Thread(() -> {
            for (int i = 1; i <= 5; i++) {
                System.out.println("Thread 2 - Count: " + i);
            }
        });
        
        thread1.setPriority(Thread.MIN_PRIORITY); // Set thread1's priority to the minimum
        thread2.setPriority(Thread.MAX_PRIORITY); // Set thread2's priority to the maximum
        
        thread1.start();
        thread2.start();
    }
}

Output:

Thread 2 - Count: 1
Thread 2 - Count: 2
Thread 2 - Count: 3
Thread 2 - Count: 4
Thread 2 - Count: 5
Thread 1 - Count: 1
Thread 1 - Count: 2
Thread 1 - Count: 3
Thread 1 - Count: 4
Thread 1 - Count: 5

Here, we set the priorities of two threads, and the one with higher priority (MAX_PRIORITY) runs first.

  1. Daemon Thread

A daemon thread is a background thread that runs without blocking the application's termination. We'll explore when and how to use daemon threads.

public class DaemonThreadExample {
    public static void main(String[] args) {
        Thread daemonThread = new Thread(() -> {
            while (true) {
                System.out.println("Daemon Thread is running.");
            }
        });
        
        daemonThread.setDaemon(true);
        daemonThread.start();
        
        // Main thread sleeps for 2 seconds
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        System.out.println("Main thread exits, and the daemon thread is stopped.");
    }
}

Output:

Daemon Thread is running.
Daemon Thread is running.
Main thread exits, and the daemon thread is stopped.

In this example, the daemon thread continues running even after the main thread exits.

  1. Thread Pool

Thread pools are a mechanism for managing and reusing a fixed number of threads. We'll discuss how to create and utilize thread pools effectively.

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadPoolExample {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(3);
        
        for (int i = 1; i <= 5; i++) {
            Runnable task = new WorkerThread(i);
            executorService.execute(task);
        }
        
        executorService.shutdown();
    }
    
    static class WorkerThread implements Runnable {
        private final int taskId;
        
        WorkerThread(int taskId) {
            this.taskId = taskId;
        }
        
        public void run() {
            System.out.println("Task " + taskId + " is being executed by " + Thread.currentThread().getName());
        }
    }
}

Output:

Task 1 is being executed by pool-1-thread-1
Task 2 is being executed by pool-1-thread-2
Task 3 is being executed by pool-1-thread-3
Task 4 is being executed by pool-1-thread-1
Task 5 is being executed by pool-1-thread-2

In this example, we create a thread pool and execute multiple tasks concurrently.

  1. Thread Group

Thread groups provide a way to manage and organize threads. We'll delve into the concept of thread groups and their uses.

public class ThreadGroupExample {
    public static void main(String[] args) {
        ThreadGroup group = new ThreadGroup("MyThreadGroup");
        
        Thread thread1 = new Thread(group, () -> {
            System.out.println("Thread 1 is part of the MyThreadGroup.");
        });
        
        Thread thread2 = new Thread(group, () -> {
            System.out.println("Thread 2 is part of the MyThreadGroup.");
        });
        
        thread1.start();
        thread2.start();
    }
}

Output:

Thread 2 is part of the MyThreadGroup.
Thread 1 is part of the MyThreadGroup.

Here, we create a thread group and add threads to it.

  1. ShutdownHook

The shutdown hook allows you to perform cleanup operations when the JVM is about to terminate. We

'll demonstrate how to register a shutdown hook for your application.

public class ShutdownHookExample {
    public static void main(String[] args) {
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            System.out.println("Shutdown hook executed. Performing cleanup operations.");
        }));
        
        System.out.println("Main application is running.");
        
        // Simulate a long-running application
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        System.out.println("Main application is exiting.");
    }
}

Output:

Main application is running.
Main application is exiting.
Shutdown hook executed. Performing cleanup operations.

In this example, the shutdown hook is executed when the main application is about to exit.

  1. Performing Multiple Tasks

Multithreading allows you to perform multiple tasks concurrently. We'll provide examples of common scenarios where multithreading can improve application performance and responsiveness.

public class MultitaskingExample {
    public static void main(String[] args) {
        // Create threads for different tasks
        Thread task1 = new Thread(() -> {
            System.out.println("Task 1 is processing data.");
        });
        
        Thread task2 = new Thread(() -> {
            System.out.println("Task 2 is downloading a file.");
        });
        
        Thread task3 = new Thread(() -> {
            System.out.println("Task 3 is rendering graphics.");
        });
        
        // Start all tasks concurrently
        task1.start();
        task2.start();
        task3.start();
    }
}

Output:

Task 1 is processing data.
Task 2 is downloading a file.
Task 3 is rendering graphics.

In this example, we demonstrate how multithreading allows you to perform multiple tasks concurrently, enhancing the overall application experience.

Conclusion

Multithreading is a powerful and essential concept in Java that enables you to make your applications more efficient and responsive. Understanding the various aspects of multithreading, from the life cycle of a thread to managing thread pools and using daemon threads, is crucial for writing high-quality Java applications. By mastering these concepts and techniques, you can take full advantage of multithreading and create robust, parallelized applications that can handle multiple tasks effectively.