In Java, the Thread Lifecycle and Synchronization are fundamental concepts for understanding how threads operate and interact with each other. Let’s break them down in detail:
1. Thread Lifecycle
A thread in Java goes through various states during its lifecycle. These states are defined in the Thread.State
enum:
- New: The thread is created but not yet started.
- Runnable: The thread is ready to run or is currently running.
- Blocked/Waiting: The thread is waiting for a monitor lock or another thread to perform a specific action.
- Timed Waiting: The thread is waiting for a specified amount of time.
- Terminated: The thread has completed its execution.
Thread Lifecycle Diagram
New → Runnable → (Blocked/Waiting/Timed Waiting) → Terminated
Example: Thread Lifecycle
public class ThreadLifecycleExample {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
System.out.println("Thread is running: " + Thread.currentThread().getName());
});
System.out.println("Thread State: " + thread.getState()); // NEW
thread.start();
System.out.println("Thread State: " + thread.getState()); // RUNNABLE
thread.join(); // Wait for the thread to terminate
System.out.println("Thread State: " + thread.getState()); // TERMINATED
}
}
2. Thread Synchronization
When multiple threads access shared resources, synchronization is required to avoid race conditions and ensure thread safety. Java provides several mechanisms for synchronization:
a. Synchronized Methods
- A method marked as
synchronized
can be executed by only one thread at a time. - The lock is acquired on the object instance (for instance methods) or the class object (for static methods).
Example: Synchronized Method
class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
public int getCount() {
return count;
}
}
public class SynchronizationExample {
public static void main(String[] args) throws InterruptedException {
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();
thread1.join();
thread2.join();
System.out.println("Count: " + counter.getCount()); // Output: 2000
}
}
b. Synchronized Blocks
- A block of code marked as
synchronized
can be executed by only one thread at a time. - The lock is acquired on the specified object.
Example: Synchronized Block
class Counter {
private int count = 0;
private final Object lock = new Object();
public void increment() {
synchronized (lock) {
count++;
}
}
public int getCount() {
return count;
}
}
3. Thread Communication
Threads can communicate with each other using the wait()
, notify()
, and notifyAll()
methods. These methods are used in synchronized contexts.
Example: Producer-Consumer Problem
class SharedResource {
private int data;
private boolean available = false;
public synchronized void produce(int value) throws InterruptedException {
while (available) {
wait(); // Wait for the consumer to consume
}
data = value;
available = true;
notify(); // Notify the consumer
}
public synchronized int consume() throws InterruptedException {
while (!available) {
wait(); // Wait for the producer to produce
}
available = false;
notify(); // Notify the producer
return data;
}
}
public class ProducerConsumerExample {
public static void main(String[] args) {
SharedResource resource = new SharedResource();
Runnable producer = () -> {
try {
for (int i = 0; i < 5; i++) {
resource.produce(i);
System.out.println("Produced: " + i);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
};
Runnable consumer = () -> {
try {
for (int i = 0; i < 5; i++) {
int value = resource.consume();
System.out.println("Consumed: " + value);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
};
Thread producerThread = new Thread(producer);
Thread consumerThread = new Thread(consumer);
producerThread.start();
consumerThread.start();
}
}
4. Deadlock
Deadlock occurs when two or more threads are blocked forever, waiting for each other to release resources.
Example: Deadlock
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: Holding lock 1");
try { Thread.sleep(100); } catch (InterruptedException e) {}
synchronized (lock2) {
System.out.println("Thread 1: Holding lock 1 and lock 2");
}
}
});
Thread thread2 = new Thread(() -> {
synchronized (lock2) {
System.out.println("Thread 2: Holding lock 2");
try { Thread.sleep(100); } catch (InterruptedException e) {}
synchronized (lock1) {
System.out.println("Thread 2: Holding lock 2 and lock 1");
}
}
});
thread1.start();
thread2.start();
}
}
5. Best Practices for Synchronization
- Use synchronized methods/blocks to protect shared resources.
- Avoid deadlock by acquiring locks in a consistent order.
- Use
wait()
,notify()
, andnotifyAll()
for thread communication. - Prefer concurrent collections (e.g.,
ConcurrentHashMap
) for thread-safe data structures. - Use atomic variables (e.g.,
AtomicInteger
) for simple thread-safe operations.
6. Example: Combining Thread Lifecycle and Synchronization
class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
public int getCount() {
return count;
}
}
public class ThreadLifecycleAndSynchronizationExample {
public static void main(String[] args) throws InterruptedException {
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);
System.out.println("Thread 1 State: " + thread1.getState()); // NEW
System.out.println("Thread 2 State: " + thread2.getState()); // NEW
thread1.start();
thread2.start();
System.out.println("Thread 1 State: " + thread1.getState()); // RUNNABLE
System.out.println("Thread 2 State: " + thread2.getState()); // RUNNABLE
thread1.join();
thread2.join();
System.out.println("Thread 1 State: " + thread1.getState()); // TERMINATED
System.out.println("Thread 2 State: " + thread2.getState()); // TERMINATED
System.out.println("Count: " + counter.getCount()); // Output: 2000
}
}
By understanding the Thread Lifecycle and Synchronization, you can write efficient, thread-safe, and deadlock-free Java applications!