Thread Lifecycle and Synchronization

Loading

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 *