Java Synchronization and Thread Safety Tutorial with Examples

Introduction

Java is a multi-threaded programming language that allows multiple threads to run concurrently. While this concurrency improves performance, it also introduces challenges such as race conditions and inconsistent data. Synchronization in Java ensures that only one thread can access a critical section of code at a time, preventing data corruption and inconsistencies.

This tutorial explores Java synchronization, different synchronization techniques, and best practices for achieving thread safety with practical examples.

Understanding Thread Safety

Thread safety means that multiple threads can access shared resources concurrently without leading to inconsistent data or unexpected behavior. If a class or method is thread-safe, it can be accessed by multiple threads without causing race conditions.

Example of a Race Condition

A race condition occurs when multiple threads attempt to modify the same shared resource simultaneously.

class Counter {
    private int count = 0;

    public void increment() {
        count++; // Not thread-safe
    }

    public int getCount() {
        return count;
    }
}

public class RaceConditionExample {
    public static void main(String[] args) {
        Counter counter = new Counter();

        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();
        
        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("Final count: " + counter.getCount()); // Expected 2000, but result is unpredictable
    }
}

In the example above, multiple threads increment the counter simultaneously, leading to inconsistent results.

Synchronization in Java

Synchronization prevents multiple threads from executing critical sections of code simultaneously, ensuring data consistency.

1. Synchronized Methods

One way to ensure thread safety is by declaring a method as synchronized. This ensures that only one thread at a time can execute the method.

class SynchronizedCounter {
    private int count = 0;

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

    public synchronized int getCount() {
        return count;
    }
}

Example:

public class SynchronizedExample {
    public static void main(String[] args) {
        SynchronizedCounter counter = new SynchronizedCounter();

        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();
        
        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("Final count: " + counter.getCount()); // Always 2000
    }
}

2. Synchronized Blocks

Instead of synchronizing an entire method, we can synchronize only the critical section using a synchronized block.

class BlockSynchronizedCounter {
    private int count = 0;
    private final Object lock = new Object();

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

    public int getCount() {
        synchronized (lock) {
            return count;
        }
    }
}

This approach improves performance by reducing the scope of synchronization.

3. Using ReentrantLock

ReentrantLock provides more flexibility compared to synchronized blocks.

import java.util.concurrent.locks.ReentrantLock;

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

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

    public int getCount() {
        return count;
    }
}

4. Using Atomic Variables

Java provides atomic classes such as AtomicInteger that allow atomic operations without explicit synchronization.

import java.util.concurrent.atomic.AtomicInteger;

class AtomicCounter {
    private AtomicInteger count = new AtomicInteger(0);

    public void increment() {
        count.incrementAndGet();
    }

    public int getCount() {
        return count.get();
    }
}

5. Using Volatile (Not a Thread-Safety Mechanism)

The volatile keyword ensures visibility of changes to variables across threads but does not provide atomicity.

class VolatileExample {
    private volatile int count = 0;

    public void increment() {
        count++; // Still not thread-safe
    }

    public int getCount() {
        return count;
    }
}

Best Practices for Thread Safety

  1. Minimize the use of synchronization – Use synchronized blocks instead of synchronizing entire methods.
  2. Use thread-safe classes – Prefer ConcurrentHashMap, CopyOnWriteArrayList, etc.
  3. Use atomic variables – Use AtomicInteger, AtomicBoolean, etc.
  4. Avoid unnecessary locking – Prefer non-blocking algorithms where possible.
  5. Use thread pools – Managing threads using ExecutorService improves efficiency.
  6. Use immutability – Immutable objects are inherently thread-safe.

Conclusion

Synchronization is crucial in Java for ensuring thread safety, preventing race conditions, and maintaining data consistency. By using synchronized methods, synchronized blocks, ReentrantLock, and atomic variables, we can write efficient and thread-safe programs. However, overuse of synchronization can lead to performance bottlenecks, so it should be applied wisely.

Understanding and implementing these synchronization techniques effectively will help you build robust, concurrent applications in Java.

Further Reading:

  • Java Concurrency in Practice by Brian Goetz
  • Official Java Documentation on Synchronization
  • Java Executor Framework and Thread Pooling

Related articles

AWS Interview Questions 2025

AWS Interview Questions 2025 Basic AWS Concepts What is AWS, and how does it benefit businesses? Define cloud computing...

How to use sql aws and gis​

  How to Use SQL, AWS, and GIS Combining SQL, AWS, and GIS can help you manage, analyze, and visualize...

Kubernetes Container Orchestration

Kubernetes Container Orchestration Container orchestration is one of the most transformative advancements in modern software development, enabling developers to...

Backup and Restore Cloud Resources

Backup and Restore Cloud Resources Backing up and restoring cloud resources is critical for maintaining data integrity, ensuring business...