Synchronized Blocks & Methods

About

In Java, synchronization ensures that multiple threads do not interfere with each other while accessing shared resources. The synchronized keyword is used to prevent race conditions, ensure atomicity, and maintain data consistency in multi-threaded applications.

Why is Synchronization Needed?

When multiple threads operate on shared data, race conditions can occur. This leads to inconsistent, corrupted, or unpredictable results. Synchronization ensures that only one thread at a time can access a critical section.

Example Without Synchronization (Race Condition)

class SharedResource {
    private int count = 0;

    void increment() { // Not synchronized
        count++;
    }

    int getCount() {
        return count;
    }
}

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

        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                resource.increment();
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                resource.increment();
            }
        });

        t1.start();
        t2.start();
        t1.join();
        t2.join();

        System.out.println("Final Count: " + resource.getCount()); // Expected: 2000, but may be incorrect due to race condition
    }
}

Output (Inconsistent Results)

Final Count: 1892  // (May vary)

Here, multiple threads modify count simultaneously, leading to data corruption.

Synchronized Methods

A synchronized method ensures that only one thread at a time can execute the method on an instance of the class.

  • When a thread enters a synchronized method, it acquires an intrinsic lock (also known as monitor lock) on the object.

  • If another thread attempts to call any other synchronized method on the same object, it will be blocked until the first thread releases the lock.

  • This prevents race conditions but introduces performance overhead due to blocking.

Syntax

synchronized returnType methodName(parameters) {
    // Critical section
}

Example of Synchronized Method

class SharedCounter {
    private int count = 0;

    public synchronized void increment() { // Only one thread can access this method at a time
        count++;
    }

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

public class SynchronizedMethodExample {
    public static void main(String[] args) throws InterruptedException {
        SharedCounter counter = new SharedCounter();

        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();
        t1.join();
        t2.join();

        System.out.println("Final Count: " + counter.getCount()); // Always 2000
    }
}

Output (Always Correct)

Final Count: 2000

How It Works?

  • The method is synchronized using synchronized keyword.

  • Java uses an intrinsic lock (monitor) on the instance (this).

  • If one thread enters the method, other threads must wait until it exits.

When to Use?

  • When the entire method needs to be protected.

  • When a class method modifies instance variables shared between threads.

  • When a method is small and does not require fine-grained locking.

Synchronized Blocks

Instead of locking an entire method, a synchronized block locks only a specific critical section inside a method.

  • This allows other non-critical code to execute without blocking.

  • Instead of locking the entire object (this), it can use a custom lock object, allowing more flexibility.

Syntax

synchronized(lockObject) {
    // Critical section
}

Example of Synchronized Block

class SharedCounter {
    private int count = 0;
    private final Object lock = new Object(); // Custom lock object

    public void increment() {
        synchronized (lock) { // Synchronizing only this block
            count++;
        }
    }

    public int getCount() {
        return count;
    }
}

public class SynchronizedBlockExample {
    public static void main(String[] args) throws InterruptedException {
        SharedCounter counter = new SharedCounter();

        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();
        t1.join();
        t2.join();

        System.out.println("Final Count: " + counter.getCount()); // Always 2000
    }
}

How It Works?

  • The lock is applied only on the necessary code block.

  • Other parts of the method can execute without blocking.

  • This increases efficiency compared to synchronizing the whole method.

When to Use?

  • When only a part of the method needs protection.

  • When performance is a concern, and unnecessary locking is avoided.

  • When multiple locks are required for different data elements.

Class-Level Synchronization (Static Methods)

Class-level synchronization ensures that only one thread at a time can execute a synchronized static method, across all instances of the class.

  • The lock is on the Class object (Class<T>) instead of an instance.

  • Even if multiple objects of the class exist, they share the same class-level lock.

Example

class SharedCounter {
    private static int count = 0;

    public static synchronized void increment() { // Class-level lock
        count++;
    }

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

When to Use?

  • When modifying static variables shared across multiple instances.

Class-Level Lock vs. Instance-Level Lock

Aspect

Instance-Level Lock

Class-Level Lock

Scope

Applied to a single object

Applied to the entire class

Effect

Only one thread per object can enter

Only one thread across all instances can enter

Use Case

When dealing with instance-specific data

When dealing with shared static data

Comparison Table

Feature

Synchronized Method

Synchronized Block

Class-Level Synchronization

Lock Type

Intrinsic lock on this (object)

Lock on a specific section

Lock on Class<T>

Performance

Low (entire method is locked)

High (only necessary section locked)

Moderate (static method locks entire class)

Flexibility

Low (locks entire method)

High (can use custom lock objects)

Medium (locks entire static method)

Use Case

When entire method needs synchronization

When part of a method needs synchronization

When static shared data needs synchronization

Last updated

Was this helpful?