What is Synchronization?
Imagine a shared bathroom in a house with multiple people. If two people try to use it at the same time, chaos! The solution? A lock on the door. Only one person can use it at a time. Synchronization works the same way in Java - it controls access to shared resources so only one thread can use them at a time.
Real-World Analogy: ATM Machine
You're at an ATM checking your balance ($1000). While you're deciding how much to withdraw, your spouse at another ATM also sees $1000 and starts withdrawing. You both withdraw $600...
Without synchronization: Both see $1000, both withdraw $600 → account becomes -$200 (overdraft!)
With synchronization: One person completes their transaction first (balance → $400), then the other sees $400 and gets "Insufficient funds"
The Problem: Race Condition
A race condition occurs when multiple threads try to modify shared data simultaneously, leading to unpredictable results.
class Counter {
private int count = 0;
public void increment() {
count++; // Looks simple, but NOT atomic!
}
}
// What count++ actually does (3 steps):
1. Read current value of count
2. Add 1 to it
3. Write new value back to count
// Race condition scenario:
Counter counter = new Counter();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) counter.increment();
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) counter.increment();
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(counter.count);
// Expected: 2000
// Actual: 1847, 1923, 1956... (varies every run!)
// WHY? Threads interfere with each other:
// T1 reads: 100 | T2 reads: 100
// T1 adds 1: 101 | T2 adds 1: 101
// T1 writes: 101 | T2 writes: 101
// Result: 101 instead of 102! One increment lost!
Solution 1: Synchronized Methods
The synchronized keyword ensures only ONE thread can execute the method at a time.
class SafeCounter {
private int count = 0;
public synchronized void increment() {
count++; // Now thread-safe!
}
public synchronized int getCount() {
return count;
}
}
// Now it always returns 2000!
How Synchronized Works
Every object in Java has an invisible LOCK (monitor)
┌─────────────────┐
│ Counter Object │ ← Has a lock
│ count = 0 │
│ 🔒 LOCK │
└─────────────────┘
Step-by-step execution:
1. Thread A calls increment() → Tries to acquire lock
2. Lock is available → Thread A gets it 🔒✅
3. Thread A executes count++ safely
4. Thread B calls increment() → Tries to acquire lock
5. Lock is HELD by A → Thread B waits ⏳
6. Thread A finishes → Releases lock 🔓
7. Thread B acquires lock 🔒✅ → Executes count++
Complete Bank Account Example
class BankAccount {
private int balance = 1000;
public synchronized void withdraw(int amount) {
String thread = Thread.currentThread().getName();
System.out.println(thread + ": Checking balance...");
if (balance >= amount) {
System.out.println(thread + ": Sufficient funds");
try { Thread.sleep(100); } catch (InterruptedException e) {}
balance -= amount;
System.out.println(thread + ": Withdrew $" + amount);
System.out.println(thread + ": New balance: $" + balance);
} else {
System.out.println(thread + ": Insufficient funds!");
}
}
public synchronized void deposit(int amount) {
balance += amount;
System.out.println("Deposited $" + amount + ", Balance: $" + balance);
}
}
public class BankTest {
public static void main(String[] args) throws InterruptedException {
BankAccount account = new BankAccount();
Thread person1 = new Thread(() -> account.withdraw(600), "Person1");
Thread person2 = new Thread(() -> account.withdraw(600), "Person2");
person1.start();
person2.start();
person1.join();
person2.join();
}
}
Output (synchronized - safe):
Person1: Checking balance...
Person1: Sufficient funds
Person1: Withdrew $600
Person1: New balance: $400
Person2: Checking balance...
Person2: Insufficient funds!
Solution 2: Synchronized Blocks
Sometimes you only need to synchronize part of a method, not the whole thing. Use synchronized blocks for better performance.
Syntax
public void myMethod() {
// This code can run concurrently
doSomeWork();
synchronized(this) {
// Only THIS part is synchronized
criticalSection();
}
// This code can run concurrently
doMoreWork();
}
Example: Synchronized Block
class Logger {
private List<String> logs = new ArrayList<>();
public void log(String message) {
// Formatting doesn't need synchronization
String formatted = LocalDateTime.now() + ": " + message;
// Only synchronize the critical part
synchronized(this) {
logs.add(formatted); // Shared resource - needs lock
}
}
}
Synchronizing on Different Objects
class BankAccount {
private int balance = 1000;
private final Object balanceLock = new Object();
private List<String> transactions = new ArrayList<>();
private final Object transactionLock = new Object();
public void withdraw(int amount) {
synchronized(balanceLock) {
balance -= amount;
}
}
public void addTransaction(String transaction) {
synchronized(transactionLock) {
transactions.add(transaction);
}
}
}
// Benefits: withdraw() and addTransaction() can run concurrently!
// They use different locks
Solution 3: Volatile Keyword
Use volatile for simple flags that are read/written by multiple threads.
The Problem: Caching
class TaskRunner {
private boolean running = true; // Problem: might be cached!
public void run() {
while (running) { // Thread might cache this value
doWork();
}
}
public void stop() {
running = false; // Other thread might not see this change!
}
}
The Solution: Volatile
class TaskRunner {
private volatile boolean running = true; // Always read from main memory
public void run() {
while (running) { // Always sees latest value
doWork();
}
}
public void stop() {
running = false; // Immediately visible to all threads
}
}
// volatile ensures:
✅ Changes are immediately visible to all threads
✅ No caching in thread-local memory
❌ Does NOT provide atomicity (use synchronized for that)
When to Use Volatile
- ✅ Simple flags (boolean running, boolean done)
- ✅ Single variable read/write
- ✅ Status indicators
- ❌ NOT for compound operations (count++)
- ❌ NOT when multiple variables need consistency
Solution 4: Atomic Classes
Java provides atomic classes for lock-free thread-safe operations.
AtomicInteger Example
import java.util.concurrent.atomic.*;
class AtomicCounter {
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet(); // Atomic operation - thread-safe!
}
public int getCount() {
return count.get();
}
}
// No need for synchronized - AtomicInteger handles it!
Common Atomic Classes
AtomicInteger atomicInt = new AtomicInteger(0);
atomicInt.incrementAndGet(); // ++count
atomicInt.decrementAndGet(); // --count
atomicInt.addAndGet(5); // count += 5
atomicInt.compareAndSet(10, 20); // if(count==10) count=20
AtomicLong atomicLong = new AtomicLong(0L);
AtomicBoolean atomicBool = new AtomicBoolean(false);
AtomicReference<String> atomicRef = new AtomicReference<>("initial");
Solution 5: ReentrantLock - Advanced Locking
More flexible than synchronized, with tryLock(), timed locks, and interruptible locks.
import java.util.concurrent.locks.*;
class BankAccountWithLock {
private int balance = 1000;
private final Lock lock = new ReentrantLock();
public void withdraw(int amount) {
lock.lock(); // Acquire lock
try {
if (balance >= amount) {
balance -= amount;
System.out.println("Withdrew $" + amount);
}
} finally {
lock.unlock(); // ALWAYS unlock in finally!
}
}
public boolean tryWithdraw(int amount) {
if (lock.tryLock()) { // Try to get lock, don't wait
try {
if (balance >= amount) {
balance -= amount;
return true;
}
} finally {
lock.unlock();
}
}
return false; // Couldn't get lock
}
}
ReentrantLock Features
tryLock()- Try to acquire lock, return immediatelytryLock(timeout, unit)- Wait for specified timelockInterruptibly()- Can be interrupted while waitingisLocked()- Check if lock is heldgetHoldCount()- How many times current thread holds lock
Comparison: Synchronized vs Lock vs Atomic
| Feature | synchronized | ReentrantLock | Atomic Classes |
|---|---|---|---|
| Ease of Use | Very easy | More complex | Very easy |
| Performance | Good | Good | Best (lock-free) |
| Try Lock | ❌ No | ✅ Yes | N/A |
| Timeout | ❌ No | ✅ Yes | N/A |
| Fairness | No guarantee | ✅ Optional | N/A |
| Use Case | General purpose | Advanced control | Single variables |
Common Pitfalls & Best Practices
❌ Deadlock Example
Object lock1 = new Object();
Object lock2 = new Object();
// Thread 1
synchronized(lock1) {
Thread.sleep(100);
synchronized(lock2) { // Waits for lock2
doWork();
}
}
// Thread 2
synchronized(lock2) {
Thread.sleep(100);
synchronized(lock1) { // Waits for lock1
doWork();
}
}
// DEADLOCK! Both threads wait forever
✅ Avoiding Deadlock
// Always acquire locks in same order!
synchronized(lock1) {
synchronized(lock2) {
doWork();
}
}
- ✅ Keep synchronized blocks small - minimize lock time
- ✅ Use specific locks for specific resources
- ✅ Always acquire locks in same order (prevent deadlock)
- ✅ Use concurrent collections (ConcurrentHashMap, CopyOnWriteArrayList)
- ✅ Prefer atomic classes for simple counters/flags
- ✅ Use volatile for simple shared flags
- ✅ Document synchronization strategy in comments
- ❌ Don't synchronize on public objects (use private locks)
- ❌ Don't synchronize on String literals (they're interned!)
- ❌ Don't call unknown methods inside synchronized blocks
Why It Matters
- Correctness: Prevents data corruption from race conditions
- Reliability: Programs produce consistent, predictable results
- Safety: Critical for financial, medical, and mission-critical systems
- Professional Skill: Essential for multi-threaded applications
Synchronization is like traffic lights - it prevents collisions but adds overhead. Use it when needed, but don't over-synchronize!