Concurrency Basics

The synchronized Keyword

15 min Lesson 5 of 13

The synchronized Keyword

In the previous lesson you saw how race conditions arise when multiple threads read and write shared state without coordination. Java's oldest and most fundamental answer to that problem is the synchronized keyword. Understanding it deeply — not just the syntax, but the memory model guarantees and the performance trade-offs — is essential before you reach for higher-level concurrency utilities.

The Monitor: Java's Built-In Lock

Every Java object carries a hidden field called a monitor (also called an intrinsic lock or object lock). You cannot see this field in source code; the JVM manages it. The monitor enforces two things simultaneously:

  • Mutual exclusion — at most one thread can hold the monitor at any given time. Every other thread that tries to enter a synchronized section on that same monitor is blocked until the holder releases it.
  • Memory visibility — when a thread releases a monitor, all writes it made are flushed to main memory. When a thread acquires the same monitor next, it sees those writes. This makes synchronized a happens-before boundary.
One monitor per object. The monitor belongs to the object (or, for static members, to the Class object). Two synchronized methods on the same instance share the same monitor and therefore cannot run concurrently. Two methods on different instances have separate monitors and can run concurrently.

Synchronized Methods

Add synchronized to a method declaration and the JVM automatically acquires this object's monitor on entry and releases it on exit — whether the method returns normally or throws an exception.

public class SafeCounter { private int count = 0; public synchronized void increment() { count++; // read-modify-write is now atomic } public synchronized int getCount() { return count; // the read also needs synchronization } }

Without synchronized, count++ compiles to three bytecode instructions (read, add, write). A context switch between any two of them on a different thread causes a lost update — the classic race condition. Making both methods synchronized means only one thread can be inside either method at once.

Don't forget the getter. A common mistake is synchronizing only the write method. If the getter runs unsynchronized, the JVM is free to cache the value in a register and never re-read from main memory. The thread sees a stale value even though the writer has already updated it. Reads of shared mutable state must also be synchronized.

Synchronized Blocks: Finer Granularity

A synchronized method locks this for its entire duration. A synchronized block lets you choose which object to lock and how much code to protect:

public class BankAccount { private final Object balanceLock = new Object(); // dedicated lock object private double balance; private String lastAuditNote; // unrelated state — no lock needed public void deposit(double amount) { // expensive validation — runs without holding any lock if (amount <= 0) throw new IllegalArgumentException("Must be positive"); synchronized (balanceLock) { balance += amount; // only this line needs the lock } } public double getBalance() { synchronized (balanceLock) { return balance; } } public void updateAuditNote(String note) { // no shared mutable state here — no lock needed this.lastAuditNote = note; // safe only if only one thread ever writes this } }

By locking on a private final Object rather than this, you make the lock invisible to callers. If you lock on this, external code can also synchronize on the same object and potentially cause unexpected contention or deadlock.

Prefer private final lock objects. Use private final Object lock = new Object(); rather than locking on this or on the class. It prevents caller interference and makes the locking strategy explicit and readable.

Static Synchronized Methods

When a method is both static and synchronized, the monitor used is the Class object, not any instance. That lock is shared across all instances of the class.

public class IdGenerator { private static long nextId = 0; public static synchronized long generate() { return ++nextId; } }

Locking on the Class object is coarser than locking on an instance. Be careful not to mix static and instance synchronization if they protect the same state — they use different monitors and provide no mutual exclusion between them.

Reentrancy

Java's monitors are reentrant: a thread that already holds a lock can re-acquire it without blocking. This prevents a thread from deadlocking itself when a synchronized method calls another synchronized method on the same object.

public class Node { private int value; public synchronized void set(int v) { value = v; audit(); // calls another synchronized method on the same object } public synchronized void audit() { System.out.println("value is now " + value); } }

When set calls audit, the same thread re-enters the monitor it already owns. The JVM maintains a per-thread entry count; the lock is released only when the count drops to zero.

Performance and Trade-offs

Every synchronized entry point is a potential bottleneck: all threads compete for the same lock and only one proceeds at a time. Consider these strategies to reduce contention:

  • Keep critical sections short. Move I/O, network calls, or heavy computation outside the synchronized block.
  • Use separate locks for independent state. If two fields are never accessed together, protect them with two distinct lock objects so threads can work on them in parallel.
  • Consider read/write locking. java.util.concurrent.locks.ReentrantReadWriteLock allows many concurrent readers when no writer is active — a win for read-heavy workloads.
  • Consider atomic variables. For single-variable counters, AtomicLong or LongAdder offer non-blocking throughput superior to synchronized.
Modern JVM lock optimizations. The HotSpot JVM applies biased locking, thin locks, and lock coarsening automatically. In practice, uncontended synchronized blocks are very cheap — often just a few nanoseconds. Contention is the real cost; reduce it by shrinking critical sections and splitting locks.

Summary

The synchronized keyword is Java's foundational mutual-exclusion primitive. Every object has a monitor; acquiring it guarantees both exclusion and memory visibility. Synchronized methods lock this; synchronized blocks let you choose the lock object and the exact scope. Prefer private lock objects, keep critical sections as narrow as possible, and remember that every synchronized read is just as necessary as every synchronized write. In the next lesson we will look at volatile — a lighter tool that provides visibility without exclusion.