What Is the Synchronized Keyword in Java? Explained with Examples

Table of Contents

When multiple threads run at the same time in Java, they often try to access the same resources — like a variable, object, or file. Without any control, this can cause unpredictable behavior and bugs that are hard to trace. That’s where the synchronized keyword in Java comes in.

In simple words, synchronized is a tool Java gives us to prevent multiple threads from interfering with each other while working on shared resources. Let’s break it down in a clear and practical way.

Why Do We Need the synchronized Keyword?

Imagine two people trying to withdraw money from the same bank account at the exact same time. If both transactions run without coordination, the account might go into a negative balance.

This type of problem is called a race condition. In Java, the synchronized keyword is used to avoid such situations by allowing only one thread at a time to access a block of code or method.

How Does synchronized Work?

When a thread enters a synchronized block or method, it locks the object it belongs to. Other threads trying to enter the same block or method must wait until the lock is released.

This locking mechanism ensures thread safety, but it also slows things down if used too often. That’s why it’s important to use it wisely.

Types of Synchronization in Java

There are two main ways to use the synchronized keyword in Java:

  1. Synchronized Method — Entire method is synchronized.
  2. Synchronized Block — Only a specific part of the code is synchronized.

Let’s look at both with examples.

Synchronized Method

Java
class Counter {
    private int count = 0;

    // Synchronized method
    public synchronized void increment() {
        count++;
    }

    public int getCount() {
        return count;
    }
}

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

        // Two threads incrementing the 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();

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

        System.out.println("Final Count: " + counter.getCount());
    }
}
  • The increment() method is marked as synchronized.
  • This means only one thread can execute it at a time.
  • Without synchronization, the final count might not be 2000 due to race conditions.
  • With synchronization, the output will always be 2000.

Run the program twice: once with synchronization enabled and once without. Compare the outputs to observe the effect of synchronization.

In this case, we will always get 2000 as the output in both scenarios.

Why the Result Can Still Be the Same (2000) Without Synchronization

When two threads increment the counter (count++), here’s what happens under the hood:

count++ is not atomic. It actually breaks down into three steps:

  1. Read the current value of count.
  2. Add 1 to it.
  3. Write the new value back to memory.

If two threads interleave at the wrong time, one update can overwrite the other. That’s the race condition.

But… race conditions don’t guarantee wrong results every single run. Sometimes:

  • The threads happen to run sequentially (one finishes a batch before the other interferes).
  • The CPU scheduler doesn’t interleave them in a conflicting way.
  • The number of operations is small, so the timing never collides.

In those cases, you might still get the “correct” result of 2000 by luck, even though the code isn’t thread-safe.

Why It’s Dangerous

The key point: the result is non-deterministic.

  • You might run the program 10 times and see 2000 each time.
  • But on the 11th run, you might get 1987 or 1995.

The behavior depends on CPU scheduling, thread timing, and hardware. That’s why without synchronization, the program is unsafe even if it sometimes looks fine.

How to Force the Wrong Behavior (to See the Bug)

If you want to actually see the race condition happen more often:

  • Increase the loop count (e.g., 1000000 instead of 1000).
  • Run on a machine with multiple cores.
  • Add artificial delays (like Thread.yield() inside the loop).

You’ll quickly notice results less than 2000 when threads interfere.

Without synchronized, getting 2000 doesn’t mean the code is correct — it just means the timing didn’t trigger a race condition in that run. Synchronization guarantees correctness every time, not just by chance.

Synchronized Block

Sometimes, we don’t need to synchronize an entire method — just a small critical section of code. That’s where synchronized blocks are useful.

Java
class Printer {
    public void printMessage(String message) {
        synchronized(this) {
            System.out.print("[" + message);
            try {
                Thread.sleep(100); // Simulate delay
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("]");
        }
    }
}

public class SyncBlockExample {
    public static void main(String[] args) {
        Printer printer = new Printer();

        Thread t1 = new Thread(() -> printer.printMessage("Hello"));
        Thread t2 = new Thread(() -> printer.printMessage("World"));

        t1.start();
        t2.start();
    }
}
  • Only the block inside synchronized(this) is locked.
  • This ensures that printing of messages happens in a safe, consistent way (e.g., [Hello] and [World], instead of jumbled outputs).
  • Synchronizing just the critical section improves performance compared to locking the whole method.

Static Synchronization

If a method is declared as static synchronized, the lock is placed on the class object rather than the instance. This is useful when you want synchronization across all instances of a class.

Java
class SharedResource {
    public static synchronized void showMessage(String msg) {
        System.out.println("Message: " + msg);
    }
}

Here, only one thread across all objects of SharedResource can access showMessage() at a time.

Pros and Cons of Using synchronized

Advantages

  • Prevents race conditions.
  • Ensures data consistency.
  • Provides a simple way to handle multi-threading issues.

Disadvantages

  • Can reduce performance because of thread blocking.
  • May lead to deadlocks if not handled carefully.
  • In large-scale systems, too much synchronization can become a bottleneck.

Best Practices for Using synchronized

  • Synchronize only the critical section, not the entire method, when possible.
  • Keep synchronized blocks short and efficient.
  • Avoid nested synchronization to reduce deadlock risks.
  • Consider higher-level concurrency tools like ReentrantLock or java.util.concurrent classes for complex scenarios.

Conclusion

The synchronized keyword in Java is a powerful tool to ensure thread safety. It allows you to control how multiple threads interact with shared resources, preventing errors like race conditions.

However, it’s not always the most efficient choice. Use it when necessary, but also explore modern concurrency utilities for more flexible and performant solutions.

If you’re just starting with multithreading in Java, mastering synchronized is the first step toward writing safe, concurrent programs.

Skill Up: Software & AI Updates!

Receive our latest insights and updates directly to your inbox

Related Posts

error: Content is protected !!