In modern software development, performance and efficiency are key factors. Multithreading is one of the powerful tools in Java that helps developers achieve concurrency, allowing multiple tasks to execute simultaneously. In this blog, we’ll explore the basics of multithreading, its benefits, and how to implement it in Java.
What is Multithreading?
Multithreading is the ability of a CPU (or a single process) to execute multiple threads concurrently.
Often, people confuse between process and threads. Let's see the difference between them.
Thread Vs Process:
Process | Thread |
---|---|
An independent program with its own memory and resources. | A smaller unit of execution within a process. |
Has its own memory space (separate heap and stack). | Shares the process's heap but has its own stack. |
Heavyweight; more resources are needed for creation and management. | Lightweight; requires fewer resources to create and manage. |
A crashing process doesn’t affect others. | A crashing thread can impact the entire process. |
Processes are isolated from one another. | Threads within the same process are not isolated. |
Can run on different CPUs/cores independently. | Can run on multiple cores but within the same process context. |
Each process has its own code, data, and operating system resources. | Threads share code, data, and OS resources of the parent process. |
Running two Java applications. | Running multiple threads in a single Java application. |
Why Use Multithreading?
-
Better CPU Utilization
: Keeps the CPU busy by running threads in parallel. -
Improved Application Responsiveness
: Especially useful in GUI applications where long-running tasks don’t block the user interface. -
Faster Execution
: Speeds up tasks by dividing them into smaller threads. -
Efficient Resource Sharing
: Threads within the same process share memory and resources, making them less resource-intensive than processes.
How Multithreading Works in Java
Java provides built-in support for multithreading through the java.lang.Thread
class and the java.util.concurrent
package.
Creating Threads in Java
There are two ways to create threads in Java:
- Extending the Thread Class
class MyThread extends Thread {
public void run() {
System.out.println("Thread is running");
}
}
public class Main {
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start(); // Starts the thread and calls the `run` method
}
}
- Implementing the Runnable Interface
class MyRunnable implements Runnable {
public void run() {
System.out.println("Thread is running");
}
}
public class Main {
public static void main(String[] args) {
Thread thread = new Thread(new MyRunnable());
thread.start(); // Starts the thread and calls the `run` method
}
}
Key Thread Methods
start()
: Starts the thread and invokes the run() method.
run()
: Contains the code to be executed by the thread.
sleep(long millis)
: Puts the thread to sleep for the specified time.
join()
: Waits for the thread to finish execution.
isAlive()
: Checks if the thread is alive.
Thread States
A thread in Java can be in one of the following states:
New
: The thread is created but not started.
Runnable
: The thread is ready to run but waiting for CPU time.
Running
: The thread is currently executing.
Blocked/Waiting
: The thread is waiting for a resource or another thread.
Terminated
: The thread has finished execution.
Thread Lifecycle
The Thread Lifecycle describes the different stages a thread goes through from creation to termination.
1. New (Created)
- When a thread is first created but hasn't started yet, it is in the
New
state. No CPU time is allocated to the thread at this stage.
How It Happens?
This state is reached when a Thread object is instantiated using the Thread class or any subclass of it.
Thread thread = new Thread(); // Thread is in New state
2. Runnable
- Here, a thread is ready for execution and is waiting for CPU time to be scheduled by the thread scheduler.
How It Happens?
A thread enters into runnable state after invoking the start() method. It is then placed in the system's ready queue to get CPU time.
Transition: From Runnable, the thread can either:
Move to Running when the CPU allocates it time.
Move to Blocked or Waiting if there is a resource dependency.
Thread thread = new Thread();
thread.start(); // Thread is now in Runnable state
3. Running
- A thread enters the Running state when the thread scheduler allocates CPU time for its execution.
How It Happens?
The thread is picked from the Runnable state by the operating system's thread scheduler, which grants it CPU time to execute the run() method.
Transition: The thread can transition to other states like Blocked, Waiting, or Terminated based on conditions like resource availability or exceptions.
// Inside the run method
public void run() {
// Thread executes this code when running
}
4. Blocked
- A thread enters the Blocked state when it is waiting to acquire a lock or resource.
How It Happens?
This happens when a thread tries to access a resource or critical section that is already locked by another thread.
Transition: Once the thread acquires the necessary lock or resource, it moves back to the Runnable state.
synchronized (lockObject) {
// Blocked until the lockObject is available
}
5. Waiting
- A thread enters the Waiting state when it is waiting indefinitely for another thread to perform a particular action.
How It Happens?
A thread can enter the Waiting state when it calls methods like wait(), join(), or sleep().
Transition: The thread will move to Runnable after being notified by another thread
synchronized (lockObject) {
lockObject.wait(); // Thread is now in Waiting state
}
6. Timed Waiting
- A thread enters the Timed Waiting state when it is waiting for a specified period of time.
How It Happens?
Methods like sleep(long millis), join(long millis), and wait(long millis) cause a thread to enter the Timed Waiting state.
Transition: After the time expires or the condition is met, the thread moves back to Runnable.
Thread.sleep(1000); // Thread is in Timed Waiting state for 1 second
7. Terminated (Dead)
- The thread enters the Terminated state when its execution completes or when it is forcibly stopped.
How It Happens?
This state is reached after the run() method finishes execution or when an uncaught exception terminates the thread.
Transition: There’s no transition out of the Terminated state.
Summary
State | Description | Transition |
---|---|---|
New | Thread is created but not yet started. | Moves to Runnable state after invoking start() method. |
Runnable | Thread is ready to run and waiting for CPU scheduling. | Moves to Running state when CPU allocates time. |
Running | Thread is executing its task on the CPU. | Moves to Blocked, Waiting, or Terminated based on conditions. |
Blocked | Thread is waiting for a lock or resource to become available. | Moves back to Runnable when the lock/resource is available. |
Waiting | Thread is waiting indefinitely for another thread to perform a specific action. | Moves to Runnable after being notified. |
Timed Waiting | Thread is waiting for a specified period. | Moves back to Runnable after the specified time. |
Terminated | Thread has completed its execution or was terminated. | Cannot transition out of Terminated. |
Conclusion
Multithreading in Java is a powerful tool for building efficient and responsive applications. By leveraging Java's threading capabilities, developers can write programs that take full advantage of modern multi-core processors. With proper synchronization and best practices, multithreading can significantly enhance application performance.
In the following post, we’ll delve into the different types of locks, their characteristics, and scenarios where each type is best suited.
If you find this post helpful, don't forget to like and share it with your network to spread the knowledge. Follow this blog for more in-depth content on software development, distributed systems, and best practices in system design.
Thank you!!
Top comments (0)