Posted on: January 18, 2025 Posted by: rahulgite Comments: 0

Understanding Synchronization in Java Threads

Synchronization in Java ensures that multiple threads can access shared resources in a controlled and thread-safe manner. It prevents thread interference and consistency issues by coordinating thread execution.


What is Synchronization?

  • Definition: Synchronization is a process of controlling access to shared resources by multiple threads. It ensures that only one thread can access a critical section (a block of code accessing shared resources) at a time.
  • Why is it Needed? Without synchronization, threads can interfere with each other, leading to inconsistent results or race conditions. For example, two threads modifying the same variable simultaneously may overwrite each other’s changes.
  • Types of Synchronization:
    1. Process Synchronization: Ensures processes do not interfere with each other.
    2. Thread Synchronization: Coordinates threads within a process, which can be achieved in two ways:
      • Mutual Exclusion (Locks): Ensures only one thread accesses a critical section at a time.
      • Cooperation: Synchronizes threads based on specific conditions (e.g., wait() and notify()).

How to Synchronize in Java:

  1. Synchronized Methods:
    • Declaring a method as synchronized ensures that only one thread can execute it at a time for a given object.
    • Example:
class Counter {
    private int count = 0;

    public synchronized void increment() {
        count++;
    }

    public synchronized int getCount() {
        return count;
    }
}

public class Main {
    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());
    }
}
  1. Synchronized Blocks:
    • A synchronized block is used to synchronize only a specific part of the code, rather than the entire method.
    • Example:
class SharedResource {
    public void display(String message) {
        synchronized (this) {
            for (int i = 0; i < 5; i++) {
                System.out.println(message + " - " + i);
            }
        }
    }
}

public class Main {
    public static void main(String[] args) {
        SharedResource resource = new SharedResource();

        Thread t1 = new Thread(() -> resource.display("Thread 1"));
        Thread t2 = new Thread(() -> resource.display("Thread 2"));

        t1.start();
        t2.start();
    }
}
  1. Static Synchronization:
    • Synchronizing static methods ensures mutual exclusion for the class as a whole, rather than individual objects.
    • Example:
class Shared {
    public static synchronized void print(String msg) {
        System.out.println(msg);
    }
}

public class Main {
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> Shared.print("Thread 1"));
        Thread t2 = new Thread(() -> Shared.print("Thread 2"));

        t1.start();
        t2.start();
    }
}
  1. Using Locks:
    • The java.util.concurrent.locks.Lock interface provides more control over synchronization.
    • Example:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

class Counter {
    private int count = 0;
    private Lock lock = new ReentrantLock();

    public void increment() {
        lock.lock();
        try {
            count++;
        } finally {
            lock.unlock();
        }
    }

    public int getCount() {
        return count;
    }
}

public class Main {
    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());
    }
}

Key Points to Remember:

  • Synchronization is necessary to avoid thread interference and ensure thread safety.
  • Overusing synchronization can lead to performance issues due to thread contention.
  • Always minimize the synchronized block to the critical section to improve performance.
  • Use locks for more flexibility and control over synchronization compared to synchronized methods or blocks.

Leave a Comment