Multithreading
Understand the concepts of multithreading and concurrency in Java. Learn how to create and manage threads to perform tasks concurrently.
Creating and Managing Threads in Java
Introduction to Threads
Threads are a fundamental concept in concurrent programming. In Java, threads allow you to execute multiple parts of a program concurrently. This is especially useful for improving the performance of applications that perform multiple independent tasks. A thread is a lightweight subprocess within a process. A single process can contain multiple threads.
Creating Threads in Java
Java provides two primary ways to create threads:
1. Extending the Thread
Class
You can create a thread by defining a new class that extends the Thread
class and overriding the run()
method. The run()
method contains the code that will be executed by the thread.
Example:
class MyThread extends Thread {
@Override
public void run() {
System.out.println("Thread running: " + Thread.currentThread().getName());
for (int i = 0; i < 5; i++) {
System.out.println("MyThread: " + i);
try {
Thread.sleep(500); // Simulate some work
} catch (InterruptedException e) {
System.out.println("Thread interrupted!");
}
}
}
}
public class ThreadExample {
public static void main(String[] args) {
MyThread thread1 = new MyThread();
thread1.start(); // Start the thread
MyThread thread2 = new MyThread();
thread2.start(); // Start another thread
}
}
Explanation:
MyThread
extends theThread
class.- The
run()
method is overridden to define the thread's logic. thread1.start()
creates a new thread and begins its execution, calling therun()
method. The same applies forthread2
.Thread.sleep()
pauses the thread's execution for a specified amount of time (in milliseconds). This can be useful to simulate a time-consuming operation.Thread.currentThread().getName()
gets the name of currently running thread.
2. Implementing the Runnable
Interface
You can also create a thread by implementing the Runnable
interface. This approach is generally preferred because it allows your class to extend another class if needed (Java does not allow multiple inheritance of classes). The Runnable
interface requires you to implement the run()
method. To execute the Runnable
task, you need to create a Thread
object, passing your Runnable
implementation as the argument to the Thread
constructor, and then call start()
on the Thread
object.
Example:
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("Thread running: " + Thread.currentThread().getName());
for (int i = 0; i < 5; i++) {
System.out.println("MyRunnable: " + i);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
System.out.println("Thread interrupted!");
}
}
}
}
public class RunnableExample {
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
Thread thread1 = new Thread(myRunnable);
thread1.start();
Thread thread2 = new Thread(new MyRunnable()); // Another way to create a thread
thread2.start();
}
}
Explanation:
MyRunnable
implements theRunnable
interface.- The
run()
method is overridden to define the thread's logic. - A
Thread
object is created, passing theMyRunnable
instance to its constructor. thread1.start()
starts the thread, executing therun()
method of theMyRunnable
instance.- The second thread is create by directly instantiating MyRunnable in the
Thread
constructor.
When to use which approach:
- If your class needs to inherit behavior from another class, implement the
Runnable
interface. - If your class doesn't need to inherit from another class and you want a simpler approach, extend the
Thread
class. - Implementing
Runnable
is generally preferred because it promotes better code organization and avoids the limitations of single inheritance in Java.
Thread Lifecycle
A thread's lifecycle consists of several states:
- New: The thread has been created but has not yet started.
- Runnable: The thread is ready to run and is waiting for its turn to be executed by the CPU. This state includes both "ready" and "running".
- Blocked/Waiting: The thread is waiting for a resource or event (e.g., I/O completion, lock acquisition, notification from another thread).
- Timed Waiting: The thread is waiting for a resource or event for a specified amount of time (e.g., calling
Thread.sleep()
,wait(long timeout)
). - Terminated (Dead): The thread has completed its execution or has been terminated due to an exception.
You can influence a thread's state using methods like:
start()
: Moves the thread from the "New" state to the "Runnable" state.sleep(long millis)
: Moves the thread to the "Timed Waiting" state for the specified duration.join()
: Makes the current thread wait until the thread on whichjoin()
is called terminates.interrupt()
: Interrupts a thread that is blocked or waiting. This will usually cause anInterruptedException
to be thrown in the target thread, if the thread is blocked on a method that supports interruption.wait()
: Causes the current thread to wait until another thread invokes thenotify()
ornotifyAll()
methods for this object.notify()
: Wakes up a single thread that is waiting on this object's monitor.notifyAll()
: Wakes up all threads that are waiting on this object's monitor.
Managing Thread States
Thread.sleep()
The Thread.sleep(long millis)
method pauses the execution of the current thread for a specified duration in milliseconds. This is useful for introducing delays or simulating time-consuming operations.
Example:
public class SleepExample {
public static void main(String[] args) {
System.out.println("Starting...");
try {
Thread.sleep(2000); // Pause for 2 seconds
} catch (InterruptedException e) {
System.out.println("Thread interrupted!");
}
System.out.println("...Finished");
}
}
Thread.join()
The Thread.join()
method allows one thread to wait for the completion of another thread. When a thread calls join()
on another thread, the calling thread is blocked until the target thread finishes its execution.
Example:
public class JoinExample {
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
try {
Thread.sleep(3000); // Simulate some work
System.out.println("Thread 1 finished.");
} catch (InterruptedException e) {
System.out.println("Thread 1 interrupted!");
}
});
Thread t2 = new Thread(() -> {
System.out.println("Thread 2 starting...");
try {
t1.join(); // Wait for thread 1 to finish
System.out.println("Thread 1 is done, Thread 2 continuing...");
} catch (InterruptedException e) {
System.out.println("Thread 2 interrupted!");
}
});
t1.start();
t2.start();
}
}
Thread.interrupt()
The Thread.interrupt()
method interrupts a thread. If the target thread is blocked (e.g., waiting on I/O, sleeping, or waiting on a lock), an InterruptedException
will be thrown. If the thread is not blocked, the interrupt flag is set, which the thread can check using Thread.interrupted()
or Thread.isInterrupted()
.
Example:
public class InterruptExample {
public static void main(String[] args) {
Thread t = new Thread(() -> {
try {
System.out.println("Thread starting...");
Thread.sleep(5000); // Long sleep
System.out.println("Thread finished normally.");
} catch (InterruptedException e) {
System.out.println("Thread interrupted!");
}
});
t.start();
try {
Thread.sleep(1000); // Wait for a short time
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Interrupting thread...");
t.interrupt();
}
}
Conclusion
Understanding how to create and manage threads is essential for building concurrent applications in Java. By choosing the appropriate approach (extending Thread
or implementing Runnable
) and utilizing methods to manage thread states, you can effectively improve application performance and responsiveness. Proper thread management is crucial to avoid issues like race conditions, deadlocks, and resource starvation.