Master in Threads in java
What are Threads?
In computing, a thread is the smallest unit of execution within a process. Threads share the same memory space and resources of a process, but they can execute independently. This allows for concurrent execution of tasks within a single application.
Threads vs. Processes
While both threads and processes represent units of execution, there are some key differences between them:
- Threads: Threads are lightweight, and multiple threads can exist within a single process. Threads share the same memory space and resources, making communication between them more efficient.
- Processes: Processes are heavier than threads and have their own memory space and resources. Communication between processes typically involves more overhead, such as inter-process communication mechanisms.
Why are Threads Important?
Threads are essential for achieving concurrency and parallelism in Java applications. They allow multiple tasks to be executed simultaneously, improving performance and responsiveness. Common use cases for threads include:
- Performing background tasks while the main application continues to run.
- Handling multiple client requests concurrently in a server application.
- Utilizing multi-core processors effectively by parallelizing computations.
There are several ways to create threads.
Here are the main methods along with code examples for each approach:
1. Extending the Thread
Class
You can create a thread by extending the Thread
class and overriding its run()
method. Here's an example:
class MyThread extends Thread {
public void run() {
System.out.println("Thread is running...");
}
}
public class ThreadExample1 {
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start(); // Start the thread
}
}
2. Implementing the Runnable
Interface
Another way to create a thread is by implementing the Runnable
interface and passing it to a Thread
constructor. Here's an example:
class MyRunnable implements Runnable {
public void run() {
System.out.println("Thread is running...");
}
}
public class ThreadExample2 {
public static void main(String[] args) {
Thread thread = new Thread(new MyRunnable());
thread.start(); // Start the thread
}
}
3. Using Lambda Expressions
With Java 8 and later versions, you can use lambda expressions to create and start a thread more concisely. Here’s an example:
public class ThreadExample3 {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
System.out.println("Thread is running...");
});
thread.start(); // Start the thread
}
}
4. Implementing the Callable
Interface
If you need to return a result or throw an exception from a thread, you can use the Callable
interface along with the ExecutorService
. Here's an example:
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
class MyCallable implements Callable<Integer> {
public Integer call() {
return 42;
}
}
public class ThreadExample4 {
public static void main(String[] args) throws Exception {
ExecutorService executorService = Executors.newSingleThreadExecutor();
Future<Integer> future = executorService.submit(new MyCallable());
Integer result = future.get(); // Waits for task to complete and returns the result
System.out.println("Result: " + result);
executorService.shutdown();
}
}
What is Callable
interface
In Java, the Callable
interface is similar to the Runnable
interface but with a key difference:
it can return a result or throw an exception. This makes it particularly useful when you need to execute a task asynchronously and retrieve a result or handle exceptions afterwards.
1. Declaration
The Callable
interface is part of the java.util.concurrent
package and is declared as follows:
@FunctionalInterface
public interface Callable<V> {
V call() throws Exception;
}
2. Method
The Callable
interface defines a single method called call()
, which represents the task that will be executed asynchronously. The call()
method returns a value of type V
(the type parameter), and it can throw a checked exception of type Exception
.
3. Usage
To use Callable
, you typically create a class that implements the Callable
interface and provides the implementation for the call()
method. This class represents the task you want to execute asynchronously. Here's an example
import java.util.concurrent.Callable;
class MyCallable implements Callable<Integer> {
public Integer call() throws Exception {
// Perform some computation and return a result
return 42;
}
}
4. Execution
Once you have a Callable
instance, you can submit it for execution to an ExecutorService
. The ExecutorService
manages the execution of the Callable
task and returns a Future
object, which represents the result of the computation. Here's how you can do it:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class Main {
public static void main(String[] args) throws Exception {
// Create an ExecutorService
ExecutorService executorService = Executors.newSingleThreadExecutor();
// Submit the Callable task for execution
Future<Integer> future = executorService.submit(new MyCallable());
// Wait for the task to complete and retrieve the result
Integer result = future.get();
// Shutdown the ExecutorService
executorService.shutdown();
// Print the result
System.out.println("Result: " + result);
}
}
5. Handling Exceptions
If the call()
method throws an exception, it will be propagated to the caller when you call future.get()
. You can handle exceptions using try-catch blocks as you would with any other method that can throw checked exceptions.
In summary, Callable
allows you to execute tasks asynchronously and retrieve results or handle exceptions afterwards. It's a powerful tool for concurrent programming in Java, especially when combined with ExecutorService
for managing thread execution.
--> join()
method
The join()
method in Java is used to wait for a thread to complete its execution before continuing with the execution of the current thread. It allows one thread to wait for another thread to finish
A real-time scenario for joining threads can be illustrated in a situation where you have multiple tasks running concurrently, and you need to ensure that some tasks are completed before proceeding with others. Let’s consider a simple example where you have two threads performing different tasks, and you want the main thread to wait until both threads complete their tasks before proceeding.
public class JoinExample {
public static void main(String[] args) {
Thread task1 = new Thread(new Task("Task 1"));
Thread task2 = new Thread(new Task("Task 2"));
task1.start();
task2.start();
try {
// Main thread waits for task1 to complete
task1.join();
System.out.println("Task 1 completed.");
// Main thread waits for task2 to complete
task2.join();
System.out.println("Task 2 completed.");
System.out.println("All tasks completed.");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
class Task implements Runnable {
private String name;
public Task(String name) {
this.name = name;
}
@Override
public void run() {
System.out.println("Executing " + name);
try {
// Simulating some task execution time
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(name + " completed.");
}
}
In this example:
- We have a
JoinExample
class with a main method where two threads,task1
andtask2
, are created to perform different tasks. - Both threads are started, and the main thread calls
join()
on each thread to wait for them to complete. - The
Task
class implements theRunnable
interface, and each task simulates some execution time by sleeping for 2 seconds. - The main thread waits for
task1
to complete first, then waits fortask2
to complete before printing "All tasks completed."
This scenario demonstrates how joining threads can be used to coordinate the execution of multiple tasks and ensure that certain tasks are completed before others proceed.
Now Lets see Interview Questions on Join() Method
- What is the purpose of the
join()
method in Java threads?
Answer: The join()
method is used to wait for a thread to complete its execution before proceeding with further operations in the current thread.
Thread thread = new Thread(() -> {
// Thread execution logic
});
thread.start();
thread.join(); // Wait for the thread to complete
2.How does the join()
method work in Java threads?
Answer: The join()
method blocks the current thread until the thread on which it is called completes its execution or the specified timeout period elapses.
thread.join(); // Waits indefinitely until the thread completes
// or
thread.join(timeoutInMillis); // Waits for the specified timeou
3 Can you explain the difference between calling join()
and not calling join()
on a thread?
Answer: When you call join()
on a thread, the current thread waits for that thread to complete before continuing its execution. If you don't call join()
, the current thread continues its execution immediately after the thread is started, without waiting for it to complete.
4. What happens if you call join()
on a thread that has already finished its execution?
Answer: Calling join()
on a thread that has already finished its execution has no effect. The method returns immediately.
5. Can you use the join()
method with a timeout? If so, how?
Answer: Yes, you can use join(timeout)
to specify a timeout period. If the thread does not complete within the specified timeout, the method returns, and the current thread continues execution.
thread.join(1000); // Wait for 1 second
6. What exceptions can be thrown by the join()
method and how would you handle them?
Answer: The join()
method throws InterruptedException
if the current thread is interrupted while waiting. You can handle it by catching the exception and taking appropriate action.
try {
thread.join();
} catch (InterruptedException e) {
// Handle interruption
e.printStackTrace();
}
7. In what scenarios would you use the join()
method in a real-world application?
Answer: You would use join()
when you need to wait for the completion of a thread before proceeding with subsequent operations, such as aggregating results from multiple threads, coordinating tasks, or ensuring synchronization.
8. How would you coordinate the execution of multiple threads using the join()
method?
Answer: You can call join()
on each thread sequentially to wait for their completion one by one, or you can use a combination of join()
and synchronization mechanisms to coordinate the execution of multiple threads.
9. What happens if you call join()
on the current thread?
Answer: Calling join()
on the current thread would result in a deadlock because the current thread would be waiting for itself to complete, which will never happen.
10. Can you call the join()
method on multiple threads simultaneously?
Answer: Yes, you can call join()
on multiple threads sequentially or in parallel to wait for their completion.
11. What happens if you call join()
on a thread before it is started?
Answer: If you call join()
on a thread before it is started, it will throw a IllegalThreadStateException
. This is because the thread must be in the "started" state for join()
to be invoked on it.
Thread thread = new Thread(() -> {
// Thread execution logic
});
try {
thread.join(); // Throws IllegalThreadStateException
} catch (InterruptedException e) {
e.printStackTrace();
}
12. Is it possible for a thread to call join()
on itself?
Answer: No, it is not possible for a thread to call join()
on itself. Doing so would result in a deadlock, as the thread would be waiting for itself to complete, which will never happen.
13. What happens if multiple threads call join()
on the same thread simultaneously?
Answer: Multiple threads can call join()
on the same thread simultaneously, but only one of them will be able to acquire the lock on the joined thread and wait for it to complete. The other threads will continue execution without waiting.
Thread thread = new Thread(() -> {
// Thread execution logic
});
Thread thread1 = new Thread(() -> {
try {
thread.join();
System.out.println("Thread1: Thread joined successfully");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
Thread thread2 = new Thread(() -> {
try {
thread.join();
System.out.println("Thread2: Thread joined successfully");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
thread.start();
thread1.start();
thread2.start();
14. Can you call join()
inside a synchronized block?
Answer: Yes, you can call join()
inside a synchronized block. However, it is not necessary unless you need to synchronize access to shared resources or variables.
Thread thread = new Thread(() -> {
synchronized (this) {
// Thread execution logic
}
});
try {
thread.start();
synchronized (this) {
thread.join();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
15. How can you implement a timeout for join()
without using the join(long timeout)
method?
Answer: One way to implement a timeout for join()
without using the join(long timeout)
method is to use a combination of join()
and Thread.sleep()
. You can repeatedly call join()
in a loop with a short sleep interval and check if the joined thread has completed within a certain timeout period.
Thread thread = new Thread(() -> {
try {
Thread.sleep(3000); // Simulating long-running task
System.out.println("Thread completed execution");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
thread.start();
long startTime = System.currentTimeMillis();
long timeout = 5000; // Timeout in milliseconds
try {
while (thread.isAlive()) {
thread.join(100); // Join with a short timeout
if ((System.currentTimeMillis() - startTime) > timeout) {
System.out.println("Timeout occurred. Exiting...");
break;
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
Practical use of join method
The join()
method in Java is commonly used in scenarios where you need to wait for the completion of one or more threads before proceeding with further execution. Here are some practical use cases where the join()
method can be beneficial:
- Parallel Processing: Suppose you have a task that can be split into multiple independent subtasks that can be executed in parallel by different threads. You can use the
join()
method to wait for all subtasks to complete before aggregating their results.
List<Thread> threads = new ArrayList<>();
List<Result> results = new ArrayList<>();
for (int i = 0; i < NUM_THREADS; i++) {
Thread thread = new Thread(() -> {
// Perform some computation and produce a result
Result result = compute();
results.add(result);
});
threads.add(thread);
thread.start();
}
// Wait for all threads to complete
for (Thread thread : threads) {
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// Aggregate results from all threads
// ...
2. Main Thread Synchronization: In some cases, you may want to ensure that certain initialization tasks in the main thread are completed before other operations are performed. You can use join()
to wait for background threads to complete before continuing.
Thread workerThread = new Thread(() -> {
// Perform initialization tasks
});
workerThread.start();
// Wait for initialization to complete before proceeding
try {
workerThread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
// Perform other operations after initialization
3. Producer-Consumer Pattern: In a producer-consumer scenario, where one thread produces data and another consumes it, you may want to ensure that all data is produced before consuming it. join()
can be used to synchronize the producer and consumer threads.
Thread producerThread = new Thread(() -> {
// Produce data
});
producerThread.start();
// Wait for producer to finish producing data
try {
producerThread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
// Start consumer thread after producer finishes
Thread consumerThread = new Thread(() -> {
// Consume data
});
consumerThread.start();
-→ yield() Method
The yield()
method in Java is used to pause the execution of the current thread temporarily, allowing other threads of the same priority to execute. Here's an example demonstrating the use of yield()
:
public class YieldExample {
public static void main(String[] args) {
Thread thread1 = new Thread(new MyRunnable("Thread 1"));
Thread thread2 = new Thread(new MyRunnable("Thread 2"));
thread1.start();
thread2.start();
}
static class MyRunnable implements Runnable {
private String name;
public MyRunnable(String name) {
this.name = name;
}
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(name + " executing iteration " + i);
Thread.yield(); // Pause the execution and give other threads a chance
}
}
}
}
In this example:
- We have a
YieldExample
class that starts two threads. - Each thread is represented by the
MyRunnable
class, which implements theRunnable
interface. - Inside the
run()
method ofMyRunnable
, we have a loop where each iteration prints a message indicating the thread name and the current iteration. - The
Thread.yield()
method is called in each iteration, which allows other threads to execute if they are of the same priority. - You may notice that due to yielding, the output may not be strictly interleaved between threads. Some iterations of one thread may be executed before others due to the yielding behavior.
This example demonstrates the use of yield()
to give other threads a chance to execute when multiple threads are running concurrently.
Deadlock
Deadlock in threading occurs when two or more threads are blocked indefinitely, waiting for each other to release resources that they need. This situation can arise due to improper synchronization of resources, leading to a deadlock condition where none of the threads can proceed. Let’s illustrate deadlock with a simple Java example:
public class DeadlockExample {
private static final Object lock1 = new Object();
private static final Object lock2 = new Object();
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
synchronized (lock1) {
System.out.println("Thread 1 acquired lock1");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock2) {
System.out.println("Thread 1 acquired lock2");
}
}
});
Thread thread2 = new Thread(() -> {
synchronized (lock2) {
System.out.println("Thread 2 acquired lock2");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock1) {
System.out.println("Thread 2 acquired lock1");
}
}
});
thread1.start();
thread2.start();
// Join threads to ensure main thread waits for their completion
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Execution completed");
}
}
What is CyclicBarrier
A CyclicBarrier
in Java is a synchronization utility that allows multiple threads to wait for each other at a predefined barrier point before continuing their execution. It's useful when you have a fixed number of threads that need to synchronize at certain points in their execution.
Let’s illustrate CyclicBarrier
with a simple example of a hiking trip:
Suppose a group of friends plans a hiking trip. They agree to meet at a designated point before starting the hike. Once everyone arrives, they synchronize, take a group photo, and then start hiking together.
Here’s how you can implement this scenario using a CyclicBarrier
:
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
public class HikingTrip {
private static final int NUM_FRIENDS = 4;
private static final CyclicBarrier barrier = new CyclicBarrier(NUM_FRIENDS, () -> {
System.out.println("All friends have arrived. Let's take a group photo!");
});
public static void main(String[] args) {
for (int i = 1; i <= NUM_FRIENDS; i++) {
new Thread(new Friend(i)).start();
}
}
static class Friend implements Runnable {
private final int friendId;
Friend(int friendId) {
this.friendId = friendId;
}
@Override
public void run() {
try {
System.out.println("Friend " + friendId + " is on the way to the meeting point.");
Thread.sleep(1000); // Simulate travel time
System.out.println("Friend " + friendId + " has arrived at the meeting point.");
// Wait for other friends to arrive
barrier.await();
// After all friends have arrived, continue with the hike
System.out.println("Friend " + friendId + " starts hiking.");
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
}
}
}
Friend 1 --> | | <-- Friend 3
| Meeting |
Friend 2 --> | Point | <-- Friend 4
|__________|
As each friend arrives at the meeting point, they wait until all friends have arrived. Once everyone is present, they take a group photo (barrier
action) and then start hiking together.
— — — one more example
Suppose we have a game where players wait for each other to start. Once all players are ready, the game begins.
import java.util.concurrent.CyclicBarrier;
public class GameStarter {
public static void main(String[] args) {
final int numPlayers = 4;
final CyclicBarrier barrier = new CyclicBarrier(numPlayers, () -> System.out.println("Game on!"));
// Initialize and start players
for (int i = 0; i < numPlayers; i++) {
new Thread(new Player(barrier)).start();
}
}
}
class Player implements Runnable {
private final CyclicBarrier barrier;
Player(CyclicBarrier barrier) {
this.barrier = barrier;
}
@Override
public void run() {
try {
System.out.println(Thread.currentThread().getName() + " is ready.");
barrier.await(); // Wait for all players to be ready
} catch (Exception e) {
e.printStackTrace();
}
}
}
CountDownLatch
CountDownLatch
is a synchronization aid in Java that allows one or more threads to wait until a set of operations being performed in other threads completes. It's essentially a counter that counts down from a specified number to zero.
Imagine you have a group of friends planning to meet at a restaurant for dinner. However, some friends are running late, and you don’t want to start eating until everyone has arrived. To synchronize the group’s arrival, you use a CountDownLatch
.
import java.util.concurrent.CountDownLatch;
public class DinnerExample {
public static void main(String[] args) throws InterruptedException {
// Create a CountDownLatch with a count of 3 (representing the number of friends)
CountDownLatch latch = new CountDownLatch(3);
// Start three threads representing the arrival of each friend
new Thread(new Friend("Alice", latch)).start();
new Thread(new Friend("Bob", latch)).start();
new Thread(new Friend("Charlie", latch)).start();
// Main thread waits until all friends have arrived
latch.await();
// All friends have arrived, so start dinner
System.out.println("All friends have arrived. Let's start dinner!");
}
}
// Represents a friend arriving at the restaurant
class Friend implements Runnable {
private final String name;
private final CountDownLatch latch;
public Friend(String name, CountDownLatch latch) {
this.name = name;
this.latch = latch;
}
@Override
public void run() {
System.out.println(name + " has arrived at the restaurant.");
// Each friend counts down the latch after arriving
latch.countDown();
}
}
one more example — -
Imagine you have a group of friends planning to meet at a park for a picnic. However, they are arriving from different locations, and you want everyone to arrive before starting the picnic. You can use CountDownLatch
to achieve this synchronization. Here's an easy example:
import java.util.concurrent.CountDownLatch;
public class PicnicExample {
public static void main(String[] args) throws InterruptedException {
// Create a CountDownLatch with a count of 5 (representing 5 friends)
CountDownLatch latch = new CountDownLatch(5);
// Each friend arrives and counts down the latch
for (int i = 1; i <= 5; i++) {
new Friend("Friend " + i, latch).start();
}
// Main thread waits until all friends arrive (latch counts down to 0)
latch.await();
// All friends have arrived, so start the picnic
System.out.println("All friends have arrived. Let's start the picnic!");
}
}
class Friend extends Thread {
private final CountDownLatch latch;
public Friend(String name, CountDownLatch latch) {
super(name);
this.latch = latch;
}
@Override
public void run() {
System.out.println(getName() + " has arrived at the park.");
// Each friend counts down the latch upon arrival
latch.countDown();
}
}
Lets see difference between CountDownLatch and CyclicBarrier.
import java.util.concurrent.CountDownLatch;
public class CountDownLatchExample {
public static void main(String[] args) throws InterruptedException {
CountDownLatch latch = new CountDownLatch(3);
for (int i = 0; i < 3; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " has completed its task.");
latch.countDown(); // Decrement the latch count
}).start();
}
latch.await(); // Main thread waits until latch count reaches zero
System.out.println("All threads have completed their tasks. Main thread resumes.");
}
}
import java.util.concurrent.CyclicBarrier;
public class CyclicBarrierExample {
public static void main(String[] args) {
CyclicBarrier barrier = new CyclicBarrier(3, () -> System.out.println("All threads have reached the barrier point."));
for (int i = 0; i < 3; i++) {
new Thread(() -> {
try {
System.out.println(Thread.currentThread().getName() + " has reached the barrier point.");
barrier.await(); // Wait at the barrier
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
}
}