> ## Content Index
> Fetch the complete content index at: https://www.ggorantala.dev/llms.txt
> Use this file to discover other available public pages before exploring further.

# How to Use Atomic Classes in Java: AtomicLong, LongAdder
- URL: https://www.ggorantala.dev/how-to-use-atomic-classes-java/
- Published: 2026-06-15T05:06:00.000Z
- Updated: 2026-10-31T06:33:50.000Z
- Description: Build a parcel-scan counter on JDK 21 with AtomicLong, LongAdder, AtomicReference and AtomicBoolean, then fix a dock gate that lets three trucks into two bays.
- Author: Gopi Gorantala
- Tags: how-to-tutorials, java-concurrency, java-tutorials

## Here's the scan station when it's finished

Four scanner threads feed one parcel-scan station. Every scan gets a unique event ID, the totals add up, the heaviest parcel is tracked, the manifest closes exactly once, and a loading dock never lets in more trucks than it has bays. This is what the finished program printed on my machine:

```log
expected      4000000
plain int     3773215
AtomicInteger 4000000
scans         1000000
unique ids    1000000
heaviest (g)  999
totals        Totals[count=1000000, grams=549220000]
manifest closed by 1 thread(s)
broken dock   highest seen 3
fixed dock    highest seen 2

```

Look at the second line. `plain int` is what you get from `count++` on four threads: about 227,000 scans vanished. The fix is one class swap. The last two lines are the more interesting lesson, because the "broken" dock is built entirely from atomic calls and still lets three trucks into a two-bay dock.

If you want to learn how to use atomic classes in Java without memorising the whole `java.util.concurrent.atomic` package, this is the shape of it: five small classes, each for one job, and one rule about when they stop helping. I'll show the five, then the rule.

This one is for Java developers who already know what a thread is and have seen `synchronized`. There's no framework or database here, and no build tool either. You need a JDK and a terminal.

```log
 scanner-0 ─┐
 scanner-1 ─┤        ┌─ AtomicLong       → next event id
 scanner-2 ─┼─ ScanStats ─ LongAdder      → scan count
 scanner-3 ─┘        ├─ LongAccumulator  → heaviest parcel
                     ├─ AtomicReference  → (count, grams) pair
                     └─ AtomicBoolean    → close manifest once
 trucks ──────────  Dock ─ AtomicInteger + compareAndSet → bay limit

```

## Check your setup

| Tool       | Version I used                   | Check it with                                |
| ---------- | -------------------------------- | -------------------------------------------- |
| JDK        | OpenJDK 21.0.12.1 (Ubuntu build) | java -version                                |
| Build tool | none, plain javac                | javac -version                               |
| CPU cores  | 2                                | nproc (Linux/macOS), Task Manager on Windows |

Any JDK from 17 up compiles this. I only ran it on 21\. I didn't run it on 25, and I didn't run it on ARM, where contended atomics can behave differently from x86.

Core count matters for the demos. On a single core the broken counter and the broken dock often look fine, because only one thread runs at a time. If your numbers come out perfect on the broken versions, you're on one core or you have too few threads; raise `THREADS` in `Main`.

Need a JDK? SDKMAN works on Linux and macOS (`sdk install java 21.0.12-tem`, where the exact identifier depends on what `sdk list java` shows you). On Windows, install a Temurin 21 MSI.

I couldn't pull JUnit from Maven Central in the environment I built this in, so the tests here are plain Java with a tiny `check` helper. They run the same way everywhere, and I link to the JUnit version of this at the end.

## Create the parcel-scanner project

I'm using the command line as the primary path, because it removes every variable. The IDE routes are below.

```bash
mkdir parcel-scanner && cd parcel-scanner
mkdir -p src/com/example/parcels out

```

The project tree when you're done:

```log
parcel-scanner/
├── out/
└── src/com/example/parcels/
    ├── BrokenDock.java
    ├── Checks.java
    ├── Dock.java
    ├── Main.java
    ├── Reapply.java
    ├── ScanStats.java
    ├── Threads.java
    ├── TornPair.java
    └── Totals.java

```

### In IntelliJ IDEA

New Project → Java → pick JDK 21 → leave "Add sample code" off. Create the package `com.example.parcels` under `src` and add the classes there. To run, click the green arrow next to `main` in `Main` or `Checks`. I didn't click through this in this run; it's the standard Java project wizard.

