Course topics

By WebNest Studio Engineering Team

Java - Core Tutorial

Synchronization

When multiple threads read and modify shared data at the same time without coordination, you get a race condition: the final result depends on the unpredictable timing of thread execution, and it is often wrong. Synchronization is Java's built-in mechanism for making sure only one thread at a time can execute a critical section of code that touches shared, mutable state.

Why it matters

When two threads change the same data without coordination, updates get lost. The program does not crash; it just produces a slightly wrong number, and a different wrong number on the next run. Synchronization is the basic tool that makes a shared operation happen as one uninterrupted step.

Intrinsic Locks (Monitors)

Every Java object carries a hidden intrinsic lock, also called a monitor. The synchronized keyword tells the JVM that a thread must acquire an object's monitor before entering the guarded code, and must release it on exit (even if an exception is thrown). Only one thread can hold a given object's monitor at a time; any other thread that tries to enter a block synchronized on the same object is placed in the BLOCKED state until the lock is released.

Synchronized Methods vs Synchronized Blocks

Marking an entire method synchronized locks on this (for instance methods) or on the class object itself (for static methods), for the full duration of the call. A synchronized block, synchronized (lockObject) { ... }, lets you lock on a specific object and guard only the minimal section of code that actually needs protection, which reduces contention and improves throughput compared to synchronizing an entire method unnecessarily.

  • Synchronized method: public synchronized void increment() { ... } — simple, but locks for the whole method body.
  • Synchronized block: synchronized (this) { count++; } — precise, locks only what needs protecting.
  • Static synchronized methods lock on the Class object, which is a different lock than any instance lock.

Race Condition, With and Without Synchronization

A classic example is two threads both executing count++ on a shared counter. That single line is actually three steps at the bytecode level — read the value, add one, write it back — and if two threads interleave those steps, one increment can be lost. Wrapping the increment in a synchronized block (or method) forces the read-modify-write sequence to complete atomically from the perspective of other threads, eliminating the lost update.

Example

A shared counter incremented safely from two threads using a synchronized method

Java
public class SynchronizationDemo {
    private int count = 0;

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

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

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

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

        System.out.println("Final count: " + demo.count);
    }
}
Output
Final count: 200000
(Without the "synchronized" keyword on increment(), this total is frequently LESS than 200000
because of lost updates from unsynchronized concurrent access — the exact wrong value varies per run.)

Common Mistakes

  • Synchronizing on different objects (or on "this" in one place and a separate lock object elsewhere) and assuming they provide mutual exclusion together — only threads synchronizing on the SAME object are mutually exclusive.
  • Synchronizing an entire method when only a small section touches shared state, unnecessarily reducing concurrency.
  • Assuming that making a variable "synchronized" is valid syntax — synchronization applies to methods and blocks, not to variable declarations (that is what volatile is for, and it solves a different problem).
  • Forgetting that synchronized only prevents concurrent access to the guarded code; it does not automatically make an entire class or all of its methods thread-safe.

Exercise

Two threads each call increment() 10,000 times, but the program usually prints less than 20000 because count++ is not atomic. Make the counter thread-safe.

Expected output: 20000

Java · starter code
class Counter {
    private int count = 0;

    void increment() {
        count++;
    }

    int get() {
        return count;
    }
}

public class RaceFix {
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();
        Runnable task = () -> {
            for (int i = 0; i < 10000; i++) {
                counter.increment();
            }
        };

        Thread first = new Thread(task);
        Thread second = new Thread(task);
        first.start();
        second.start();
        first.join();
        second.join();

        System.out.println(counter.get());
    }
}
Show hints
  • count++ is three steps: read, add one, write. Two threads can interleave between them.
  • Marking both methods synchronized lets only one thread at a time run them on the same Counter.
Show solution
Java · solution
class Counter {
    private int count = 0;

    synchronized void increment() {
        count++;
    }

    synchronized int get() {
        return count;
    }
}

public class RaceFix {
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();
        Runnable task = () -> {
            for (int i = 0; i < 10000; i++) {
                counter.increment();
            }
        };

        Thread first = new Thread(task);
        Thread second = new Thread(task);
        first.start();
        second.start();
        first.join();
        second.join();

        System.out.println(counter.get()); // 20000
    }
}

Quick quiz

1. Is count++ an atomic operation on a shared int field?

  • A. Yes
  • B. Only on a 64-bit JVM
  • C. No; it reads, adds and writes as separate steps
  • D. Only if count is static
Show answer

C. Another thread can run between the read and the write, so one of the two updates can be lost.

2. Which lock does a synchronized instance method acquire?

  • A. The lock of the class
  • B. No lock
  • C. A global lock for the whole JVM
  • D. The lock of the object it is called on (this)
Show answer

D. An instance method locks the object itself. A static synchronized method locks the Class object.

3. Two threads call two different synchronized instance methods on the same object. What happens?

  • A. Both run at the same time
  • B. One waits until the other releases the object's lock
  • C. It does not compile
  • D. Both throw an exception
Show answer

B. Both methods need the same lock, the object's, so they cannot run at the same time on that object.

Interview questions

What is a race condition?

Show answer

A race condition occurs when the result of a program depends on the timing of threads that read and modify shared data without coordination. The classic example is two threads incrementing a counter: both read the same value, both add one, and both write it back, so one increment is lost. It is fixed by making the operation atomic, with synchronization, a lock, or an atomic class such as AtomicInteger.

What is the difference between a synchronized method and a synchronized block?

Show answer

A synchronized method holds the lock for the whole method, on this or on the class for a static method. A synchronized block holds a lock only for the statements inside it and lets you choose which object to lock. Blocks are preferred when only part of the method touches shared state, because a shorter locked section lets other threads proceed sooner.

Key Points to Remember

  • Race conditions happen when multiple threads read and write shared state without coordination.
  • The synchronized keyword uses an object's intrinsic lock (monitor) to allow only one thread at a time into a guarded section.
  • Synchronized blocks are more precise and efficient than synchronizing whole methods when only part of the method touches shared state.
  • Static synchronized methods lock on the Class object, a separate lock from any particular instance.

Practice in the playground

Write and run your own code for this topic, or start from the exercise or an example. Runs plain Java 17. Frameworks such as Spring, and databases, are not available here. Your code for each topic is kept in this browser.

Run the examples

Change an input, predict the result, then compare it with the output. Explain why the result changes.

WebNest Studio

Written by

WebNest Studio Engineering Team

Practical lessons from developers working with Java, Python, React, databases and production systems.

Led by Vansh Duggal, Founder, WebNest Studio · Full-stack & software engineering