Java Tutorial
🔍

Synchronization

Synchronization

synchronized is Java's original mechanism for mutual exclusion — ensuring that only one thread at a time can execute a given piece of code that touches shared, mutable state. Every object in Java carries a built-in lock, called its intrinsic lock or monitor, and synchronized is how code acquires and releases it.

What Is Synchronization?

Synchronization is the discipline of controlling which thread is allowed to touch a piece of shared state at any given moment. synchronized is Java's built-in tool for this — every object already carries a lock, called its monitor, and wrapping code in synchronized is how a thread acquires that monitor before entering, and releases it automatically on the way out.

Why Thread Safety Matters

Without any coordination, multiple threads incrementing a shared counter can lose updates — the increment counter++ is really three separate steps (read, add one, write back), and two threads can interleave those steps in a way that drops one thread's work entirely. This exact failure mode, a race condition, is covered in full in this section's dedicated Race Conditions article; synchronized is the oldest and most direct tool for preventing it.

synchronized releases its lock automatically, whether the protected code finishes normally or an exception propagates out of it. There is no finally block to remember, unlike the explicit Lock objects covered later in this section.

1// File: BeforeSynchronization.java 2 3public class BeforeSynchronization { 4 5 static int counter = 0; 6 7 static void increment() { 8 counter++; 9 } 10 11 public static void main(String[] args) throws InterruptedException { 12 int threadCount = 4; 13 int incrementsPerThread = 100_000; 14 15 Thread[] threads = new Thread[threadCount]; 16 for (int i = 0; i < threadCount; i++) { 17 threads[i] = new Thread(() -> { 18 for (int j = 0; j < incrementsPerThread; j++) { 19 increment(); 20 } 21 }); 22 } 23 24 for (Thread t : threads) t.start(); 25 for (Thread t : threads) t.join(); 26 27 System.out.println("Counter: " + counter); 28 } 29}

This code's final count is not reliable enough to print here as a guaranteed result — running it on a real machine will often, but not always, produce a total below the mathematically expected 400,000, and the exact shortfall differs from run to run. That unpredictability is precisely the danger a race condition creates.

Marking increment() synchronized makes each call to it mutually exclusive — only one thread can be inside it at a time — which restores a guaranteed, repeatable result.

1// File: AfterSynchronization.java 2 3public class AfterSynchronization { 4 5 static int counter = 0; 6 7 static synchronized void increment() { 8 counter++; 9 } 10 11 public static void main(String[] args) throws InterruptedException { 12 int threadCount = 4; 13 int incrementsPerThread = 100_000; 14 15 Thread[] threads = new Thread[threadCount]; 16 for (int i = 0; i < threadCount; i++) { 17 threads[i] = new Thread(() -> { 18 for (int j = 0; j < incrementsPerThread; j++) { 19 increment(); 20 } 21 }); 22 } 23 24 for (Thread t : threads) t.start(); 25 for (Thread t : threads) t.join(); 26 27 System.out.println("Counter: " + counter); 28 } 29}
Output:
Counter: 400000

Every one of the 400,000 increments across all four threads is now guaranteed to be counted — not usually, but every single time this runs.

How It Works

A short sentence before the diagram: this is what happens when two threads reach the same synchronized code at nearly the same instant.

Thread A                    Thread B
   |                            |
synchronized(lock) {            |
   holds the monitor            |
   ...doing work...      synchronized(lock) {
   |                        BLOCKED - waiting for
   |                        the monitor to free up
   } <- releases lock            |
   |                             v
   |                        acquires the monitor
   |                        ...doing work...
   |                        } <- releases lock

Thread B does not busy-wait or retry — it enters the BLOCKED state, covered in this section's Thread Lifecycle article, and the JVM wakes it automatically the instant Thread A releases the monitor.

A synchronized block narrows the protected region to only the statements that actually touch shared state, and can lock on any object — commonly a dedicated, private lock field rather than this.

1// File: SynchronizedBlockExample.java 2 3public class SynchronizedBlockExample { 4 5 private final Object lock = new Object(); 6 private int balance = 0; 7 8 void deposit(int amount) { 9 synchronized (lock) { 10 balance += amount; 11 } 12 } 13 14 int getBalance() { 15 synchronized (lock) { 16 return balance; 17 } 18 } 19 20 public static void main(String[] args) throws InterruptedException { 21 SynchronizedBlockExample account = new SynchronizedBlockExample(); 22 int threadCount = 5; 23 int depositsPerThread = 20; 24 int depositAmount = 10; 25 26 Thread[] threads = new Thread[threadCount]; 27 for (int i = 0; i < threadCount; i++) { 28 threads[i] = new Thread(() -> { 29 for (int j = 0; j < depositsPerThread; j++) { 30 account.deposit(depositAmount); 31 } 32 }); 33 } 34 35 for (Thread t : threads) t.start(); 36 for (Thread t : threads) t.join(); 37 38 System.out.println("Final balance: " + account.getBalance()); 39 } 40}
Output:
Final balance: 1000

