Threads are a fundamental concept in programming that enable concurrent execution of code, allowing applications to perform multiple operations simultaneously. In Java, threads play a vital role in achieving parallelism, improving performance, and enhancing the responsiveness of applications.
A thread is a lightweight process that can execute tasks independently of the main program flow. Each thread has its own call stack, registers, and local variables but shares memory with other threads in the same process. This shared memory model facilitates communication between threads but also introduces the complexity of synchronization to avoid concurrent access issues.
A thread in Java goes through several states during its lifecycle:
There are two primary ways to create a thread in Java:
Thread
class: By creating a subclass of the Thread
class and overriding its run()
method.Runnable
interface: By creating a class that implements the Runnable
interface and defining the run()
method.
class MyThread extends Thread {
@Override
public void run() {
System.out.println("Thread is running: " + Thread.currentThread().getName());
}
}
public class ThreadExample {
public static void main(String[] args) {
MyThread thread1 = new MyThread();
MyThread thread2 = new MyThread();
thread1.start(); // Starts the thread
thread2.start(); // Starts another thread
}
}
MyThread
class extends the Thread
class and overrides the run()
method to define the thread’s behavior.start()
method is called to begin the thread’s execution. This invokes the run()
method in a new call stack.
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("Thread is running: " + Thread.currentThread().getName());
}
}
public class RunnableExample {
public static void main(String[] args) {
Thread thread1 = new Thread(new MyRunnable());
Thread thread2 = new Thread(new MyRunnable());
thread1.start(); // Starts the thread
thread2.start(); // Starts another thread
}
}
MyRunnable
class implements the Runnable
interface and defines the run()
method.Thread
are created with MyRunnable
as the target. The start()
method is called to execute the thread.Java provides several methods in the Thread
class for thread management. Some commonly used methods include:
start()
: Starts the execution of the thread.run()
: Defines the code to be executed by the thread.sleep(long millis)
: Pauses the current thread for a specified time.join()
: Waits for the thread to die before proceeding.interrupt()
: Interrupts a thread, setting its interrupt flag.isAlive()
: Checks if the thread is still running.
class SleepyThread extends Thread {
@Override
public void run() {
try {
System.out.println(Thread.currentThread().getName() + " is sleeping for 2 seconds.");
Thread.sleep(2000); // Sleep for 2 seconds
System.out.println(Thread.currentThread().getName() + " has woken up.");
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + " was interrupted.");
}
}
}
public class ThreadMethodExample {
public static void main(String[] args) {
SleepyThread thread = new SleepyThread();
thread.start();
try {
thread.join(); // Wait for the thread to finish
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Main thread continues.");
}
}
SleepyThread
class sleeps for 2 seconds during its execution.join()
method is called to ensure the main thread waits for the SleepyThread
to finish before proceeding.When multiple threads access shared resources, synchronization is necessary to prevent data inconsistency. Java provides several mechanisms for synchronization, including:
synchronized
ensures that only one thread can execute it at a time.
class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
public int getCount() {
return count;
}
}
public class SynchronizedMethodExample {
public static void main(String[] args) {
Counter counter = new Counter();
Runnable task = () -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
};
Thread thread1 = new Thread(task);
Thread thread2 = new Thread(task);
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Final count: " + counter.getCount());
}
}
increment()
method is synchronized, ensuring that only one thread can modify the count
variable at a time.Synchronized blocks allow for more granular control over synchronization:
class Counter {
private int count = 0;
public void increment() {
synchronized (this) { // Synchronized block
count++;
}
}
public int getCount() {
return count;
}
}
public class SynchronizedBlockExample {
public static void main(String[] args) {
Counter counter = new Counter();
Runnable task = () -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
};
Thread thread1 = new Thread(task);
Thread thread2 = new Thread(task);
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Final count: " + counter.getCount());
}
}
increment()
method contains a synchronized block, allowing finer control over which part of the method is synchronized.Java provides mechanisms for thread communication using the wait()
, notify()
, and notifyAll()
methods. These methods are used to manage the execution of threads that share resources.
class SharedResource {
private int count = 0;
public synchronized void increment() {
count++;
notify(); // Notify waiting threads
}
public synchronized int getCount() throws InterruptedException {
while (count == 0) {
wait(); // Wait for count to be incremented
}
return count;
}
}
public class ThreadCommunicationExample {
public static void main(String[] args) {
SharedResource resource = new SharedResource();
Thread producer = new Thread(() -> {
for (int i = 0; i < 5; i++) {
resource.increment();
System.out.println("Incremented count to: " + resource.getCount());
}
});
Thread consumer = new Thread(() -> {
for (int i = 0; i < 5; i++) {
try {
System.out.println("Count: " + resource.getCount());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
consumer.start();
producer.start();
}
}
SharedResource
class maintains a count that is incremented by the producer thread.notify()
to wake the waiting consumer.Java provides the Executors framework, which simplifies the management of threads. It allows for thread pooling and concurrent task execution.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
class Task implements Runnable {
@Override
public void run() {
System.out.println("Task is running in: " + Thread.currentThread().getName());
}
}
public class ExecutorServiceExample {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(3); // Pool of 3 threads
for (int i = 0; i < 5; i++) {
executor.execute(new Task());
}
executor.shutdown(); // Shutdown the executor
}
}
ExecutorService
is created with a fixed thread pool size of 3.execute()
method submits tasks to the executor for concurrent execution.shutdown()
method is called to terminate the executor after all tasks are completed.