Synchronize

F

Fatima Alam

Guest
Demystifying the synchronized Keyword in Java

If youโ€™ve ever written multi-threaded code in Java, youโ€™ve likely stumbled upon the synchronized keyword. At first glance, it looks like a magic wand to solve all concurrency problemsโ€”but as with most "magical" solutions, it comes with caveats.

In this post, weโ€™ll break down what synchronized really does, when it works, when it doesnโ€™t, and weigh its pros and cons.

๐Ÿ”‘ What Does synchronized Do?

The synchronized keyword is Javaโ€™s built-in way to ensure mutual exclusion. In simple terms:

๐Ÿ‘‰ Only one thread at a time can execute a synchronized method or block thatโ€™s locked on the same object.

It achieves this by acquiring a monitor lock (also called an intrinsic lock) on the object. Other threads trying to acquire the same lock are forced to wait until itโ€™s released.

โœ… When Does synchronized Work?

synchronized is your friend when:

Multiple threads are accessing and modifying shared data.

You want to prevent race conditions.

You need to ensure memory visibility (changes made by one thread become visible to others).

You are protecting either:

Instance methods โ†’ lock is taken on this.

Static methods โ†’ lock is taken on the class object.

Code blocks โ†’ lock is taken on any object you specify (synchronized(someObject) { ... }).

โŒ When Doesnโ€™t It Help?

synchronized isnโ€™t a silver bullet. It does not help if:

Threads synchronize on different objects (wrong lock = no safety).

Youโ€™re dealing with deadlocks (threads waiting on each otherโ€™s locks).

Performance is criticalโ€”too much contention slows things down.

Youโ€™re working with non-shared resources (local variables donโ€™t need synchronization).

You need synchronization across JVMsโ€”synchronized only works within one JVM.

AspectPros โœ…Cons โŒ
Thread SafetyEnsures only one thread accesses shared resource at a time.Incorrect use (wrong lock) gives false sense of safety.
SimplicityVery easy to use (synchronized keyword is enough).Can be misused easily, leading to bugs.
Memory VisibilityGuarantees changes by one thread are visible to others.Doesnโ€™t prevent logical errors like deadlock or starvation.
FlexibilityCan synchronize methods or specific code blocks.Synchronizing too broadly (e.g., whole method) reduces performance.
ReliabilityBuilt-in, well-tested by JVM, no extra libraries needed.Performance bottleneck if many threads contend for same lock.
GranularityWorks well for small critical sections.Coarse-grained locking makes code slower and less scalable.

๐Ÿ‘ฉโ€๐Ÿ’ป Example: Counter Without and With synchronized
โŒ Without Synchronization (Race Condition)


Code:
class Counter {
    private int count = 0;

    public void increment() {
        count++; // not thread-safe
    }

    public int getCount() {
        return count;
    }
}

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

        Runnable task = () -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        };

        Thread t1 = new Thread(task);
        Thread t2 = new Thread(task);

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

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

Output -> ๐Ÿ‘‰ Youโ€™ll often get a result less than 2000 because both threads modify count at the same time, causing lost updates.

Why?
Let's look at the Timeline Graph

Time โ†“T1T2
t1BEGIN
t2READ count = 0
t3READ count = 0
t4UPDATE count = 1
t5UPDATE count = 1 (overwrites T1)
t6COMMIT (Final count = 1, lost update)COMMIT

โœ… With Synchronization


Code:
class Counter {
    private int count = 0;

    public synchronized void increment() {
        count++; // now thread-safe
    }

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

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

        Runnable task = () -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        };

        Thread t1 = new Thread(task);
        Thread t2 = new Thread(task);

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

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

Output -> ๐Ÿ‘‰ This time youโ€™ll reliably get 2000, because only one thread at a time can enter the increment() method.

How?
Timeline Graph

Time โ†“T1T2
t1ENTER synchronized block (lock acquired)
t2READ count = 0, UPDATE count = 1
t3EXIT synchronized block (lock released)
t4ENTER synchronized block (lock acquired)
t5READ count = 1, UPDATE count = 2
t6EXIT synchronized block (lock released)

Continue reading...
 


Join ๐•‹๐•„๐•‹ on Telegram
Channel PREVIEW:
Back
Top