Open In App

Java Atomic vs Volatile vs Synchronized

Last Updated : 11 Jan, 2025
Comments
Improve
Suggest changes
Like Article
Like
Report

In Java, multithreading can lead to challenges related to thread safety and data consistency. Java provides concurrency mechanisms like Atomic, Volatile, and Synchronized to address these issues and ensure thread safety. These mechanisms offer unique advantages and limitations.

Atomic vs Volatile vs Synchronized

Features

                     SynchronizedVolatileAtomic

Applies to

It applies to only blocks or methods.It applies to variables only.It is also applicable to variables only.

Purpose

It ensures mutual exclusion and data consistency by acquiring locks.It ensures the visibility of variables across threads but does not guarantee atomicityIt provides atomic operations on variables without needing locks

Performance

Performance is relatively low compared to volatile and atomic keywords because of the acquisition and release of the lock.Performance is relatively high compared to synchronized Keyword.Performance is relatively high compared to both volatile and synchronized keyword.

Concurrency

Because of its locking nature, it is not immune to concurrency hazards such as deadlock and livelock.Because of its non-locking nature, it is immune to concurrency hazards such as deadlock and livelock.Because of its non-locking nature, it is immune to concurrency hazards such as deadlock and livelock.

Synchronized Modifier

The synchronized keyword in Java is used to control access to shared resources among multiple threads. It can be applied to methods or blocks of code. When a method or block is declared as synchronized, only one thread can execute it on a given object at a time. This ensures data consistency and prevents race conditions.

Working of Synchronized Modifier:

  • It can be applied to methods or blocks of code.
  • When a method or block is synchronized, only one thread can execute it on a given object at any time.
  • Every object in Java has an intrinsic lock associated with it. A thread must acquire this lock before entering a synchronized method or block.
  • Synchronized code blocks may lead to thread contention, which can negatively impact performance, especially with excessive synchronization.

Example:

Java
// Java program to demonstrate 
// the synchronized modifier
import java.util.concurrent.TimeUnit;

class CounterThread extends Thread {
    private int count;

    // Synchronized method to prevent race conditions
    @Override
    public synchronized void run() {
        try {
            TimeUnit.MILLISECONDS.sleep(100);  // Simulate work with sleep
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        count++; // Increment the count
    }

    public int getCount() {
        return count;
    }
}

public class Geeks {
    public static void main(String[] args) throws InterruptedException {
        CounterThread t1 = new CounterThread();
        CounterThread t2 = new CounterThread();

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

        // Expected output is 1 because only 
        // one thread can increment at a time
        System.out.println("Count: " + t1.getCount());
    }
}

Output
Count: 1

Explanation: In the above example, the synchronized method ensures that only one thread can execute the run() method at a time, preventing race conditions. Though two threads attempting to increment the count concurrently, the synchronization ensures they are executed sequentially.

Volatile Modifier

The volatile keyword in Java ensures that all threads have a consistent view of a variable's value. It prevents caching of the variable's value by threads, ensuring that updates to the variable are immediately visible to other threads.

Working of Volatile Modifier:

  • It applies only to variables.
  • volatile guarantees visibility i.e. any write to a volatile variable is immediately visible to other threads.
  • It does not guarantee atomicity, meaning operations like count++ (read-modify-write operations) can still result in inconsistent values.

Example:

Java
// Java program to demonstrate the 
// volatile modifier with non-atomic increment
class Counter {
    private volatile int count;   // Volatile variable

    public void increment() {
        count++;   // This operation is not atomic
    }

    public int getCount() {
        return count;
    }
}

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

        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                c.increment();
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                c.increment();
            }
        });

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

        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }


        System.out.println("Count: " + c.getCount());
    }
}

Output
Count: 2000

Explanation: In the above example, the volatile variable is count. While volatile ensures visibility of changes across threads and the increament operation is non-atomic, race condition can still occur. The expected result may be less than or equal to 2000.

Atomic Modifier  

Atomic classes, such as AtomicInteger, are part of the java.util.concurrent.atomic package. These classes provide thread-safe operations on variables without the need for synchronization. They use low-level atomic operations like compare-and-swap (CAS) to ensure thread safety.

Working of Atomic Modifier:

  • Atomic operations ensure atomicity of the read-modify-write actions on variables.
  • These classes are lock-free and more efficient than synchronized blocks because they avoid the overhead of acquiring locks.
  • Atomic operations are performed using methods like incrementAndGet(), compareAndSet(), and getAndSet().

Example:

Java
import java.util.concurrent.atomic.AtomicInteger;

class CounterThread extends Thread {
    private AtomicInteger count = new AtomicInteger();  // Atomic variable

    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            try {
                Thread.sleep(i * 50);  // Simulate work with sleep
                count.incrementAndGet();  // Atomic increment operation
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public int getCount() {
        return count.get();    // Get current value of count
    }
}

public class Geeks {
    public static void main(String[] args) 
      throws InterruptedException {
        CounterThread t1 = new CounterThread();
        CounterThread t2 = new CounterThread();

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

        // Expected output is 10 because 
        // each thread increments 5 times.
        System.out.println("Count: " + (t1.getCount() + t2.getCount()));
    }
}

Output
Count: 10

Explanation: In the above example, the AtomicInteger class ensures that the incrementAndGet() operation is atomic across multiple threads. Each thread increments the count 5 times, so the final output will always be 10 (5 increments per thread). 


Next Article
Practice Tags :

Similar Reads