Skip to content
Dev Dump

Synchronization in Java

“Synchronization ensures that shared resources are accessed by only one thread at a time, preventing data inconsistency and race conditions.

  • In multithreading, multiple threads can access shared resources (like variables, objects, files).

  • Without synchronization → threads may interfere → causes race conditions.

  • Ensures one thread at a time executes a critical section.

  • Prevents data inconsistency.

  • Achieved using locks/monitors.

Can we use Volatile instead of Synchronization
Section titled “Can we use Volatile instead of Synchronization”

“Volatile ensures visibility of changes across threads but doesn’t provide atomicity. It’s useful for simple flags but insufficient for compound operations.”

“Race conditions are the most insidious bugs in concurrent programming - they’re intermittent, hard to reproduce, and often appear only under load.”

class Counter {
private int count = 0;
public void increment() { count++; }
public int getCount() { return count; }
}

Two threads calling increment() simultaneously can cause incorrect count output due to the non-atomic nature of the increment operation.

class RaceConditionExample {
private int counter = 0;
public void increment() { counter++; }
public int getCounter() { return counter; }
public static void main(String[] args) throws InterruptedException {
RaceConditionExample obj = new RaceConditionExample();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) obj.increment();
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) obj.increment();
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("Final: " + obj.getCounter()); // Often < 2000
}
}

✅ Expected = 2000
❌ Actual = some random number (due to race condition).

Important Note: Race conditions may not occur every time - their occurrence depends on how the operating system schedules thread execution on the CPU. This unpredictability makes them particularly dangerous.

7ba6b0ae9c42a9c4ab034e4118713e33_MD5

“The synchronized keyword provides a simple and effective way to achieve thread safety, but it comes with performance overhead.”

public synchronized void increment() {
count++;
}
public void increment() {
synchronized(this) {
count++;
}
}
public static synchronized void increment() {
count++;
}
  • If method is static, it uses class-level lock (lock on Class object).
    • Only one thread per class can execute this method at a time.

fcb3a223d98041efd212eee2aea832bf_MD5

class Counter {
private int count = 0;
public synchronized void increment() { count++; }
public int getCount() { return count; }
}
  • A thread already holding a lock can acquire it again.

  • Example: One synchronized method calling another synchronized method of same object → no deadlock.

class ReentrantExample {
public synchronized void method1() {
System.out.println("Inside method1");
method2(); // re-enter same lock
}
public synchronized void method2() {
System.out.println("Inside method2");
}
}
  • (wait(), notify(), notifyAll())

  • Used to coordinate threads.

  • These must be used inside a synchronized block.

👉 Example (Producer-Consumer):

class SharedResource {
private int data;
private boolean available = false;
public synchronized void produce(int value) throws InterruptedException {
while (available) wait(); // wait if data is already available
data = value;
available = true;
System.out.println("Produced: " + value);
notify(); // wake up consumer
}
public synchronized int consume() throws InterruptedException {
while (!available) wait(); // wait until data is available
available = false;
System.out.println("Consumed: " + data);
notify(); // wake up producer
return data;
}
}
  • If multiple threads wait forever for each other’s lock → deadlock.
class DeadlockExample {
public static void main(String[] args) {
final String lock1 = "Lock1";
final String lock2 = "Lock2";
Thread t1 = new Thread(() -> {
synchronized (lock1) {
System.out.println("Thread 1: Holding lock1...");
try { Thread.sleep(100); } catch (Exception e) {}
synchronized (lock2) {
System.out.println("Thread 1: Holding lock1 & lock2...");
}
}
});
Thread t2 = new Thread(() -> {
synchronized (lock2) {
System.out.println("Thread 2: Holding lock2...");
try { Thread.sleep(100); } catch (Exception e) {}
synchronized (lock1) {
System.out.println("Thread 2: Holding lock2 & lock1...");
}
}
});
t1.start();
t2.start();
}
}
  1. Choose the Right Tool: Use synchronized for simple cases, ReentrantLock for advanced scenarios, and ReadWriteLock for read-heavy workloads.

  2. Minimize Lock Scope: Keep synchronized blocks as small as possible to reduce contention.

  3. Avoid Nested Locks: Prevent deadlocks by acquiring locks in a consistent order.

  4. Use Volatile Wisely: Only for simple flags and variables that don’t require atomicity.

  5. Consider Performance: Profile your application to understand the impact of synchronization on performance.

  • Over-synchronization: Synchronizing methods that don’t need it
  • Under-synchronization: Missing synchronization on shared data
  • Lock Ordering: Inconsistent lock acquisition order leading to deadlocks
  • Memory Visibility: Forgetting that volatile doesn’t provide atomicity

Remember: Synchronization is essential for thread safety, but it comes with performance costs. Always measure and profile to ensure you’re using the right approach for your specific use case.