### In Eclipse

File → New → Java Project, set the execution environment to JavaSE-21, and add the package under `src`. Run with Run As → Java Application. Also not clicked through this run.

### With Maven or Gradle

Nothing here needs a dependency, so a plain `pom.xml` with `maven.compiler.release` set to 21 works, as does `gradle init` with a Java application template. Put the files under `src/main/java/com/example/parcels` instead. I didn't run either build tool for this article.

## How to use atomic classes in Java: one job per class

Eight small steps. Each has a checkpoint.

### Step 1: Write a helper that starts all threads at the same moment

Goal: make races show up on demand. Threads that start one after another rarely collide, so we hold them at a gate and open it for everyone together.

`src/com/example/parcels/Threads.java`

```java
package com.example.parcels;

import java.util.concurrent.CountDownLatch;

final class Threads {
    private Threads() {}

    /** Runs the task on n threads that all start at the same moment, and waits for them. */
    static void race(int n, Runnable task) throws InterruptedException {
        CountDownLatch gate = new CountDownLatch(1);
        Thread[] ts = new Thread[n];
        for (int i = 0; i < n; i++) {
            ts[i] = new Thread(() -> {
                try { gate.await(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); return; }
                task.run();
            }, "scanner-" + i);
            ts[i].start();
        }
        gate.countDown();
        for (Thread t : ts) t.join();
    }
}

```

Why: every thread blocks on `gate.await()` until the main thread calls `countDown()`. That's all a start gate is.

Checkpoint: compile it.

```bash
javac -Xlint:all -d out src/com/example/parcels/Threads.java

```

No output means success.

### Step 2: Watch a plain counter lose scans, then swap in AtomicInteger

Goal: see the bug that atomics exist to fix.

`src/com/example/parcels/Main.java` (first version)

```java
package com.example.parcels;

import java.util.concurrent.atomic.AtomicInteger;

public class Main {
    static final int THREADS = 4;
    static final int SCANS_PER_THREAD = 1_000_000;

    static int plainCounter;   // not thread-safe, on purpose

    public static void main(String[] args) throws Exception {
        Threads.race(THREADS, () -> { for (int i = 0; i < SCANS_PER_THREAD; i++) plainCounter++; });
        AtomicInteger atomicCounter = new AtomicInteger();
        Threads.race(THREADS, () -> { for (int i = 0; i < SCANS_PER_THREAD; i++) atomicCounter.incrementAndGet(); });
        System.out.printf("expected      %d%nplain int     %d%nAtomicInteger %d%n",
                (long) THREADS * SCANS_PER_THREAD, plainCounter, atomicCounter.get());
    }
}

```

Why: `plainCounter++` is three steps (read, add one, write). Two threads can both read 41 and both write 42\. An atomic class does that read-modify-write as a single indivisible operation. `incrementAndGet()` returns the new value.

Checkpoint:

```bash
javac -Xlint:all -d out src/com/example/parcels/*.java
java -cp out com.example.parcels.Main

```

```log
expected      4000000
plain int     3773215
AtomicInteger 4000000

```

I ran the full program three times; the plain counter came out at 3,773,215, 1,227,112 and 2,945,176\. The atomic one was 4,000,000 every time. Your plain number will differ, and that's the point. If it says exactly 4,000,000, see "Check your setup" about cores.

### Step 3: Hand out event IDs with AtomicLong

Goal: give every scan a unique, increasing ID, using the value that `incrementAndGet()` hands back.

`src/com/example/parcels/ScanStats.java` (first version)

```java
package com.example.parcels;

import java.util.concurrent.atomic.AtomicLong;

public final class ScanStats {
    private final AtomicLong lastEventId = new AtomicLong(1000);          // need the new value back

    /** Records one scan and returns the event id it was given. */
    public long recordScan(int weightGrams) {
        return lastEventId.incrementAndGet();
    }
}

```

Why `AtomicLong` and not a counter class built for speed: you need the number back, so each caller gets its own ID. `LongAdder` can't do this, as the next step shows.

Checkpoint: compile again with the same `javac` command. IDs start at 1001, because `incrementAndGet` adds first and returns after. The checks later in the article assert it.

