Java AtomicInteger Example

Java AtomicInteger

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

Four threads each add 1 ten thousand times, so you should get 40000.

UnsafeCounter.java

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:

Terminal

Expected : 40000
Got      : 10396

Output, second run:

Terminal

Expected : 40000
Got      : 12569

Output, tenth run:

Terminal

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.

Flow

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.

Flow

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.

SafeCounter.java

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:

Terminal

Expected : 40000
Got      : 40000

Output, second run:

Terminal

Expected : 40000
Got      : 40000

Output, tenth run:

Terminal

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:

Flow

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.

AtomicMethods.java

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:

Terminal

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.

SeatBooking.java

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:

Terminal

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.

AtomicUpdate.java

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:

Terminal

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.

UnsafeOrderIdGenerator.java

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:

Terminal

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.

OrderIdGenerator.java

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:

Terminal

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.

MistakeGetSet.java

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:

Terminal

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.

MistakeOffByOne.java

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:

Terminal

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.

MistakeNotProtected.java

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:

Terminal

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.

FixNotProtected.java

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:

Terminal

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().

MistakeEquals.java

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:

Terminal

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.

WhenPlainInt.java

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:

Terminal

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.

References

No comments yet. Be the first to leave a comment!

Leave a Comment

Your email address will not be published. Required fields are marked *