synchronized is Java's built-in mutual-exclusion mechanism. Every object has an intrinsic lock (a monitor); a synchronized block or method acquires that monitor on entry and releases it on exit — automatically, even if the block throws. It is the simplest way to make a section of code run one-thread-at-a-time, and despite the richer java.util.concurrent locks, it remains the right default for straightforward mutual exclusion.

What object are you locking?

The single most important question, and the most common source of bugs. A synchronized method locks a specific object, and only threads contending for the same object's monitor are mutually excluded:

FormLocks
synchronized instance methodthis
synchronized static methodthe Class object
synchronized(obj) { }obj's monitor

Two threads synchronizing on different objects do not exclude each other at all. A frequent bug is a synchronized instance method that is supposed to protect shared static state — each instance locks its own this, so the shared state is unprotected.

Advertisement

Block vs method synchronization

synchronized on a method locks for the method's entire duration. A synchronized block lets you narrow the critical section to just the lines that need it, reducing contention:

public void process(Item item) {
    Result r = expensivePureComputation(item);  // no shared state -- keep it OUT of the lock
    synchronized (this) {
        sharedList.add(r);                       // only this needs protection
    }
}

Holding a lock across slow work (I/O, expensive computation) that does not touch shared state serialises threads needlessly and can tank throughput. Keep critical sections as short as correctness allows.

Lock on a private final object

Locking on this or on a Class exposes your lock to the outside world: any code with a reference to your object can also synchronized(yourObject) and interfere with your locking, causing surprising contention or deadlock. The defensive idiom is a private lock object nobody else can see:

private final Object lock = new Object();
public void update() {
    synchronized (lock) { /* ... */ }
}

This encapsulates the lock so external code cannot participate in it, and lets you use separate locks for independent state within one object.

Reentrancy and happens-before

Intrinsic locks are reentrant: a thread already holding an object's monitor can enter another synchronized block on the same object without deadlocking — the JVM tracks a hold count. This is what lets a synchronized method call another synchronized method on the same object safely. synchronized also establishes happens-before: everything done before releasing a monitor is visible to the next thread that acquires it, so it guarantees both mutual exclusion and memory visibility — the two things concurrent code needs.

Advertisement

Deadlock and lock ordering

The classic failure: two threads acquire two locks in opposite orders and each waits for the other forever. Intrinsic locks give no timeout and no way to back out, so once deadlocked the threads are stuck until the process dies. The standard prevention is a global lock ordering: whenever code must hold more than one lock, always acquire them in the same canonical order everywhere. If you need timeouts or the ability to abandon acquisition, that is precisely when to switch from synchronized to ReentrantLock.tryLock().

synchronized and virtual threads

On JDK 21, a virtual thread that blocked inside a synchronized block pinned its carrier platform thread — it could not unmount, undermining scalability under heavy locking. Later JDK releases addressed much of this so that synchronized no longer pins in the common cases. If you run large numbers of virtual threads that contend on intrinsic locks, check your JDK version; on older 21 builds, replacing hot synchronized blocks with ReentrantLock avoided the pinning. For most code, synchronized remains the correct, simplest choice.

synchronized acquires an object's intrinsic monitor and auto-releases it, giving mutual exclusion plus happens-before visibility. Always know which object you lock — instance methods lock this, statics lock the Class — and prefer a private final lock object to keep the lock encapsulated. Keep critical sections short, enforce a global lock order to avoid deadlock, and reach for ReentrantLock only when you need timeouts, interruptibility, or multiple conditions.