### Step 4: Count scans with LongAdder and track the heaviest parcel with LongAccumulator

Goal: add two things that many threads update and nobody needs back immediately.

`src/com/example/parcels/ScanStats.java` (second version)

```java
package com.example.parcels;

import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.LongAccumulator;
import java.util.concurrent.atomic.LongAdder;

public final class ScanStats {
    private final AtomicLong lastEventId = new AtomicLong(1000);
    private final LongAdder scans = new LongAdder();                      // hot counter, read rarely
    private final LongAccumulator heaviestGrams =
            new LongAccumulator(Long::max, Long.MIN_VALUE);               // running maximum

    public long recordScan(int weightGrams) {
        long id = lastEventId.incrementAndGet();
        scans.increment();
        heaviestGrams.accumulate(weightGrams);
        return id;
    }

    public long scanCount()     { return scans.sum(); }
    public long heaviestGrams() { return heaviestGrams.get(); }
}

```

Why: `LongAdder` spreads updates across several internal cells so threads stop fighting over one memory location, and `sum()` adds them up when you ask. It has no `incrementAndGet`, so it can't tell you "which number was I". `LongAccumulator` is the same idea with a function you choose; `Long::max` with an identity of `Long.MIN_VALUE` gives a running maximum. The function must be commutative and associative, because the order of combination isn't defined.

One caveat that bites people: `sum()` isn't a snapshot. If threads are still updating while you call it, the answer may or may not include their latest increments. For a dashboard that's fine. For "stop when we hit exactly 1,000," it isn't. The sibling article on [AtomicInteger, AtomicLong and LongAdder](https://ggorantala.dev/java-atomic-classes/?ref=ggorantala.dev) goes into the cells and the contention numbers.

Checkpoint: recompile; no output means it compiled.

### Step 5: Keep two numbers consistent with one AtomicReference

Goal: track scan count and total weight so that a reader can compute an average without ever seeing half an update.

First, the immutable pair:

`src/com/example/parcels/Totals.java`

```java
package com.example.parcels;

/** Count and weight that must always move together. Immutable, so it can sit behind one AtomicReference. */
public record Totals(long count, long grams) {
    public static final Totals EMPTY = new Totals(0, 0);

    public Totals plus(int weightGrams) {
        return new Totals(count + 1, grams + weightGrams);
    }
}

```

Then add it to `ScanStats`.

`src/com/example/parcels/ScanStats.java` (third version)

```java
package com.example.parcels;

import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.LongAccumulator;
import java.util.concurrent.atomic.LongAdder;

public final class ScanStats {
    private final AtomicLong lastEventId = new AtomicLong(1000);
    private final LongAdder scans = new LongAdder();
    private final LongAccumulator heaviestGrams =
            new LongAccumulator(Long::max, Long.MIN_VALUE);
    private final AtomicReference<Totals> totals = new AtomicReference<>(Totals.EMPTY);

    public long recordScan(int weightGrams) {
        long id = lastEventId.incrementAndGet();
        scans.increment();
        heaviestGrams.accumulate(weightGrams);
        totals.updateAndGet(t -> t.plus(weightGrams));   // pure function: safe to retry
        return id;
    }

    public long scanCount()      { return scans.sum(); }
    public long heaviestGrams()  { return heaviestGrams.get(); }
    public Totals totals()       { return totals.get(); }
}

```

