Thread Lifecycle and Synchronization

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:

  1. New: The thread is created but not yet started.
  2. Runnable: The thread is ready to run or is currently running.
  3. Blocked/Waiting: The thread is waiting for a monitor lock or another thread to perform a specific action.
  4. Timed Waiting: The thread is waiting for a specified amount of time.
  5. 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(), and notifyAll() 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!

Leave a Reply

Your email address will not be published. Required fields are marked *