Understanding Atomic Variables in Java
- Details
- Written by Nam Ha Minh
- Last Updated on 13 August 2019   |   Print Email
incrementAndGet(): Atomically increments by one the current value.
decrementAndGet(): Atomically decrements by one the current value.
These operations are guaranteed to execute atomically using machine-level instructions on modern processors.Using atomic variables help avoiding the overhead of synchronization on a single primitive variable, so it is more efficient than using synchronization/locking mechanism.To understand clearly, let’s walk through an example.public class Counter { private int value; public void increment() { value++; } public void decrement() { value--; } public int get() { return value; } }This class has two methods that update the value of an int variable, and a method to get the value.Next, suppose we have a thread class that updates a shared instance of Counter like this:
public class UpdateThread extends Thread { private Counter counter; public UpdateThread(Counter counter) { this.counter = counter; } public void run() { try { Thread.sleep(100); } catch (InterruptedException ex) { ex.printStackTrace(); } counter.increment(); } }As you can see, this thread simply increases the value of the counter variable after sleeping for a short time (100 milliseconds).And here’s the test program that runs 100 threads that concurrently update a shared instance of Counter:
public class ThreadsTest { static final int NUMBER_THREADS = 100; public static void main(String[] args) throws InterruptedException { Counter counter = new Counter(); System.out.println("Initial Counter = " + counter.get()); UpdateThread[] threads = new UpdateThread[NUMBER_THREADS]; for (int i = 0; i < NUMBER_THREADS; i++) { threads[i] = new UpdateThread(counter); threads[i].start(); } for (int i = 0; i < NUMBER_THREADS; i++) { threads[i].join(); } System.out.println("Final Counter = " + counter.get()); } }Let’s do a simple math. There are 100 threads, each increase the counter by one, so eventually the final value of the counter variable must be 100, right?Now, try to compile and run this test program. You will see that sometimes it prints correct result:
Initial Counter = 0 Final Counter = 100But more than one time, it prints incorrect result:
Initial Counter = 0 Final Counter = 96The result is inconsistent. You can easily figure out why this happens, because the increment() method is executed by multiple threads concurrently without any synchronization or locking.We can fix the problem by adding synchronization for the Counter class like this:
public class Counter { private int value; public synchronized void increment() { value++; } public synchronized void decrement() { value--; } public synchronized int get() { return value; } }or using explicit locking mechanism like this:
import java.util.concurrent.locks.*; public class Counter { private int value; private Lock lock = new ReentrantLock(); public void increment() { lock.lock(); value++; lock.unlock(); } public void decrement() { lock.lock(); value--; lock.unlock(); } public synchronized int get() { return value; } }Now recompile and run the test program again for at least 10 times, you will realize that the problem has gone, proved by the consistent output:
Initial Counter = 0 Final Counter = 100However, synchronization/locking comes at the cost of slow performance as it requires resources and thread scheduler to monitor the lock.Therefore, atomic variable is a good alternative to synchronization on a single primitive type as mentioned earlier, atomic variable uses machine-level instructions to guarantee atomicity.For example, you can use the AtomicInteger class to replace the int primitive type in the Counter class like this:
import java.util.concurrent.atomic.*; public class Counter { private AtomicInteger value = new AtomicInteger(); public void increment() { value.incrementAndGet(); } public void decrement() { value.decrementAndGet(); } public int get() { return value.get(); } }Here, the methods incrementAndGet() and decrementAndGet() guarantee to execute atomically, which means that they are safely executed by multiple threads.Now recompile and run the test program again, you will observe the same result as using synchronization/locking with better performance though it’s hard to see the difference with this simple example. At least you got the idea, right?In addition to increment/decrement methods, the AtomicInteger and AtomicLong classes provide other atomic methods such as:
- addAndGet(int delta): Atomically adds the given value to the current value.
- compareAndSet(int expect, int update): Atomically sets the value to the given updated value if the current value == the expected value.
- getAndAdd(int delta): Atomically adds the given value to the current value.
- set(int newValue): Sets to the given value.
- AtomicIntegerArray
- AtomicLongArray
- AtomicReference
- AtomicReferenceArray
API References:
Other Java Concurrency Tutorials:
- How to use Threads in Java (create, start, pause, interrupt and join)
- Understanding Deadlock, Livelock and Starvation with Code Examples in Java
- Understanding Java Fork-Join Framework with Examples
- Java Synchronization Tutorial
- Understand Thread Pool and Executors
Comments