Why: two separate atomics (`count` and `grams`) are each correct and still disagree for a moment between the two updates. A reader that grabs both can compute garbage. Putting both in one immutable record behind one `AtomicReference` means the swap is one step: you see the old pair or the new pair. Records are a natural fit; I covered why in [how to use Java records](https://ggorantala.dev/how-to-use-java-records/?ref=ggorantala.dev) and [how to create an immutable class in Java](https://ggorantala.dev/how-to-create-immutable-class-java/?ref=ggorantala.dev).

The lambda in `updateAndGet` has a rule: no side effects. It can run more than once when threads collide. I'll show you the number in the troubleshooting section.

Checkpoint: recompile.

### Step 6: Close the manifest exactly once with AtomicBoolean

Goal: when eight threads all notice "end of shift," exactly one of them should do the closing work.

Add these two things to `ScanStats`, a field and a method.

`src/com/example/parcels/ScanStats.java` (final version)

```java
package com.example.parcels;

import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.LongAccumulator;
import java.util.concurrent.atomic.LongAdder;

public final class ScanStats {
    private final AtomicLong lastEventId = new AtomicLong(1000);          // need the new value back
    private final LongAdder scans = new LongAdder();                      // hot counter, read rarely
    private final LongAccumulator heaviestGrams =
            new LongAccumulator(Long::max, Long.MIN_VALUE);               // running maximum
    private final AtomicReference<Totals> totals = new AtomicReference<>(Totals.EMPTY);
    private final AtomicBoolean manifestClosed = new AtomicBoolean();     // do-once flag

    /** Records one scan and returns the event id it was given. */
    public long recordScan(int weightGrams) {
        long id = lastEventId.incrementAndGet();
        scans.increment();
        heaviestGrams.accumulate(weightGrams);
        totals.updateAndGet(t -> t.plus(weightGrams));   // pure function: safe to retry
        return id;
    }

    public long scanCount()      { return scans.sum(); }
    public long heaviestGrams()  { return heaviestGrams.get(); }
    public Totals totals()       { return totals.get(); }

    /** Returns true for exactly one caller, however many threads race. */
    public boolean closeManifest() {
        return manifestClosed.compareAndSet(false, true);
    }
}

```

Why: `compareAndSet(false, true)` means "flip it to true only if it's still false, and tell me whether I was the one who flipped it." Exactly one caller sees `true`. Compare that with `if (!closed) { closed = true; doWork(); }`, where two threads can both pass the `if`.

Checkpoint: recompile. The station is finished; the dock is next.

### Step 7: Build the dock the obvious way, then fix it

Goal: limit a dock to N trucks without blocking. First the version everyone writes.

`src/com/example/parcels/BrokenDock.java`

```java
package com.example.parcels;

import java.util.concurrent.atomic.AtomicInteger;

/** The version everyone writes first. Every call is atomic; the pair is not. */
public final class BrokenDock {
    private final AtomicInteger trucks = new AtomicInteger();
    private final int cap;

    public BrokenDock(int cap) { this.cap = cap; }

    /** Returns the count after entering, or -1 if full. */
    public int tryEnter() {
        if (trucks.get() >= cap) return -1;      // look...
        return trucks.incrementAndGet();         // ...then leap
    }

    public void leave() { trucks.decrementAndGet(); }
}

```

`get()` is atomic. `incrementAndGet()` is atomic. Together they aren't: between the look and the leap, another truck can take the last bay. Now the fix.

`src/com/example/parcels/Dock.java`

```java
package com.example.parcels;

import java.util.concurrent.atomic.AtomicInteger;

/** Loading dock that admits at most {@code cap} trucks at once. Never blocks. */
public final class Dock {
    private final AtomicInteger trucks = new AtomicInteger();
    private final int cap;

    public Dock(int cap) { this.cap = cap; }

    public boolean tryEnter() {
        int current;
        do {
            current = trucks.get();
            if (current >= cap) return false;                    // check...
        } while (!trucks.compareAndSet(current, current + 1));   // ...bound to the write
        return true;
    }

    public void leave() { trucks.decrementAndGet(); }

    public int trucksInside() { return trucks.get(); }
}

```

Why: `compareAndSet(current, current + 1)` only writes if the value is still `current`. If another truck got in first, it returns false, you loop, re-read, and re-check. The check can't go stale, because it's welded to the write. `updateAndGet` with a cap inside the lambda works too, but you can't return "full" from it cleanly, so I prefer the explicit loop here.

If you want blocking, fairness or timeouts, use a `Semaphore` instead; I'd reach for [the Semaphore article](https://ggorantala.dev/java-semaphore/?ref=ggorantala.dev) first and this loop only when `tryEnter` must never block.

Checkpoint: recompile.

### Step 8: Put it all together in Main

Goal: run the whole station and both docks.

`src/com/example/parcels/Main.java` (final version)

```java
package com.example.parcels;

import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;

public class Main {
    static final int THREADS = 4;
    static final int SCANS_PER_THREAD = 1_000_000;

    static int plainCounter;   // not thread-safe, on purpose

    public static void main(String[] args) throws Exception {
        // 1. The counter everyone writes first
        Threads.race(THREADS, () -> { for (int i = 0; i < SCANS_PER_THREAD; i++) plainCounter++; });
        AtomicInteger atomicCounter = new AtomicInteger();
        Threads.race(THREADS, () -> { for (int i = 0; i < SCANS_PER_THREAD; i++) atomicCounter.incrementAndGet(); });
        System.out.printf("expected      %d%nplain int     %d%nAtomicInteger %d%n",
                (long) THREADS * SCANS_PER_THREAD, plainCounter, atomicCounter.get());

        // 2. The whole scan station
        ScanStats stats = new ScanStats();
        Set<Long> ids = ConcurrentHashMap.newKeySet();
        Threads.race(THREADS, () -> {
            for (int i = 0; i < 250_000; i++) ids.add(stats.recordScan(100 + (i % 900)));
        });
        System.out.println("scans         " + stats.scanCount());
        System.out.println("unique ids    " + ids.size());
        System.out.println("heaviest (g)  " + stats.heaviestGrams());
        System.out.println("totals        " + stats.totals());

        // 3. Close the manifest: eight threads try, one wins
        AtomicInteger winners = new AtomicInteger();
        Threads.race(8, () -> { if (stats.closeManifest()) winners.incrementAndGet(); });
        System.out.println("manifest closed by " + winners.get() + " thread(s)");

        // 4. The dock with a cap of 2
        System.out.println("broken dock   highest seen " + hammerBroken(2));
        System.out.println("fixed dock    highest seen " + hammerFixed(2));
    }

    static int hammerBroken(int cap) throws InterruptedException {
        BrokenDock dock = new BrokenDock(cap);
        AtomicInteger highest = new AtomicInteger();
        Threads.race(THREADS, () -> {
            for (int i = 0; i < 2_000_000; i++) {
                int now = dock.tryEnter();
                if (now < 0) continue;
                highest.accumulateAndGet(now, Math::max);
                dock.leave();
            }
        });
        return highest.get();
    }

    static int hammerFixed(int cap) throws InterruptedException {
        Dock dock = new Dock(cap);
        AtomicInteger highest = new AtomicInteger();
        Threads.race(THREADS, () -> {
            for (int i = 0; i < 2_000_000; i++) {
                if (!dock.tryEnter()) continue;
                highest.accumulateAndGet(dock.trucksInside(), Math::max);
                dock.leave();
            }
        });
        return highest.get();
    }
}

```

Why: `Threads.race` appears four times, and each one is "same code, many threads, same moment." The `ConcurrentHashMap.newKeySet()` is a thread-safe set; we use it to prove no two scans got the same ID (more on that class in [how to use ConcurrentHashMap](https://ggorantala.dev/how-to-use-concurrenthashmap-java/?ref=ggorantala.dev)).

Checkpoint: next section.

## Take it for a spin

```bash
javac -Xlint:all -d out src/com/example/parcels/*.java
java -cp out com.example.parcels.Main

```

```log
expected      4000000
plain int     3773215
AtomicInteger 4000000
scans         1000000
unique ids    1000000
heaviest (g)  999
totals        Totals[count=1000000, grams=549220000]
manifest closed by 1 thread(s)
broken dock   highest seen 3
fixed dock    highest seen 2

```

I ran it three times. The `plain int` line changed every run (3,773,215, 1,227,112, 2,945,176). Every other line was identical, including `broken dock highest seen 3` three times out of three on two cores with four threads. That one's a probability, not a promise. If yours says 2, add threads or more iterations.

Check the grams figure by hand, because it proves no update was lost. Each thread's weights cycle through 100 to 999\. A thread does 250,000 scans, which is 277 full cycles plus 700 more (weights 100 to 799). That's 277 × 494,550 + 314,650 = 137,305,000 grams per thread, and four threads make 549,220,000\. It matches.

"Heaviest 999" is the top of the `100 + (i % 900)` range, so that's correct too.

## Prove it with 19 checks

JUnit isn't available where I built this, so here's a self-contained checker. When you add JUnit, each `check` becomes an `@Test` with `assertThat`, and [the unit-testing article](https://ggorantala.dev/how-to-write-unit-tests-junit-5-java/?ref=ggorantala.dev) shows the setup.

`src/com/example/parcels/Checks.java`

```java
package com.example.parcels;

import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;

public class Checks {
    static int passed, failed;

    static void check(String name, boolean ok) {
        if (ok) passed++; else failed++;
        System.out.println((ok ? "PASS  " : "FAIL  ") + name);
    }

    public static void main(String[] args) throws Exception {
        // single-threaded behaviour
        ScanStats s = new ScanStats();
        check("first id is 1001", s.recordScan(300) == 1001);
        check("second id is 1002", s.recordScan(700) == 1002);
        check("scanCount is 2", s.scanCount() == 2);
        check("heaviest is 700", s.heaviestGrams() == 700);
        check("totals are 2 / 1000", s.totals().equals(new Totals(2, 1000)));
        check("empty heaviest is Long.MIN_VALUE", new ScanStats().heaviestGrams() == Long.MIN_VALUE);
        check("closeManifest true then false", new ScanStats().closeManifest() && !closeTwice());

        Dock d = new Dock(2);
        check("dock admits 1st", d.tryEnter());
        check("dock admits 2nd", d.tryEnter());
        check("dock refuses 3rd", !d.tryEnter());
        d.leave();
        check("dock admits after leave", d.tryEnter());

        // concurrent behaviour
        ScanStats shared = new ScanStats();
        Set<Long> ids = ConcurrentHashMap.newKeySet();
        Threads.race(4, () -> { for (int i = 0; i < 100_000; i++) ids.add(shared.recordScan(250)); });
        check("400k ids, all unique", ids.size() == 400_000);
        check("scanCount is 400k", shared.scanCount() == 400_000);
        check("totals count is 400k", shared.totals().count() == 400_000);
        check("totals grams is 100M", shared.totals().grams() == 100_000_000L);
        check("ids span 1001..401000",
                ids.stream().min(Long::compare).get() == 1001 && ids.stream().max(Long::compare).get() == 401_000);

        AtomicInteger winners = new AtomicInteger();
        ScanStats once = new ScanStats();
        Threads.race(16, () -> { if (once.closeManifest()) winners.incrementAndGet(); });
        check("exactly one thread closes the manifest", winners.get() == 1);

        // a reader must never see count and grams out of step
        ScanStats pair = new ScanStats();
        AtomicBoolean torn = new AtomicBoolean();
        AtomicBoolean done = new AtomicBoolean();
        Thread reader = new Thread(() -> {
            while (!done.get()) {
                Totals t = pair.totals();
                if (t.grams() != t.count() * 250) torn.set(true);
            }
        });
        reader.start();
        Threads.race(3, () -> { for (int i = 0; i < 200_000; i++) pair.recordScan(250); });
        done.set(true);
        reader.join();
        check("reader never sees count and grams out of step", !torn.get());

        // the dock never goes over cap, measured by every thread, many runs
        int worst = 0;
        for (int run = 0; run < 5; run++) {
            Dock dock = new Dock(2);
            AtomicInteger inside = new AtomicInteger();
            AtomicInteger highest = new AtomicInteger();
            Threads.race(4, () -> {
                for (int i = 0; i < 500_000; i++) {
                    if (!dock.tryEnter()) continue;
                    int now = inside.incrementAndGet();
                    highest.accumulateAndGet(now, Math::max);
                    inside.decrementAndGet();
                    dock.leave();
                }
            });
            worst = Math.max(worst, highest.get());
        }
        check("dock never admitted more than 2 across 5 runs (worst=" + worst + ")", worst <= 2);

        System.out.println(passed + " passed, " + failed + " failed");
        if (failed > 0) System.exit(1);
    }

    static boolean closeTwice() { ScanStats x = new ScanStats(); x.closeManifest(); return x.closeManifest(); }
}

```

```bash
javac -Xlint:all -d out src/com/example/parcels/*.java
java -cp out com.example.parcels.Checks

```

```log
PASS  first id is 1001
PASS  second id is 1002
PASS  scanCount is 2
PASS  heaviest is 700
PASS  totals are 2 / 1000
PASS  empty heaviest is Long.MIN_VALUE
PASS  closeManifest true then false
PASS  dock admits 1st
PASS  dock admits 2nd
PASS  dock refuses 3rd
PASS  dock admits after leave
PASS  400k ids, all unique
PASS  scanCount is 400k
PASS  totals count is 400k
PASS  totals grams is 100M
PASS  ids span 1001..401000
PASS  exactly one thread closes the manifest
PASS  reader never sees count and grams out of step
PASS  dock never admitted more than 2 across 5 runs (worst=2)
19 passed, 0 failed

```

A passing concurrency test is evidence, not proof. The reason these hold is the argument in the next section, not the fact that they went green once.

## Why one call is safe and two aren't

An atomic class wraps one variable and offers operations that happen as a single step: `incrementAndGet`, `compareAndSet`, `updateAndGet`. Under the hood, most of them rest on a CPU instruction called compare-and-swap, "set this to the new value only if it still holds the old one, and tell me if you did." No lock is taken, and no thread is put to sleep. Memory visibility comes with it: a successful atomic update has the effect of a `volatile` write, so what you did before it is visible to a thread that later reads the result. The [happens-before article](https://ggorantala.dev/java-memory-model-happens-before/?ref=ggorantala.dev) has the exact rules, and [the volatile article](https://ggorantala.dev/java-volatile-keyword/?ref=ggorantala.dev) explains why `volatile` alone doesn't fix a counter.

The rule the dock taught us: **atomicity covers exactly one call on exactly one variable.** `trucks.get()` followed by `trucks.incrementAndGet()` is two calls, and anything can happen between them. `AtomicLong count` plus `AtomicLong grams` is two variables, so the pair can be seen half-updated. The cures are the ones in the steps: one call that does the check and the write (`compareAndSet`, `updateAndGet`), or one variable that holds the whole state (`AtomicReference` to an immutable record).

`updateAndGet(f)` is a loop around compare-and-set. If another thread changed the value while `f` was running, the swap fails and `f` runs again on the new value. That's why `f` has to be free of side effects. I measured how often it re-runs: four threads, a million updates each, on two cores:

```log
updates=4000000 lambdaCalls=5486742 extra=1486742
updates=4000000 lambdaCalls=5464250 extra=1464250
updates=4000000 lambdaCalls=5802298 extra=1802298

```

About 37% to 45% of the calls were retries in this worst-case loop. A log line or an ID allocation inside that lambda would have fired thousands of times for results nobody kept. Real code collides far less than this loop, which is why it can run fine for months and then misbehave on a busy day. The retry mechanics and the `LongAdder` internals are in [the atomic classes explainer](https://ggorantala.dev/java-atomic-classes/?ref=ggorantala.dev); `AtomicReference` has its own trap, ABA, in [this article](https://ggorantala.dev/java-atomicreference/?ref=ggorantala.dev); and the broader family of bug is in [the race condition article](https://ggorantala.dev/java-race-condition/?ref=ggorantala.dev).

## Errors I hit, and the fixes

**Two atomics that need to agree.** I wanted to watch the torn read happen instead of asserting it. Count and grams as separate `AtomicLong`s, a reader thread comparing them while three threads write:

```java
// TornPair.java (body of main, abbreviated)
count.incrementAndGet();
grams.addAndGet(250);
// reader: long c = count.get(); long g = grams.get(); if (g != c * 250) torn++;

```

```log
inconsistent reads: 643767
inconsistent reads: 1082586
inconsistent reads: 918581

```

Hundreds of thousands of reads saw a count that didn't match the weight. The reader loop is tight, so the number is inflated, but even one is a bug. Fix: Step 5, one `AtomicReference` to a `Totals` record. The check "reader never sees count and grams out of step" passes against that design.

**Compile error when you mutate a local in the lambda.** I tried to count calls with a local `long total` inside `updateAndGet`:

```log
Bad.java:2: error: local variables referenced from a lambda expression must be final or effectively final

```

Cause: lambdas can read locals but not change them. Fix: keep the count in an atomic (a `LongAdder` for a counter), not in a local. And remember the lambda shouldn't have side effects at all; a counter in there will over-count, as the table above shows.

**`cannot find symbol` for LongAdder.**

```log
Bad2.java:1: error: cannot find symbol
  symbol:   class LongAdder

```

Cause: it lives in `java.util.concurrent.atomic`, not `java.util.concurrent`. A wildcard import of `java.util.concurrent.*` won't pick it up, and neither will `java.util.concurrent.atomic.AtomicLong` alone. IDEs fix this with Alt+Enter (IntelliJ IDEA) or Ctrl+1 (Eclipse); I didn't run those here.

**The broken dock looks fine.** If it prints `highest seen 2` on your machine, you're on one core, a very light machine, or too few threads. Raise `THREADS` and the loop count. Don't take it as proof the code is right. Review any `get()` followed by an update with suspicion.

**Plain int counter prints exactly 4,000,000.** Same cause as above: too few cores or threads. Run it a few times before you trust it.

## Variations worth knowing

- **Gradle or Maven**: copy the sources under `src/main/java/...`. No dependencies needed. I didn't run either.
- **When not to use atomics**: if two fields must change together and the logic has branches, a lock or an `AtomicReference` to an immutable record is easier to reason about. I'd use `synchronized` the moment the update path grows past one comparison. See [synchronized vs ReentrantLock](https://ggorantala.dev/how-to-use-reentrantlock-synchronized-java/?ref=ggorantala.dev).
- **Fields in objects you can't change**: `AtomicIntegerFieldUpdater` and friends give atomic operations on a `volatile` field without a wrapper. Setup is fiddly; skip unless you're allocating millions of objects.
- `**weakCompareAndSet**`: deprecated since JDK 9 in favour of `weakCompareAndSetPlain` and `weakCompareAndSetVolatile`. If you have a hand-written `do { ... } while (!cas)` loop in a newer codebase, it's usually habit, not need.
- **Pipelines**: when the work is handing items between threads rather than counting, [a BlockingQueue](https://ggorantala.dev/how-to-use-blockingqueue-producer-consumer-java/?ref=ggorantala.dev) is the better building block.
- **Version note**: the `updateAndGet` family and `LongAdder` appeared in Java 8; the basic `AtomicInteger`/`AtomicReference` have been there since Java 5\. Records need Java 16; on Java 11 write `Totals` as a final class with `equals`, `hashCode` and `toString`.

## What you did

- Showed a plain `int` counter losing about 6% to 69% of its increments across three runs, and fixed it with `AtomicInteger`.
- Used `AtomicLong` for IDs (you need the new value), `LongAdder` for a hot counter you read rarely, `LongAccumulator` for a maximum, `AtomicReference` plus an immutable record for a pair that moves together, and `AtomicBoolean.compareAndSet` for a do-once flag.
- Wrote a dock gate whose check and write are one `compareAndSet` loop, after watching the obvious version let three trucks into two bays.
- Remembered the rules: atomicity covers one call on one variable, and the function in `updateAndGet` can run more than once.

The build, start to green checks, took me one sitting. Writing the classes was quick; the time went into getting the broken versions to fail reliably, which needed the start gate and four threads on two cores.

## Atomic class questions

**What is the difference between AtomicInteger and volatile int?**  
`volatile` makes reads and writes visible across threads, but `count++` is still read, add, write, so updates get lost. `AtomicInteger.incrementAndGet()` does the whole thing as one step. In my run, 4 threads times 1,000,000 plain increments lost between roughly 6% and 69%.

**When should I use LongAdder instead of AtomicLong?**  
When many threads update a total and you read it rarely, like request or scan counts. Use `AtomicLong` when you need the updated value back (IDs) or need `compareAndSet`. `LongAdder.sum()` isn't an atomic snapshot.

**Is AtomicInteger thread-safe for check-then-update?**  
Each method is. A `get()` followed by an update isn't, as the broken dock shows. Use `compareAndSet` in a loop, or `updateAndGet` when a failed check can leave the value unchanged.

**Does updateAndGet use a lock?**  
No. It retries a compare-and-set, so the function may run several times and must have no side effects. I measured up to 45% extra runs in a tight four-thread loop.

## References

- `java.util.concurrent.atomic` package summary, Java SE 21: [https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/concurrent/atomic/package-summary.html](https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/concurrent/atomic/package-summary.html?ref=ggorantala.dev)
- `AtomicInteger`, `AtomicLong`, `AtomicReference`, `AtomicBoolean`, `LongAdder`, `LongAccumulator` Javadoc, Java SE 21 (same package, linked from the summary above)
- JEP 395, Records: [https://openjdk.org/jeps/395](https://openjdk.org/jeps/395?ref=ggorantala.dev)