Multi-Threading
In Java, you can create a thread in multiple ways. Below are the most common approaches:
1. Extending the Thread
class
You can create a new thread by extending the Thread
class and overriding the run()
method.
class MyThread extends Thread {
@Override public void run() {
System.out.println("Thread is running: " + Thread.currentThread().getName());
}
public static void main(String[] args) {
MyThread thread = new MyThread(); // Creating a thread
thread.start(); // Starting the thread
}
}
Explanation:
start()
initiates a new thread and calls therun()
method internally.- Avoid calling
run()
directly; it will execute in the main thread instead of a new one.
2. Implementing the Runnable
interface
This approach is more flexible because it allows your class to extend other classes, as Java supports single inheritance.
class MyRunnable implements Runnable {
@Override public void run() {
System.out.println("Thread is running: " + Thread.currentThread().getName());
}
public static void main(String[] args) {
Thread thread = new Thread(new MyRunnable()); // Passing Runnable to Thread
thread.start(); // Starting the thread
}
}
Explanation:
- You implement the
Runnable
interface and pass it to theThread
constructor. Runnable
is preferred when your class needs to inherit from another class.
3. Using Lambda Expressions (Java 8 and above)
This method simplifies the creation of threads using functional programming.
public class LambdaThread {
public static void main(String[] args) {
Thread thread = new Thread(() - > {
System.out.println("Thread is running: " + Thread.currentThread().getName());
});
thread.start(); // Starting the thread
}
}
Explanation:
- The
Runnable
interface is a functional interface, so you can use a lambda expression to provide the implementation of therun()
method.
4. Using ExecutorService
(Thread Pool)
If you need to manage a pool of threads efficiently, use ExecutorService
.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPoolExample {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(2);
executor.submit(() - > {
System.out.println("Thread 1 is running: " + Thread.currentThread().getName());
});
executor.submit(() - > {
System.out.println("Thread 2 is running: " + Thread.currentThread().getName());
});
executor.shutdown(); // Shutdown the executor
}
}
Explanation:
ExecutorService
provides better management of threads and should be preferred in complex applications.
Summary
Approach | When to Use |
---|---|
Extending Thread | When you don’t need to extend another class. |
Implementing Runnable | When you want more flexibility and reusability. |
Using Lambda Expression | When working with Java 8+ for concise code. |
Using ExecutorService | When managing a pool of threads efficiently. |
Each method has its advantages. Use ExecutorService
for large-scale or production applications, while extending Thread
or using Runnable
works well for simpler cases.
The Preferred Way To Create Threads
The preferred way to create threads in Java is by using the Runnable
interface or an ExecutorService (thread pool). Let’s discuss the reasons and the context in which each approach is recommended.
Why Runnable
is Preferred Over Extending Thread
1. Separation of Task and Thread Logic
- By implementing the
Runnable
interface, the task to be executed is kept separate from the actual thread logic. This provides better design flexibility.
2. Single Inheritance in Java
- Since Java allows a class to extend only one superclass, if you extend
Thread
, you cannot extend any other class. ImplementingRunnable
allows the class to inherit from other useful classes if needed.
3. Reusability and Code Maintenance
- Using
Runnable
promotes code reusability. You can pass the sameRunnable
instance to multiple threads if needed, which makes it easier to maintain.
Why ExecutorService / Thread Pools are Better for Real Applications
For large-scale or production applications, ExecutorService
is preferred.
Benefits of ExecutorService over Manual Thread Creation:
- Better Thread Management
- Creating threads manually can lead to performance issues when the number of threads becomes large. A thread pool manages threads efficiently by reusing existing threads instead of creating new ones for each task.
- Avoiding Resource Exhaustion
- With thread pools, you avoid uncontrolled thread creation that could exhaust CPU or memory. Thread pools restrict the maximum number of concurrent threads.
- Task Queueing
- Tasks submitted to an
ExecutorService
are queued and executed when threads are available, preventing congestion and improving system stability.
- Tasks submitted to an
- Graceful Shutdown and Exception Handling
ExecutorService
provides methods to gracefully shutdown the thread pool and handle exceptions more effectively, which is crucial in production systems.
When to Use Each Approach
Approach | When to Use |
---|---|
Implementing Runnable | When creating a few lightweight threads for short-lived tasks. |
Extending Thread | When a specialized thread needs its own state (but this is rare and discouraged). |
ExecutorService | When dealing with many tasks that need to be executed concurrently, especially in production. |
Conclusion: Preferred Approach
- For simple concurrent tasks: Use
Runnable
with theThread
class. - For scalable, production-level applications: Use
ExecutorService
or thread pools.
The use of Runnable
is encouraged over Thread
for flexibility and good design practices. For complex scenarios involving multiple tasks and threads, ExecutorService
or other concurrency frameworks (like ForkJoinPool
) are preferred.
Daemon thread
In Java, a daemon thread is a background thread that runs continuously to provide services or perform tasks for other threads. The JVM will exit when all non-daemon threads (also called user threads) have finished execution, even if daemon threads are still running. Essentially, the JVM doesn’t wait for daemon threads to finish before shutting down.
Key Characteristics of Daemon Threads:
- Background services: Typically used for background tasks like garbage collection, logging, or monitoring.
- No blocking on shutdown: JVM exits when only daemon threads remain, regardless of whether they are completed.
- Lower priority: Daemon threads have lower priority than user threads.
- Lifecycle depends on user threads: If no user threads are running, the daemon threads terminate automatically.
How to Create a Daemon Thread in Java
You can create a daemon thread by calling the setDaemon(true)
method on a thread before it starts.
public class DaemonThreadExample {
public static void main(String[] args) {
Thread daemonThread = new Thread(() - > {
while (true) {
try {
System.out.println("Daemon thread is running...");
Thread.sleep(1000);
// Sleep for 1 second
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
// Setting the thread as a daemon
daemonThread.setDaemon(true);
daemonThread.start();
System.out.println("Main thread is terminating...");
}
}
Output:
Main thread is terminating...
Daemon thread is running...
Daemon thread is running...
Here, the main thread will terminate quickly, and since the daemon thread is running in the background, the JVM will exit immediately without waiting for the daemon thread to complete its infinite loop.
Checking if a Thread is a Daemon
You can use the isDaemon()
method to check if a thread is a daemon.
System.out.println("Is daemon thread: " + daemonThread.isDaemon());
Real-world Examples of Daemon Threads
- Garbage Collector (GC) in the JVM runs as a daemon thread to reclaim memory.
- Timer tasks that perform periodic background tasks like cleanup or logging.
- Idle connection management in server applications.
Key Points to Remember
- A user thread can outlive a daemon thread if it takes longer to complete.
- Always set a thread to daemon mode before calling
start()
. Otherwise, it will throwIllegalThreadStateException
. - Be cautious when using daemon threads for critical tasks because they can terminate abruptly when the JVM exits.
If you try to call setDaemon(true)
after calling start()
on a thread, Java will throw an IllegalThreadStateException
. This is because the daemon status must be set before the thread starts. Once a thread is started, its properties, such as whether it is a daemon or not, cannot be changed.
public class InvalidDaemonThreadExample {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
System.out.println("Thread is running...");
});
thread.start(); // Start the thread
// Attempt to set it as a daemon after starting
try {
thread.setDaemon(true); // This will cause an exception
} catch (IllegalThreadStateException e) {
System.out.println("Exception: " + e);
}
}
}
Thread is running...
Exception: java.lang.IllegalThreadStateException
We can not set main thread as Daemon Thread. Reason is above
Example For Daemon Thread
A real-time example of a daemon thread could be a background task like auto-saving documents, logging, or cleaning up idle connections. Below is an example that simulates auto-saving a file every few seconds using a daemon thread.
Real-Time Example: Auto-Save Feature Using a Daemon Thread
Imagine an application where a user works on a document, and the daemon thread saves the content every 5 seconds in the background.
Code:
public class AutoSaveExample {
// Simulate the auto-save task in a daemon thread
static class AutoSaveTask implements Runnable {
@Override public void run() {
while (true) {
try {
System.out.println(“Auto-saving document…”);
Thread.sleep(5000); // Sleep for 5 seconds
} catch (InterruptedException e) {
System.out.println(“Auto-save interrupted.”);
}
}
}
}
public static void main(String[] args) {
Thread autoSaveThread = new Thread(new AutoSaveTask()); // Set the thread as daemon before starting it
autoSaveThread.setDaemon(true);
autoSaveThread.start(); // Simulate user working on the document
System.out.println("User is working on the document...");
try {
Thread.sleep(10000);
// Simulate 10 seconds of user work
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("User has finished working. Exiting the application...");
// The application will exit, and the daemon thread will terminate.
}
}
Output:
User is working on the document...
Auto-saving document...
Auto-saving document...
User has finished working.
Exiting the application...
Explanation:
- Main Thread (User Work):
- The main thread simulates the user working on a document for 10 seconds.
- Daemon Thread (Auto-Save):
- The daemon thread runs in the background, printing “Auto-saving document…” every 5 seconds.
- When the main thread finishes (after 10 seconds), the JVM exits immediately, terminating the daemon thread.
- Behavior of Daemon Thread:
- Even though the auto-save task is infinite, the JVM terminates it as soon as the main thread completes because it’s running as a daemon thread.
Why Use a Daemon Thread Here?
- Non-critical task: Auto-saving is important but not critical. If the user closes the application, the auto-save task doesn’t need to continue.
- Automatic termination: The JVM ensures that the daemon thread doesn’t block the shutdown, so the user can exit quickly.
This is a practical use case of a daemon thread, where the background task assists the primary thread but doesn’t prevent the application from exiting.
If you have only daemon threads running in your Java application and no active work in the main thread, the JVM will terminate immediately once the main thread finishes execution. Here’s why and how it behaves:
Key Concepts:
- Main Thread: When your program starts, the
main
method runs on the main thread. - Daemon Threads: These are background threads that the JVM doesn’t wait for when all non-daemon (user) threads finish or exit. Daemon threads are used for tasks like garbage collection or other background services.
What Happens If Only Daemon Threads Exist:
- JVM exits immediately after the main thread completes, even if daemon threads are still running.
- Since daemon threads don’t block the JVM from exiting, the application will shut down as soon as there is no non-daemon thread left, including the main thread.
Example to Illustrate:
public class DaemonExample {
public static void main(String[] args) {
Thread daemonThread = new Thread(() -> {
while (true) {
System.out.println("Daemon thread running...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
daemonThread.setDaemon(true); // Set the thread as daemon
daemonThread.start();
System.out.println("Main thread finished.");
// No further code in main thread, JVM will exit here
}
}
Output:
Main Thread Finished
The JVM exits right after printing “Main thread finished” because:
- The main thread finishes execution.
- Since only a daemon thread is running and the JVM doesn’t wait for daemon threads, the process ends, and the daemon thread is stopped without completing its infinite loop.
How to Keep the JVM Alive:
If you need the JVM to continue running, you must ensure that:
- A non-daemon (user) thread remains active, or
- Add some activity in the main thread (e.g.,
Thread.sleep()
orjoin()
).
Here’s an example with a user thread:
public class UserThreadExample {
public static void main(String[] args) {
Thread userThread = new Thread(() -> {
while (true) {
System.out.println("User thread running...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
userThread.start(); // Not a daemon thread
System.out.println("Main thread finished.");
}
}
Now the JVM will keep running because the user thread is not marked as a daemon.
Conclusion:
- If the main thread finishes and only daemon threads remain, the JVM shuts down immediately.
- To prevent termination, you need either user threads or some blocking activity in the main thread.
Thread Priority
In Java, thread priority determines the relative importance of a thread for the CPU’s scheduling. The JVM thread scheduler uses these priorities to decide when to switch between threads. However, it’s important to note that thread priority is only a hint to the operating system, and its effect can vary depending on the OS and JVM implementation.
How Thread Priority Works
- Each thread in Java is assigned a priority value between 1 (lowest) to 10 (highest).
- The default priority of a thread is 5.
- The Java scheduler will generally prefer higher-priority threads over lower-priority threads, but it doesn’t guarantee strict order—other factors like time-slicing and fairness also play a role.
Java Thread Priority Constants
Java provides three constants defined in the Thread
class:
public static final int MIN_PRIORITY = 1; // Minimum priority
public static final int NORM_PRIORITY = 5; // Default priority
public static final int MAX_PRIORITY = 10; // Maximum priority
Setting and Getting Thread Priority
You can use the following methods from the Thread
class:
- Set priority:
thread.setPriority(int priority);
Throws IllegalArgumentException if the priority is not in the range of 1 to 10.
Get priority:
int priority = thread.getPriority();
Example: Setting Thread Priorities
class MyThread extends Thread {
public void run() {
System.out.println(Thread.currentThread().getName() +
" with priority " + Thread.currentThread().getPriority());
}
}
public class ThreadPriorityExample {
public static void main(String[] args) {
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
MyThread t3 = new MyThread();
t1.setPriority(Thread.MIN_PRIORITY); // Priority 1
t2.setPriority(Thread.NORM_PRIORITY); // Priority 5
t3.setPriority(Thread.MAX_PRIORITY); // Priority 10
t1.start();
t2.start();
t3.start();
}
}
Output:
The output might differ each time due to the non-deterministic behavior of thread scheduling. However, you may observe that the higher-priority thread (t3) usually completes earlier.
How Priority Affects Thread Execution
- Higher-priority threads are given preference but are not guaranteed to run before others.
- Time-slicing: The JVM can split the CPU time among threads with equal priority.
- Some OSes ignore thread priority, so it may not have any significant effect on them.
Key Points to Remember
- Platform Dependency: Thread priority behavior depends on the OS and JVM. Some operating systems may ignore Java thread priorities.
- Fairness: Prioritizing threads does not mean starvation won’t occur. To avoid starvation, thread scheduling policies like fair scheduling are used.
- Use with Caution: Over-reliance on thread priorities can make your code difficult to maintain and platform-dependent.
Common Issues with Thread Priorities
- Starvation: If higher-priority threads keep executing, lower-priority threads may starve (never get CPU time).
- Platform Inconsistencies: On some platforms, all threads might be treated equally, regardless of priority.
- Multicore Systems: Thread priorities are less relevant on modern multicore processors, where threads run in parallel on different cores.
The deprecated stop()
, suspend()
, and resume()
methods in Java’s Thread
class
The deprecated stop()
, suspend()
, and resume()
methods in Java’s Thread
class were removed due to various issues, such as deadlocks, data inconsistency, and thread state corruption. Instead, safe and modern alternatives involve using flags, wait()
, notify()
, and Interrupts
to control thread behavior gracefully.
Below are examples that demonstrate starting, pausing, resuming, and stopping threads safely.
class ControlledThread implements Runnable {
private volatile boolean isPaused = false;
private volatile boolean isStopped = false;
@Override
public void run() {
System.out.println("Thread started.");
while (!isStopped) {
synchronized (this) {
// If the thread is paused, wait until notified to resume
while (isPaused) {
try {
System.out.println("Thread paused...");
wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // Handle interrupt properly
System.out.println("Thread interrupted during pause.");
}
}
}
// Perform some work
System.out.println("Thread running...");
try {
Thread.sleep(1000); // Simulate some work
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.out.println("Thread interrupted during sleep.");
}
}
System.out.println("Thread stopped.");
}
// Method to pause the thread
public void pause() {
isPaused = true;
}
// Method to resume the thread
public synchronized void resumeThread() {
isPaused = false;
notify(); // Notify the thread to resume work
}
// Method to stop the thread
public void stop() {
isStopped = true;
}
}
Driver Code to Demonstrate Usage
public class ThreadControlDemo {
public static void main(String[] args) throws InterruptedException {
ControlledThread controlledTask = new ControlledThread();
Thread thread = new Thread(controlledTask);
thread.start(); // Start the thread
Thread.sleep(3000); // Let the thread run for 3 seconds
controlledTask.pause(); // Pause the thread
Thread.sleep(2000); // Simulate pause duration
controlledTask.resumeThread(); // Resume the thread
Thread.sleep(3000); // Let the thread run again
controlledTask.stop(); // Stop the thread gracefully
thread.join(); // Wait for the thread to finish
System.out.println("Main thread exiting.");
}
}
Explanation
- Flags for Control:
- We use a
volatile boolean
flag (isPaused
) to control when the thread should pause. - Another
volatile boolean
flag (isStopped
) determines if the thread should stop.
- We use a
- Pausing the Thread:
- When
pause()
is called, theisPaused
flag is set totrue
. - Inside the
run()
method, the thread checks this flag and callswait()
if it’s paused.
- When
- Resuming the Thread:
- When
resumeThread()
is called,isPaused
is set tofalse
andnotify()
is invoked to wake up the thread.
- When
- Stopping the Thread:
- When
stop()
is called, theisStopped
flag is set totrue
, which causes the loop to terminate gracefully.
- When
- Handling Interruptions:
- The thread uses
Thread.currentThread().interrupt()
to handle interruptions properly, ensuring it can exit safely without locking resources.
- The thread uses
Sample Output
mathematica
Copy code
Thread started. Thread running... Thread running... Thread running... Thread paused... Thread running... Thread running... Thread running... Thread stopped. Main thread exiting.
Advantages of this Approach
- Graceful control: No risk of deadlocks or data inconsistencies.
- Safe pause/resume using
wait()
/notify()
. - Interrupt handling ensures the thread exits properly.
This approach ensures better thread management without the pitfalls of deprecated methods.
wait()
and sleep()
wait()
and sleep()
in Java both pause the execution of a thread, but they have different purposes, behavior, and use cases. Here’s a detailed comparison:
Comparison: wait()
vs sleep()
Feature | wait() | sleep() |
---|---|---|
Class | Defined in java.lang.Object | Defined in java.lang.Thread |
Lock/Monitor Dependency | Requires a synchronized block/method | No lock or monitor required |
Releases Monitor/Lock | Yes, releases the monitor | No, retains the lock if held |
Resumption | Must be resumed using notify() or notifyAll() | Resumes automatically after timeout |
Use Case | Inter-thread communication (e.g., producer-consumer) | Delays thread execution temporarily |
Interrupt Handling | Throws InterruptedException | Throws InterruptedException |
Static Method | No (called on objects) | Yes (Thread.sleep() ) |
Blocking State | Blocks the current thread until notified or timed out | Blocks for a fixed amount of time |
Impact on CPU Scheduling | Pauses thread but allows others to execute | Pauses thread but others continue execution |
Detailed Behavior
1. wait()
– Inter-Thread Communication Tool
- Purpose: Used when one thread needs to wait for a condition to be met, typically in producer-consumer scenarios.
- How it works:
- A thread that calls
wait()
enters the waiting state and releases the monitor (lock). - It will only resume when another thread calls
notify()
ornotifyAll()
on the same monitor object.
- A thread that calls
public class WaitExample {
private static final Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
Thread waitingThread = new Thread(() -> {
synchronized (lock) {
try {
System.out.println("Thread waiting...");
lock.wait(); // Wait until notified
System.out.println("Thread resumed.");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.out.println("Thread interrupted.");
}
}
});
waitingThread.start();
Thread.sleep(2000); // Main thread sleeps to simulate some work
synchronized (lock) {
System.out.println("Main thread notifying...");
lock.notify(); // Notify the waiting thread
}
}
}
Output :
Thread waiting...
Main thread notifying...
Thread resumed.
2. sleep()
– Pausing Execution Temporarily
- Purpose: Used when a thread needs to be paused for a specific amount of time (e.g., rate limiting or periodic tasks).
- How it works:
- The thread that calls
Thread.sleep()
pauses for the specified time. - It does not release any lock during the sleep period.
- The thread that calls
public class SleepExample {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
System.out.println("Thread started.");
try {
Thread.sleep(2000); // Pause for 2 seconds
System.out.println("Thread resumed after sleep.");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.out.println("Thread interrupted.");
}
});
thread.start();
}
}
Output :
Thread started.
Thread resumed after sleep.
Key Differences Explained with Use Cases
Use Case for wait()
- Producer-Consumer Problem:
When a consumer thread needs to wait for a product to be available, it callswait()
on a shared object. Once the producer creates a product, it callsnotify()
to wake up the consumer.
Use Case for sleep()
- Scheduled Tasks:
If you want to perform an action at fixed intervals (e.g., sending a heartbeat every 5 seconds), you can useThread.sleep()
.
Summary of Key Differences
wait()
is used for inter-thread communication and relies on synchronization, whilesleep()
is used to pause execution temporarily without affecting locks.wait()
releases the monitor lock, whilesleep()
does not.notify()
ornotifyAll()
is needed to wake up a waiting thread, but a sleeping thread resumes automatically.
Choosing between wait()
and sleep()
depends on your use case:
- Use
wait()
when threads need to communicate. - Use
sleep()
when you want to pause a thread for a fixed amount of time.
stop()
and yield()
In Java, both stop()
and yield()
relate to thread behavior, but they serve different purposes. Since stop()
is deprecated, understanding its risks and knowing safe alternatives is crucial. Here’s a detailed comparison of stop()
vs yield()
, along with their use cases.
Comparison: stop()
vs yield()
Feature | stop() | yield() |
---|---|---|
Class | java.lang.Thread | java.lang.Thread |
Purpose | Forcefully stops a thread | Hints the scheduler to pause and allow other threads to execute |
Deprecation Status | Deprecated (since Java 2) | Not deprecated (but rarely used) |
Effect on Thread State | Terminates thread immediately | Thread moves to ready state |
Releases Locks | Yes, abruptly releases locks | No lock involvement |
Potential for Data Issues | High (can cause deadlocks, data corruption) | No known risks |
Control by Developer | Not recommended (uncontrollable termination) | Scheduler decides (no guarantee other threads will run) |
Alternative | Use a flag or interrupt() | Use thread priorities or better scheduling mechanisms |
Impact on CPU Scheduling | Stops the thread; CPU moves to other tasks | Helps in cooperative multitasking |
Interrupt Handling | N/A (forcefully stops) | Not directly related to interrupts |