An AtomicInteger is a number box that many threads can change at the same time without breaking it.
P.S. Tested with Java 25
Table of contents:
- The problem: a normal int breaks
- Fix it with AtomicInteger
- The everyday methods
- compareAndSet: change it only if it still matches
- updateAndGet: your own rule
- Real-life use case: order ID generator
- Common mistakes
- When to use int, when to use AtomicInteger
- References
The problem: a normal int breaks
Four threads each add 1 ten thousand times, so you should get 40000.
package com.mkyong;
import java.util.concurrent.Executors;
public class UnsafeCounter {
// A normal number shared by all threads
static int count = 0;
public static void main(String[] args) {
// Start a pool of 4 threads
try (var executor = Executors.newFixedThreadPool(4)) {
for (int i = 0; i < 4; i++) {
executor.submit(() -> {
for (int j = 0; j < 10_000; j++) {
// Looks like one step, but it is really three:
// read the number, add 1, write it back
count++;
}
});
}
} // close() waits for all threads to finish
System.out.println("Expected : 40000");
System.out.println("Got : " + count);
}
}
Output, first run:
Expected : 40000
Got : 10396
Output, second run:
Expected : 40000
Got : 12569
Output, tenth run:
Expected : 40000
Got : 12516
Never 40000, and never the same number twice.
Why does this happen?
count++ looks like one word, but the computer does three things.
count++ -> 1. read count from memory
2. add 1 to it
3. write the answer back
A thread can be paused between any two of those steps. Watch two threads work on the number 7.
Time Thread A Thread B count
---- ---------------------- ---------------------- -----
1 read count (7) 7
2 read count (7) 7
3 add 1 -> 8 7
4 add 1 -> 8 7
5 write 8 8
6 write 8 8
Two threads added 1, but the number only went up by 1. That lost +1 is called a lost update.
There is a second problem. Each CPU core keeps its own copy of the number in a fast cache, so one thread may not see what another thread just wrote. That is why 40000 turns into a random smaller number.
Fix it with AtomicInteger
Swap the normal int for an AtomicInteger.
package com.mkyong;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
public class SafeCounter {
// A safe number box, starts at 0
static AtomicInteger count = new AtomicInteger(0);
public static void main(String[] args) {
try (var executor = Executors.newFixedThreadPool(4)) {
for (int i = 0; i < 4; i++) {
executor.submit(() -> {
for (int j = 0; j < 10_000; j++) {
// Add 1 in ONE step, no one can cut in
count.incrementAndGet();
}
});
}
}
System.out.println("Expected : 40000");
System.out.println("Got : " + count.get());
}
}
Output, first run:
Expected : 40000
Got : 40000
Output, second run:
Expected : 40000
Got : 40000
Output, tenth run:
Expected : 40000
Got : 40000
Same code, same threads, and 40000 every single time. Compare that with section 1, where the same loop lost about 70% of the counts.
Why does this work?
incrementAndGet() does not read, add and write as three separate steps that anyone can cut into. It uses one CPU instruction called compare-and-swap (CAS). In plain words:
1. read the number -> 7
2. work out the answer -> 8
3. tell the CPU: "if the number is STILL 7, make it 8"
- still 7? -> done
- changed? -> throw the answer away and start again at step 1
So a thread never writes over someone else’s work. If another thread sneaked in, the write is refused and this thread simply tries again. The loop is inside the JDK, you do not write it.
The value inside is also volatile, so every thread reads the newest number from main memory instead of an old copy in its own cache. That fixes the second problem from section 1.1.
The big word for "one step, all or nothing" is atomic. That is where the name comes from.
The everyday methods
These are the methods you will use most.
package com.mkyong;
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicMethods {
public static void main(String[] args) {
AtomicInteger n = new AtomicInteger(5); // start at 5
System.out.println(n.get()); // just look inside
System.out.println(n.incrementAndGet()); // add 1 first, then show
System.out.println(n.getAndIncrement()); // show first, then add 1
System.out.println(n.get()); // proof the add happened
System.out.println(n.addAndGet(10)); // add 10, then show
System.out.println(n.getAndSet(100)); // show old, then put 100 in
System.out.println(n.get()); // the new value
System.out.println(n.decrementAndGet()); // take away 1, then show
}
}
Output:
5
6
6
7
17
17
100
99
Read the name from left to right. incrementAndGet adds first; getAndIncrement shows first.
compareAndSet: change it only if it still matches
This is how you grab a seat and make sure nobody else grabbed it first.
package com.mkyong;
import java.util.concurrent.atomic.AtomicInteger;
public class SeatBooking {
public static void main(String[] args) {
AtomicInteger seat = new AtomicInteger(0); // 0 = empty, 1 = taken
// "If the seat is still 0, make it 1"
boolean first = seat.compareAndSet(0, 1);
// Someone else tries the same thing, but the seat is 1 now
boolean second = seat.compareAndSet(0, 1);
System.out.println("First person got the seat : " + first);
System.out.println("Second person got the seat : " + second);
System.out.println("Seat value : " + seat.get());
}
}
Output:
First person got the seat : true
Second person got the seat : false
Seat value : 1
The first person wins and gets true. The second person sees the value already changed, so nothing happens and they get false.
updateAndGet: your own rule
Pass a small lambda and the box follows your rule, still in one safe step.
package com.mkyong;
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicUpdate {
public static void main(String[] args) {
AtomicInteger n = new AtomicInteger(4);
// Your rule: square the number
System.out.println(n.updateAndGet(x -> x * x));
// Keep the bigger of the two: max(16, 4)
System.out.println(n.accumulateAndGet(4, Math::max));
// Keep the bigger of the two: max(16, 50)
System.out.println(n.accumulateAndGet(50, Math::max));
}
}
Output:
16
16
50
updateAndGet uses one number, the one already in the box. accumulateAndGet mixes the box value with a number you give it — handy for keeping a "highest score so far".
Real-life use case: order ID generator
Every online order needs its own number, and no two orders may share one. Here 1000 orders come in at the same time on virtual threads.
Without AtomicInteger
A plain int counter hands out the numbers.
package com.mkyong;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.Executors;
public class UnsafeOrderIdGenerator {
// A normal shared counter
static int orderId = 0;
public static void main(String[] args) {
// A thread-safe queue to keep every id you handed out
var ids = new ConcurrentLinkedQueue<Integer>();
// 1000 orders, all at once
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 1000; i++) {
executor.submit(() -> ids.add(++orderId)); // not safe!
}
} // wait for all 1000 orders
long unique = ids.stream().distinct().count();
System.out.println("Total ids : " + ids.size());
System.out.println("Unique ids : " + unique);
System.out.println("Duplicates : " + (ids.size() - unique));
System.out.println("Last id : " + orderId);
}
}
Output:
Total ids : 1000
Unique ids : 981
Duplicates : 19
Last id : 981
1000 customers, but only 981 different order numbers. Some customers share an id, and the shop’s own counter now says it sold 981 orders instead of 1000. Two people would open the same invoice.
With AtomicInteger
Change one word: int becomes AtomicInteger.
package com.mkyong;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
public class OrderIdGenerator {
// One shared ticket machine for the whole shop
static final AtomicInteger orderId = new AtomicInteger(0);
public static void main(String[] args) {
// A thread-safe queue to keep every id you handed out
var ids = new ConcurrentLinkedQueue<Integer>();
// Java 25 virtual threads: 1000 orders, all at once
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 1000; i++) {
executor.submit(() -> ids.add(orderId.incrementAndGet()));
}
} // wait for all 1000 orders
long unique = ids.stream().distinct().count();
System.out.println("Total ids : " + ids.size());
System.out.println("Unique ids : " + unique);
System.out.println("Duplicates : " + (ids.size() - unique));
System.out.println("Last id : " + orderId.get());
}
}
Output:
Total ids : 1000
Unique ids : 1000
Duplicates : 0
Last id : 1000
What AtomicInteger solved
Plain int |
AtomicInteger |
|
|---|---|---|
| Unique ids | 981 | 1000 |
| Duplicate ids | 19 | 0 |
| Counter total | wrong (981) | right (1000) |
| Same answer every run | no | yes |
Three problems went away at once: two orders can no longer grab the same id, no id is skipped, and the final total is trustworthy. Note the ids still arrive in a mixed order — AtomicInteger promises different numbers, not sorted ones.
Other everyday uses: counting page hits, counting failed logins, and picking the next server in a round-robin.
Common mistakes
Using get() and set() instead of one method
This is the number one mistake. get() is safe and set() is safe, but two safe steps are still two steps.
package com.mkyong;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
public class MistakeGetSet {
static AtomicInteger count = new AtomicInteger(0);
public static void main(String[] args) {
try (var executor = Executors.newFixedThreadPool(4)) {
for (int i = 0; i < 4; i++) {
executor.submit(() -> {
for (int j = 0; j < 10_000; j++) {
// WRONG: read, add, write - the same three steps as count++
count.set(count.get() + 1);
}
});
}
}
System.out.println("Expected : 40000");
System.out.println("Got : " + count.get());
}
}
Output:
Expected : 40000
Got : 26104
The AtomicInteger did not help here. You must do the whole job in one method call, like incrementAndGet().
Picking the wrong “get”
getAndIncrement() gives you the old number. Off-by-one bugs love this.
package com.mkyong;
import java.util.concurrent.atomic.AtomicInteger;
public class MistakeOffByOne {
public static void main(String[] args) {
AtomicInteger id = new AtomicInteger(0);
// WRONG if you want ids to start at 1
System.out.println("First id (getAndIncrement) : " + id.getAndIncrement());
AtomicInteger id2 = new AtomicInteger(0);
// RIGHT: add first, then hand out the number
System.out.println("First id (incrementAndGet) : " + id2.incrementAndGet());
}
}
Output:
First id (getAndIncrement) : 0
First id (incrementAndGet) : 1
Thinking it protects everything around it
An AtomicInteger only protects the number inside itself. It does not protect a list, a map, or any other object you touch nearby.
package com.mkyong;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
public class MistakeNotProtected {
public static void main(String[] args) {
AtomicInteger count = new AtomicInteger(0);
// WRONG: ArrayList is not thread-safe, the AtomicInteger cannot help it
List<Integer> list = new ArrayList<>();
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 1000; i++) {
executor.submit(() -> list.add(count.incrementAndGet()));
}
}
System.out.println("Counter says : " + count.get());
System.out.println("List size : " + list.size());
}
}
Output:
Counter says : 1000
List size : 987
The counter is perfect, the list is not. It can even throw an exception instead of printing. Swap ArrayList for a thread-safe ConcurrentLinkedQueue.
package com.mkyong;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
public class FixNotProtected {
public static void main(String[] args) {
AtomicInteger count = new AtomicInteger(0);
// RIGHT: a queue built for many threads
var queue = new ConcurrentLinkedQueue<Integer>();
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 1000; i++) {
executor.submit(() -> queue.add(count.incrementAndGet()));
}
}
System.out.println("Counter says : " + count.get());
System.out.println("Queue size : " + queue.size());
System.out.println("Unique items : " + queue.stream().distinct().count());
}
}
Output:
Counter says : 1000
Queue size : 1000
Unique items : 1000
Nothing lost. Remember the rule: pick a safe number and a safe collection. One does not fix the other.
Comparing with ==
AtomicInteger is an object, not a number, so == compares boxes and not values. It also has no useful equals().
package com.mkyong;
import java.util.concurrent.atomic.AtomicInteger;
public class MistakeEquals {
public static void main(String[] args) {
AtomicInteger a = new AtomicInteger(5);
AtomicInteger b = new AtomicInteger(5);
System.out.println(a == b); // two different boxes
System.out.println(a.equals(b)); // still two different boxes
System.out.println(a.get() == b.get()); // compare the numbers inside
}
}
Output:
false
false
true
Always call get() before you compare. For the same reason, never use an AtomicInteger as a HashMap key.
When to use int, when to use AtomicInteger
If only one thread touches the number, a plain int is the right answer. It is smaller and faster.
package com.mkyong;
public class WhenPlainInt {
public static void main(String[] args) {
// Only main thread touches this - a normal int is fine
int total = 0;
for (int price : new int[]{10, 20, 30}) {
total += price;
}
System.out.println("Total : " + total);
}
}
Output:
Total : 60
Here is the short guide.
| Situation | Use |
|---|---|
| A loop counter inside one method | int |
| A local sum, one thread only | int |
| A number that never changes | int (make it final) |
| A shared counter: page hits, active users, errors | AtomicInteger |
| Handing out ids or ticket numbers to many threads | AtomicInteger |
| Round-robin: pick the next server | AtomicInteger |
| Changing two or more fields together | synchronized or a lock |
The last row matters. If your rule needs two numbers at once, for example "move money from account A to account B", one AtomicInteger per account is not enough. You need a lock or a synchronized block around the whole job.
No comments yet. Be the first to leave a comment!