Skip to content

Thread Synchronization in Java

Thread synchronization is a mechanism to control the access of multiple threads to shared resources to prevent inconsistent or incorrect results. It is essential in multithreaded applications to avoid thread interference and ensure data consistency.


Why Synchronization is Needed?

When multiple threads access shared resources (e.g., variables, objects, files) simultaneously, conflicts may arise, leading to unpredictable behavior. For example:

  • Thread Interference: Two threads modifying the same variable at the same time can lead to incorrect results.
  • Race Conditions: When threads race to execute a block of code, the output depends on the execution order, leading to inconsistent results.

Mechanisms for Synchronization

  1. Synchronized Methods
  2. Synchronized Blocks
  3. Static Synchronization
  4. Locks (java.util.concurrent.locks)

1. Synchronized Methods

Using the synchronized keyword, you can make a method thread-safe by allowing only one thread at a time to execute the method.

Example:

class Counter {

    private int count = 0;

    public synchronized void increment() { // Synchronized method

        count++;

    }

    public int getCount() {

        return count;

    }

}

public class SyncExample {

    public static void main(String[] args) {

        Counter counter = new Counter();

        Thread t1 = new Thread(() -> {

            for (int i = 0; i < 1000; i++) counter.increment();

        });

        Thread t2 = new Thread(() -> {

            for (int i = 0; i < 1000; i++) counter.increment();

        });

        t1.start();

        t2.start();

        try {

            t1.join();

            t2.join();

        } catch (InterruptedException e) {

            e.printStackTrace();

        }

        System.out.println(“Final Count: ” + counter.getCount());

    }

}

Output:

Final Count: 2000

Without synchronization, the result might be inconsistent due to thread interference.


2. Synchronized Blocks

Instead of synchronizing the entire method, you can synchronize only the critical section of code. This improves performance by reducing the time a thread holds the lock.

Example:

class Counter {

    private int count = 0;

    public void increment() {

        synchronized (this) { // Critical section

            count++;

        }

    }

    public int getCount() {

        return count;

    }

}

  • this: Refers to the instance-level lock.
  • Synchronizing only the critical section minimizes overhead.

3. Static Synchronization

If the shared resource belongs to the class (not the instance), use a static synchronized method or block. This synchronizes on the class-level lock.

Example:

class SharedResource {

    public static synchronized void print(String msg) {

        System.out.println(“[” + msg + “]”);

    }

}

public class StaticSyncExample {

    public static void main(String[] args) {

        Thread t1 = new Thread(() -> SharedResource.print(“Thread 1”));

        Thread t2 = new Thread(() -> SharedResource.print(“Thread 2”));

        t1.start();

        t2.start();

    }

}


4. Locks (java.util.concurrent.locks)

The java.util.concurrent.locks package provides more flexible synchronization mechanisms compared to the synchronized keyword.

ReentrantLock Example:

import java.util.concurrent.locks.ReentrantLock;

class Counter {

    private int count = 0;

    private final ReentrantLock lock = new ReentrantLock();

    public void increment() {

        lock.lock(); // Acquire the lock

        try {

            count++;

        } finally {

            lock.unlock(); // Release the lock

        }

    }

    public int getCount() {

        return count;

    }

}

Advantages of Locks:

  • Fine-grained control over synchronization.
  • Allows for try-lock mechanisms to avoid deadlocks.

Inter-Thread Communication

Java provides methods such as wait(), notify(), and notifyAll() to allow synchronized threads to communicate with each other. These methods must be called from within a synchronized block or method.

Example:

class SharedResource {

    private boolean isAvailable = false;

    public synchronized void produce() throws InterruptedException {

        while (isAvailable) {

            wait();

        }

        System.out.println(“Produced!”);

        isAvailable = true;

        notify();

    }

    public synchronized void consume() throws InterruptedException {

        while (!isAvailable) {

            wait();

        }

        System.out.println(“Consumed!”);

        isAvailable = false;

        notify();

    }

}

public class InterThreadCommExample {

    public static void main(String[] args) {

        SharedResource resource = new SharedResource();

        Thread producer = new Thread(() -> {

            try {

                while (true) resource.produce();

            } catch (InterruptedException e) {

                Thread.currentThread().interrupt();

            }

        });

        Thread consumer = new Thread(() -> {

            try {

                while (true) resource.consume();

            } catch (InterruptedException e) {

                Thread.currentThread().interrupt();

            }

        });

        producer.start();

        consumer.start();

    }

}


Key Points

  1. Avoid Deadlocks:
    1. Deadlocks occur when two or more threads are waiting indefinitely for each other to release resources.
    1. To prevent deadlocks, avoid nested synchronization or use try-lock mechanisms.
  2. Minimize Synchronized Sections:
    1. Synchronize only critical sections to improve performance.
  3. Use volatile for Simple Variables:
    1. For visibility without synchronization, use volatile for single-variable states.

By using thread synchronization correctly, you can ensure safe and consistent data manipulation in a multithreaded environment.