-
The
Threadclass in Java is used to create and manage threads. By extending theThreadclass, we can create a new thread by overriding itsrun()method. -
Example:
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 } }
-
Advantages:
- Simple to use.
- Methods like
start(),join(), andsleep()are part of theThreadclass.
-
Disadvantages:
- In Java, a class can only extend one class, so extending
Threadrestricts the class from extending other classes.
- In Java, a class can only extend one class, so extending
-
The
Runnableinterface defines a single methodrun(). It is implemented by classes that wish to execute code in a separate thread. -
Example:
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 } }
-
Advantages:
- Implements
Runnable, so the class can extend other classes. - Recommended approach for creating threads.
- Implements
Java threads have the following states, as defined in the Java Language Specification:
- A thread is created but not yet started.
- The thread is ready to run or is already running. The JVM’s thread scheduler can schedule it.
- The thread is blocked and waiting to acquire a lock.
- The thread is waiting indefinitely until another thread performs a specific action (e.g.,
notify(),notifyAll()).
- The thread is waiting for a specified amount of time (e.g.,
Thread.sleep(),wait(long timeout)).
- The thread has finished execution.
Diagram:
New --> Runnable <--> Blocked / Waiting / Timed Waiting --> Terminated
- A thread pool reuses a group of threads instead of creating new ones for each task.
- Java provides the
ExecutorServiceinterface for managing thread pools.
Example:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPoolExample {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(5);
for (int i = 0; i < 10; i++) {
Runnable worker = new WorkerThread("" + i);
executor.execute(worker);
}
executor.shutdown();
}
}
class WorkerThread implements Runnable {
private String message;
public WorkerThread(String s) {
this.message = s;
}
public void run() {
System.out.println(Thread.currentThread().getName() + " (Start) Message = " + message);
}
}- Each thread has a priority, an integer value between
1(MIN_PRIORITY) and10(MAX_PRIORITY). The default priority is5. - Thread priorities affect the order in which threads are scheduled but do not guarantee any specific execution order.
Thread t1 = new Thread();
t1.setPriority(Thread.MAX_PRIORITY);- Daemon threads are low-priority threads running in the background to perform tasks such as garbage collection.
- When all non-daemon threads finish execution, the JVM terminates the program, stopping all daemon threads.
Example:
class MyDaemonThread extends Thread {
public void run() {
if (Thread.currentThread().isDaemon()) {
System.out.println("Daemon thread running");
} else {
System.out.println("User thread running");
}
}
public static void main(String[] args) {
MyDaemonThread t1 = new MyDaemonThread();
MyDaemonThread t2 = new MyDaemonThread();
t1.setDaemon(true); // Now t1 is a daemon thread
t1.start();
t2.start();
}
}Java 8 introduced lambda expressions, which allow for concise code when using functional interfaces like Runnable. The Runnable interface is a functional interface with a single method (run()), making it suitable for lambda expressions.
Why Threads Can Use Lambda Expressions:
- The
Runnableinterface has only one abstract method,run(). This allows lambda expressions to be used to represent therun()method without needing an explicit implementation class.
Example:
public class LambdaThreadExample {
public static void main(String[] args) {
Thread t = new Thread(() -> {
System.out.println("Thread using lambda expression");
});
t.start();
}
}-
Synchronization ensures that multiple threads do not concurrently modify shared data in a way that leads to data inconsistency.
-
Synchronized Block:
- Ensures only one thread at a time can access the synchronized block of code.
synchronized (object) { // Critical section }
-
Synchronized Method:
- Only one thread can execute a synchronized method at a time.
public synchronized void myMethod() { // Critical section }
-
Intrinsic Locks:
- Each object in Java has an intrinsic lock. A thread can acquire the lock by entering a synchronized block or method.
Deadlock occurs when two or more threads are blocked forever, each waiting on the other to release a resource.
Example:
class DeadlockExample {
String resource1 = "Resource1";
String resource2 = "Resource2";
Thread t1 = new Thread(() -> {
synchronized (resource1) {
System.out.println("Thread 1 locked Resource 1");
try { Thread.sleep(100); } catch (Exception e) {}
synchronized (resource2) {
System.out.println("Thread 1 locked Resource 2");
}
}
});
Thread t2 = new Thread(() -> {
synchronized (resource2) {
System.out.println("Thread 2 locked Resource 2");
try { Thread.sleep(100); } catch (Exception e) {}
synchronized (resource1) {
System.out.println("Thread 2 locked Resource 1");
}
}
});
public static void main(String[] args) {
DeadlockExample example = new DeadlockExample();
example.t1.start();
example.t2.start();
}
}In the example, thread t1 locks resource1 and waits for resource2, while thread t2 locks resource2 and waits for resource1, creating a deadlock.
- Avoid Nested Locks: Avoid acquiring locks on multiple resources at once.
- Use Lock Timeout: Use a timeout to avoid waiting indefinitely.
- Use Deadlock Detection Algorithms: Implement deadlock detection mechanisms where needed.
-
The
join()method in Java allows one thread to wait until another thread finishes its execution. Whenjoin()is called on a thread, the calling thread pauses its execution until the specified thread terminates. -
Example:
class MyThread extends Thread { public void run() { for (int i = 0; i < 5; i++) { System.out.println(Thread.currentThread().getName() + " running"); } } } public class Main { public static void main(String[] args) throws InterruptedException { MyThread t1 = new MyThread(); MyThread t2 = new MyThread(); t1.start(); t1.join(); // Main thread will wait until t1 completes t2.start(); // t2 starts after t1 finishes } }
- Ensures sequential execution where one thread must complete before another can proceed.
- Useful in scenarios where you need to combine the results of multiple threads or maintain thread synchronization.
- Prevents premature execution of code that depends on the completion of another thread.
-
join()without parameters:- The calling thread waits indefinitely until the specified thread terminates.
thread.join(); // Waits indefinitely
-
join(long millis):- The calling thread waits for a specified time (
millismilliseconds). If the thread does not complete within the specified time, the calling thread resumes.
thread.join(1000); // Waits for 1000 milliseconds
- The calling thread waits for a specified time (
-
join(long millis, int nanos):- A more precise variant where the thread waits for a combination of milliseconds and nanoseconds.
thread.join(1000, 500); // Waits for 1000 milliseconds + 500 nanoseconds
-
Example: Waiting for Data Processing: Imagine a scenario where you're fetching data from multiple sources (e.g., calling several APIs in parallel), and you need to wait for all data to be fetched before proceeding with further processing.
Example:
class FetchDataThread extends Thread { private String source; public FetchDataThread(String source) { this.source = source; } public void run() { System.out.println("Fetching data from " + source); try { Thread.sleep(2000); } catch (InterruptedException e) { } } } public class DataAggregator { public static void main(String[] args) throws InterruptedException { FetchDataThread api1 = new FetchDataThread("API1"); FetchDataThread api2 = new FetchDataThread("API2"); FetchDataThread api3 = new FetchDataThread("API3"); api1.start(); api2.start(); api3.start(); api1.join(); // Wait for API1 to complete api2.join(); // Wait for API2 to complete api3.join(); // Wait for API3 to complete System.out.println("All data fetched. Proceeding with processing..."); } }
-
Control Over Thread Execution:
- It allows you to control the flow of your program by making sure that one thread completes its execution before another proceeds.
-
Combining Results:
- In multi-threaded environments, if multiple threads are performing different tasks,
join()ensures that all tasks are completed before proceeding with the final result or further execution.
- In multi-threaded environments, if multiple threads are performing different tasks,
-
Synchronization Without Locking:
- Unlike traditional synchronization using
synchronizedblocks,join()is a simpler mechanism to ensure the orderly completion of threads without explicit locks or waiting conditions.
- Unlike traditional synchronization using