JavaSynchronization

Synchronization

When multiple threads read and write shared data at the same time, without any coordination, the results can become unpredictable and wrong in ways that are notoriously hard to reproduce. Java's synchronized keyword is the fundamental tool for protecting shared state from this kind of corruption.
A Race Condition: The Broken Counter
The classic example is a shared counter incremented by multiple threads. The ++ operation looks atomic in source code, but it actually happens in three separate steps — read the current value, add one, write it back — and another thread can interleave its own read/add/write in between any of those steps.

A broken counter — lost updates from a race condition

Java
public class BrokenCounter {
    private int count = 0;

    void increment() {
        count++; // read, add 1, write back — NOT atomic
    }

    int getCount() {
        return count;
    }

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

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

        Thread t1 = new Thread(task);
        Thread t2 = new Thread(task);
        t1.start();
        t2.start();
        t1.join();
        t2.join();

        System.out.println("Expected: 200000");
        System.out.println("Actual:   " + counter.getCount());
    }
}
Expected: 200000
Actual:   187342
Lost updates
The actual result varies between runs and is almost always less than 200,000. Some increments get "lost" when two threads both read the same value before either writes back — one thread's update silently overwrites the other's.
Fixing It With synchronized
Marking a method synchronized means only one thread at a time can be executing that method on the same object — every other thread that tries to call it has to wait until the current one finishes. Under the hood, this works by having each object carry an intrinsic lock (sometimes called a monitor); a thread must acquire that lock before entering a synchronized method or block on that object, and releases it automatically on exit.

The fix — synchronized method

Java
public class SafeCounter {
    private int count = 0;

    synchronized void increment() {
        count++; // now only one thread can execute this at a time
    }

    int getCount() {
        return count;
    }

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

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

        Thread t1 = new Thread(task);
        Thread t2 = new Thread(task);
        t1.start();
        t2.start();
        t1.join();
        t2.join();

        System.out.println("Actual: " + counter.getCount());
    }
}
Actual: 200000
synchronized Blocks
Synchronizing an entire method can be more restrictive than necessary if only part of it touches shared state. A synchronized block lets you lock on a specific object for just the critical section, leaving the rest of the method free of locking overhead.

Synchronizing only the critical section

Java
class SafeCounter {
    private int count = 0;
    private final Object lock = new Object();

    void increment() {
        // do any non-shared work here, unsynchronized
        synchronized (lock) {
            count++; // only this line needs protection
        }
    }
}
Deadlock — A Brief Warning
Locks solve races, but they introduce their own hazard: a deadlock happens when two threads each hold a lock the other one needs, and neither can proceed. Classic example: thread A locks object 1 then waits for object 2, while thread B locks object 2 then waits for object 1 — both wait forever.
Note
The most reliable way to avoid deadlock is consistent lock ordering: if every thread in your program always acquires shared locks in the same order, the circular-wait condition that causes deadlock simply can't occur.

Concept

Description

Race condition

Unsynchronized threads interleave reads/writes to shared state, producing wrong results

synchronized method

Only one thread at a time may execute this method on a given object

synchronized block

Locks a specific object for just the critical section

Intrinsic lock (monitor)

The built-in lock every object carries, used by synchronized

Deadlock

Two or more threads wait on each other's locks forever

Tip
The java.util.concurrent package (covered in Concurrency Utilities) offers higher-level alternatives — ReentrantLock, AtomicInteger, concurrent collections, and more — that solve the same problems as synchronized with finer control and, in many cases, better performance. Learning synchronized first still matters: it's the foundation those higher-level tools build on.
  • Unsynchronized access to shared mutable state causes race conditions and lost updates

  • synchronized methods/blocks let only one thread execute the protected code on a given object at a time

  • Every object carries an intrinsic lock used by synchronized

  • Deadlock occurs when threads wait on each other's locks in a cycle — consistent lock ordering avoids it

  • java.util.concurrent offers higher-level alternatives worth learning next