PART 3 · CLASS 18
Atomic Variables and AtomicInteger
We have learned about race conditions, synchronization and the volatile keyword. Now let's understand another useful way to safely update shared data in Java.
1. A Quick Look at What We Have Learned
Imagine two threads are updating the same counter. Each thread wants to increase the counter by one.
We learned that this simple operation:
count++;
is not a single indivisible operation. It involves reading the current value, increasing it and writing the new value. Two threads can interfere with each other and produce an incorrect result.
We also learned about the volatile keyword in our previous class.
Volatile helps threads see the latest value of a shared
variable. However, it does not make operations like
count++ atomic.
So, how can we safely increase a shared counter without manually writing a synchronized block?
2. What Are Atomic Variables?
Java provides a set of classes called atomic classes.
These classes help us perform certain operations on shared variables safely when multiple threads are working with them.
An atomic operation happens as one indivisible operation from the perspective of other threads.
In simple words, when an operation is atomic, another thread cannot interfere halfway through that operation.
Java provides different atomic classes for different types of values. One commonly used class is AtomicInteger.
3. Introduction to AtomicInteger
AtomicInteger is a Java class that allows us
to perform thread-safe operations on an integer value.
It belongs to the java.util.concurrent.atomic
package.
Let's see how we can create an AtomicInteger.
import java.util.concurrent.atomic.AtomicInteger;
public class Main {
public static void main(String[] args) {
AtomicInteger count = new AtomicInteger(0);
System.out.println(count.get());
}
}
Here, we created an AtomicInteger with an initial value of
0.
Notice that we use get() to read its current
value.
4. Incrementing an AtomicInteger
With a normal integer, we use count++ to
increase its value.
With AtomicInteger, we can use a method called incrementAndGet().
import java.util.concurrent.atomic.AtomicInteger;
public class Main {
public static void main(String[] args) {
AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet();
System.out.println(count.get());
}
}
The output will be:
1
The incrementAndGet() method safely increases
the value by one and returns the updated value.
We can call it multiple times:
count.incrementAndGet();
count.incrementAndGet();
count.incrementAndGet();
System.out.println(count.get());
The output will be 3.
There is also a method called getAndIncrement().
It increases the value by one but returns the old value
instead of the updated value.
For now, remember incrementAndGet() as the
method we can use when we want to safely increment an
AtomicInteger.
5. Let's Solve Our Counter Problem
Remember our earlier example where two threads were incrementing the same counter?
Let's solve that problem using AtomicInteger.
import java.util.concurrent.atomic.AtomicInteger;
class Counter {
private final AtomicInteger count = new AtomicInteger(0);
void increment() {
count.incrementAndGet();
}
int getCount() {
return count.get();
}
}
public class Main {
public static void main(String[] args)
throws InterruptedException {
Counter counter = new Counter();
Runnable task = () -> {
for (int i = 0; i < 100000; i++) {
counter.increment();
}
};
Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(counter.getCount());
}
}
Let's understand what is happening here.
- We created one shared Counter object.
- Inside it, we created an AtomicInteger starting at zero.
- Both threads use the same Counter object.
- Each thread increments the counter 100,000 times.
- We use
join()to wait for both threads to finish. - Finally, we print the counter value.
The expected output is:
200000
Both threads can safely call incrementAndGet().
The atomic operation prevents the lost updates that we
encountered with an ordinary count++.
6. AtomicInteger vs. volatile vs. synchronized
We have now learned three useful ways of dealing with shared data. Let's quickly compare them.
volatile
Helps with visibility and ordering of changes to a shared
variable. However, it does not make compound operations
like count++ atomic.
synchronized
Allows us to protect a section of code using a lock. Only one thread at a time can execute code protected by the same lock.
AtomicInteger
Provides thread-safe atomic operations on an integer.
For example, we can safely increment a shared counter
using incrementAndGet() without writing
an explicit synchronized block.
One important thing to remember: AtomicInteger makes its individual atomic operations safe. It does not automatically make an entire sequence of multiple operations thread-safe.
Test Your Understanding
Let's see how well you understood this class!
Question 1: What is the main purpose of AtomicInteger?
Question 2: Which package contains AtomicInteger?
Question 3: What does incrementAndGet() do?
Question 4: What would be the expected result if two threads each increment an AtomicInteger 100,000 times?
Question 5: Does AtomicInteger automatically make an entire multi-step workflow thread-safe?
Class Summary
Let's quickly revise what we learned today:
- Atomic variables help us perform certain shared-data operations safely.
- AtomicInteger is used for thread-safe operations on integer values.
get()reads the current value.incrementAndGet()safely increments and returns the updated value.- AtomicInteger helps prevent lost updates when multiple threads increment a shared counter.
- AtomicInteger does not automatically protect an entire multi-step workflow.
Remember: choose the right tool for the problem. Sometimes AtomicInteger is enough. Sometimes we need synchronized blocks or other coordination mechanisms.
You have completed 18 out of 46 classes.