Both deposit() and getBalance() synchronize on the same lock object, so a reader can never observe a balance mid-update — five threads each depositing 20 times at 10 each account for exactly 1000, every time.

Code Examples

Protecting a shared mutable counter or accumulator, exactly as both examples above demonstrate, is the most common reason to reach for synchronized — the same idea extends to protecting a shared collection that is not itself thread-safe, making a multi-step check-then-act sequence atomic, and guarding the one-time creation of a lazily-initialized shared resource.

Java's intrinsic locks are also reentrant — a thread already holding a lock can safely call another synchronized method on the same object without blocking on itself.

1// File: ReentrancyExample.java 2 3public class ReentrancyExample { 4 5 synchronized void outer() { 6 System.out.println("In outer"); 7 inner(); 8 } 9 10 synchronized void inner() { 11 System.out.println("In inner"); 12 } 13 14 public static void main(String[] args) { 15 new ReentrancyExample().outer(); 16 } 17}
Output:
In outer
In inner

Real-World Example

A simple transfer service moves money between two bank accounts, with each account's own balance protected by its own synchronized methods.

1// File: BankAccount.java 2 3public class BankAccount { 4 private final String accountId; 5 private int balance; 6 7 public BankAccount(String accountId, int balance) { 8 this.accountId = accountId; 9 this.balance = balance; 10 } 11 12 public synchronized void withdraw(int amount) { 13 balance -= amount; 14 } 15 16 public synchronized void deposit(int amount) { 17 balance += amount; 18 } 19 20 public synchronized int getBalance() { 21 return balance; 22 } 23 24 public String getAccountId() { 25 return accountId; 26 } 27}
1// File: TransferService.java 2 3public class TransferService { 4 5 public void transfer(BankAccount from, BankAccount to, int amount) { 6 from.withdraw(amount); 7 to.deposit(amount); 8 } 9}
1// File: TransferServiceDemo.java 2 3public class TransferServiceDemo { 4 public static void main(String[] args) throws InterruptedException { 5 BankAccount alice = new BankAccount("alice", 5000); 6 BankAccount bob = new BankAccount("bob", 5000); 7 TransferService service = new TransferService(); 8 9 int threadCount = 10; 10 int transfersPerThread = 50; 11 int transferAmount = 2; 12 13 Thread[] threads = new Thread[threadCount]; 14 for (int i = 0; i < threadCount; i++) { 15 threads[i] = new Thread(() -> { 16 for (int j = 0; j < transfersPerThread; j++) { 17 service.transfer(alice, bob, transferAmount); 18 } 19 }); 20 } 21 22 for (Thread t : threads) t.start(); 23 for (Thread t : threads) t.join(); 24 25 System.out.println("Alice: " + alice.getBalance()); 26 System.out.println("Bob: " + bob.getBalance()); 27 System.out.println("Total: " + (alice.getBalance() + bob.getBalance())); 28 } 29}
Output:
Alice: 4000
Bob: 6000
Total: 10000

Ten threads each transfer ₹2, fifty times — exactly ₹1000 moves from Alice to Bob, leaving the combined total unchanged at ₹10000. A mistake that appears often in fresher pull requests is assuming that synchronizing the individual withdraw() and deposit() calls, as BankAccount does here, makes transfer() atomic as a whole — it does not, since another thread could observe Alice's balance already decreased but Bob's not yet increased between the two calls. For this demo's final-totals check that gap does not matter, but a real transfer service checking "sufficient funds" before withdrawing would need to synchronize across both accounts together, or use a higher-level lock, to avoid a lost check-then-act race.

Best Practices

Synchronize on a private, final lock object rather than this whenever the object is otherwise accessible to outside code, so external code cannot accidentally lock on — and interfere with — your object's own monitor.

Keep synchronized blocks as short as possible, covering only the statements that actually touch shared state, rather than wrapping an entire large method unnecessarily.

Reach for a higher-level utility from java.util.concurrent — covered later in this section — when richer behavior is needed, such as a timeout on lock acquisition or a fairness guarantee, rather than building it from raw synchronized.

Never call an overridable or otherwise unpredictable method while holding a lock, since that method could itself try to acquire a lock and introduce a deadlock risk, covered in this section's dedicated Deadlock article.

