What Is Thread Synchronization in Java and How Does It Work with Examples?

In Java, thread synchronization is one of the most important aspects of multi-threaded programming. When multiple threads access a shared resource simultaneously, they can cause inconsistent data, known as a race condition. To prevent this, Java provides synchronization mechanisms that ensure only one thread can access the critical section of code at a time.

Why Is Thread Synchronization Important?

In a multithreaded environment, different threads may try to modify the same data concurrently. This can lead to:

  • Data inconsistency
  • Unpredictable behavior
  • Crashes and logical errors

Thread synchronization ensures:

  • Atomicity of operations
  • Memory visibility
  • Thread safety

How Synchronization Works in Java

Java uses a mechanism known as a monitor or intrinsic lock to provide synchronization. Every object in Java has an associated monitor, and a thread must acquire this lock before executing any synchronized code on that object.

1. Synchronized Methods

Using the synchronized keyword on a method ensures that only one thread can execute it at a time for a given object.


class Counter {
    private int count = 0;

    public synchronized void increment() {
        count++;
    }

    public int getCount() {
        return count;
    }
}

In the above example, the increment() method is synchronized, so only one thread can access it at a time.

2. Synchronized Blocks

Instead of synchronizing the entire method, you can synchronize only a specific block of code:


class Printer {
    void printDocument(String doc) {
        synchronized(this) {
            System.out.println("Printing: " + doc);
            // simulate delay
            try { Thread.sleep(500); } catch (InterruptedException e) { }
        }
    }
}

This provides more fine-grained control and can lead to better performance.

3. Static Synchronization

When you want to synchronize a static method, the lock is on the class object rather than the instance.


class Logger {
    public static synchronized void log(String message) {
        System.out.println("Log: " + message);
    }
}

4. Real-World Example

Let’s take a real-world example where two threads try to increment the same counter object:


class SharedCounter {
    private int counter = 0;

    public void increment() {
        synchronized (this) {
            counter++;
        }
    }

    public int getValue() {
        return counter;
    }
}

public class ThreadSyncExample {
    public static void main(String[] args) throws InterruptedException {
        SharedCounter counter = new SharedCounter();

        Runnable task = () -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        };

        Thread t1 = new Thread(task);
        Thread t2 = new Thread(task);

        t1.start();
        t2.start();

        t1.join();
        t2.join();

        System.out.println("Final Counter Value: " + counter.getValue());
    }
}

Expected Output:


Final Counter Value: 2000

Without synchronization, the result would likely be less than 2000 due to race conditions.

5. Deadlock and Synchronization

Improper synchronization can lead to a deadlock, where two or more threads are waiting for each other to release locks.


class DeadlockExample {
    private final Object lock1 = new Object();
    private final Object lock2 = new Object();

    void methodA() {
        synchronized (lock1) {
            System.out.println("Thread 1 acquired lock1");
            synchronized (lock2) {
                System.out.println("Thread 1 acquired lock2");
            }
        }
    }

    void methodB() {
        synchronized (lock2) {
            System.out.println("Thread 2 acquired lock2");
            synchronized (lock1) {
                System.out.println("Thread 2 acquired lock1");
            }
        }
    }
}

To avoid deadlocks:

  • Always acquire locks in the same order
  • Use timeout and deadlock detection tools

6. Reentrant Locks (Advanced Synchronization)

Java provides ReentrantLock from the java.util.concurrent.locks package for more flexible synchronization.


import java.util.concurrent.locks.ReentrantLock;

class SafeCounter {
    private final ReentrantLock lock = new ReentrantLock();
    private int count = 0;

    public void increment() {
        lock.lock();
        try {
            count++;
        } finally {
            lock.unlock();
        }
    }

    public int getCount() {
        return count;
    }
}

Best Practices

  • Use synchronized only when needed
  • Keep synchronized blocks short
  • Use volatile for flags instead of full synchronization
  • Avoid nested synchronization when possible

Synchronization vs Volatile

The volatile keyword ensures visibility but not atomicity. Synchronization provides both. Use volatile for flags and synchronized for critical sections.

7. Java Concurrency Utilities

Java provides advanced tools for synchronization in java.util.concurrent package:

  • Semaphore
  • CountDownLatch
  • ReadWriteLock
  • CyclicBarrier

Conclusion

Thread synchronization in Java is crucial for writing correct and safe multi-threaded applications. Whether you use synchronized methods, blocks, or advanced locks, understanding synchronization will help you avoid common concurrency pitfalls like

Please follow and like us:

Leave a Comment