Common Mistakes

Synchronizing two methods on two different objects does not protect a piece of state they both share — each synchronized instance method locks only on this, so calls through different objects use different locks entirely and can still race on shared state, such as a static field, with no mutual exclusion at all between them.

Avoiding a call from one synchronized method to another synchronized method on the same object, out of a mistaken fear that it will deadlock the calling thread against itself, overlooks that Java's intrinsic locks are reentrant — demonstrated in the ReentrancyExample above, where outer() calls inner() on the same object without blocking.

Interview Questions

Q1. What does the synchronized keyword actually guarantee?

Mutual exclusion — only one thread can execute a given synchronized method or block on a given object's monitor at a time — along with a happens-before relationship that makes changes made inside the block visible to the next thread that acquires the same lock. Interviewers listening carefully want both halves of this answer, not just "one thread at a time."

Q2. What is the difference between synchronizing an instance method and a static method?

A synchronized instance method locks on this, the specific object instance it was called on. A synchronized static method locks on the Class object itself, which is a different lock entirely — a static and an instance synchronized method on the same class do not exclude each other at all. This distinction is a frequent product-company follow-up after the basic definition.

Q3. Is Java's intrinsic lock reentrant?

Yes. A thread that already holds a given object's lock can acquire it again without blocking, exactly as demonstrated in this article's ReentrancyExample — this is what lets one synchronized method safely call another synchronized method on the same object.

Q4. What object does synchronized(this) lock on inside an instance method?

The specific object instance the method was invoked on — the same lock a plain synchronized instance method declaration would use implicitly.

Q5. Why is synchronizing on a private final Object field usually preferred over synchronizing on this?

Because this is often accessible to code outside the class, which could synchronize on the same object for an unrelated reason and unintentionally interfere with your class's own locking — a private, dedicated lock field, as SynchronizedBlockExample uses above, keeps the lock entirely under the class's own control.

Q6. Does a synchronized block release its lock if an exception is thrown inside it?

Yes, always. Exiting a synchronized block — whether normally or because an exception propagated out of it — unconditionally releases the lock, the same guarantee a finally block provides for cleanup code. This is the nuance that distinguishes synchronized from an explicit Lock, which needs a manual finally to guarantee the same thing.

Q7. Does synchronizing two different methods on two different objects prevent a race on a shared static field?

No. Each synchronized instance method locks on its own object, so calls through two different instances use two different locks and provide no mutual exclusion at all for state, like a static field, that both instances happen to share. A fresher answering "yes" here is one of the more common wrong answers this topic produces.

FAQs

Is synchronized the only way to achieve mutual exclusion in Java?

No. java.util.concurrent.locks.ReentrantLock, covered in this section's dedicated Lock & ReentrantLock article, provides similar mutual exclusion with additional features like timed and interruptible lock acquisition.

Does synchronized guarantee fairness (first-come-first-served lock acquisition)?

No. The JVM is free to grant a newly available lock to any waiting thread, not necessarily the one that has been waiting longest — ReentrantLock offers an optional fairness mode that synchronized does not provide.

Can a synchronized method be combined with a return value?

Yes, synchronized affects only how the method is entered and exited with respect to locking — it places no restriction on the method's signature, return type, or parameters at all.

Does synchronizing a getter matter if the setter is also synchronized?

Yes, both need it. Synchronizing only the setter can still let a reading thread see a stale or inconsistently visible value, since the visibility guarantee synchronized provides depends on both the writer and the reader synchronizing on the same lock.

What happens if a thread tries to enter a synchronized block whose lock is already held by another thread?

It blocks — entering the BLOCKED state, covered in this section's Thread Lifecycle article — until the lock becomes available.

Is synchronized needed for a field that's read but never written by multiple threads?

No. A race requires at least one thread to be writing to shared state — a field that is only ever read after being fully initialized, and never modified again, needs no synchronization for those reads.

Does the JVM guarantee any particular order for threads waiting on the same lock?

No. As with the fairness question above, the JVM specification does not require any particular ordering among threads waiting to acquire the same lock — a fair ReentrantLock is the tool to reach for when ordering genuinely matters.

Summary

synchronized provides mutual exclusion through every object's built-in intrinsic lock, turning a piece of code that touches shared state into something only one thread can execute at a time — the difference between BeforeSynchronization's unreliable count and AfterSynchronization's guaranteed 400,000 in this article's opening example.

The habit worth carrying forward from this article's bank transfer example is recognizing that synchronizing individual operations does not automatically make a sequence of several operations atomic as a whole — a mistake worth watching for even after each individual piece is already correctly protected.

What to Read Next