JavaRush/Courses/Kotlin SELF/AtomicInteger and AtomicReference: atomic operations and ...

AtomicInteger and AtomicReference: atomic operations and applicability limits

Open

1. Introduction

If you’ve only just become friends with Mutex, a logical question may arise: “Why do I need yet another synchronization tool? I already know how to lock everything up.” And indeed, Mutex is like a big, beautiful, reliable lock on a door. But sometimes you don’t need to lock the whole apartment—you just need to… prevent someone from pressing a single button at the same time. In such cases, dragging in a “door lock” can be overkill.

An atomic type is a container that provides operations like “read → modify → write” as one indivisible operation (in terms of concurrent access). That is, other threads/coroutines won’t see the “middle” of an update.

It’s important to keep two words clearly in mind:

  • atomicity — an operation either happens completely or doesn’t happen (and not “halfway”);
  • invariant — an integrity rule that must not be violated.

If your invariant can be expressed as a single number (“count of processed tasks”), an atomic is ideal. If the invariant includes multiple values (“balance never goes negative” plus “operation log is consistent”), an atomic may not be enough—and that’s normal.

2. AtomicInteger: a thread-safe counter without Mutex

Let’s start with the nicest part: AtomicInteger solves the classic “counter across multiple coroutines” problem as simply as you wish counter++ would work… but it doesn’t.

It is imported from a Java package:

import java.util.concurrent.atomic.AtomicInteger

Mini example: 1000 coroutines increment a counter

This example is similar to the “race” from the lecture about race condition, except now we’ll replace var counter = 0 with an atomic.

import java.util.concurrent.atomic.AtomicInteger
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking

fun main() = runBlocking {
    val counter = AtomicInteger(0)

    val jobs = List(1_000) {
        launch(Dispatchers.Default) {
            counter.incrementAndGet()
        }
    }

    jobs.forEach { it.join() }
    println(counter.get()) // 1000
}

Here incrementAndGet() is an atomic operation. No one will “lose” an increment.

Note the style: we do not write counter++. Atomics have a different API: get(), set(...), incrementAndGet(), and friends.

How incrementAndGet() differs from get() + set()

It’s very important to understand the boundary: what’s atomic is a specific atomic operation, not your three lines next to it.

Compare:

import java.util.concurrent.atomic.AtomicInteger

fun main() {
    val c = AtomicInteger(0)

    c.incrementAndGet()
    println(c.get()) // 1
}

and this (usually you should not do this):

import java.util.concurrent.atomic.AtomicInteger

fun main() {
    val c = AtomicInteger(0)

    val current = c.get()
    c.set(current + 1)

    println(c.get()) // 1 (in a single thread it's ok, but under contention this is already "composite logic")
}

In multithreading/multi-coroutine scenarios, the second variant turns back into “read → compute → write”, and between the read and the write another participant may manage to perform its own update.

3. When an atomic is not enough: atomic value vs. atomic logic

In this section we approach the moment that most often breaks beginners’ brains: “But I’m using an atomic—why is it still sometimes wrong?”

Because an atomic makes updating a single value atomic, but it is not obliged to make your business logic atomic if it consists of multiple steps.

To feel this, let’s model a small part of our learning app.

Imagine we have an “event handler”, and we want to:
1) increase the count of processed events,
2) but not exceed a limit (for example, 100).

Naive check-then-act with an atomic: it’s still a race

import java.util.concurrent.atomic.AtomicInteger
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking

fun main() = runBlocking {
    val processed = AtomicInteger(99)
    val limit = 100

    val jobs = List(2) {
        launch(Dispatchers.Default) {
            if (processed.get() < limit) {
                processed.incrementAndGet()
            }
        }
    }

    jobs.forEach { it.join() }
    println(processed.get()) // can become 101
}

Yes, you heard that right: it can become 101. Why? Because the condition processed.get() < limit and the increment are already “checked → acted”. Each step is “safe” on its own, but together they are not glued into one atomic block.

This is the classic check‑then‑act pattern we’ve already seen, except now the “race participants” are not a var, but a sequence of operations.

CAS loop: make “checked → updated” atomic

To fix such logic without Mutex, a CAS approach is used: Compare-And-Set. It looks like: “set a new value, but only if the current value is the one I expect”.

AtomicInteger has the method compareAndSet(expected, new).

Conceptually, you can represent it like this:

flowchart TD
    A[read current value] --> B[compute new one]
    B --> C{compareAndSet expected?}
    C -- yes --> D[success: value replaced]
    C -- no --> A[repeat: someone changed it]

And here is working code: short, but very illustrative.

import java.util.concurrent.atomic.AtomicInteger

fun tryIncrementUpToLimit(counter: AtomicInteger, limit: Int): Boolean {
    while (true) {
        val current = counter.get()
        if (current >= limit) return false

        val next = current + 1
        if (counter.compareAndSet(current, next)) return true
        // otherwise someone updated the value — retry
    }
}

And a small check:

import java.util.concurrent.atomic.AtomicInteger

fun main() {
    val c = AtomicInteger(0)
    println(tryIncrementUpToLimit(c, 1)) // true
    println(tryIncrementUpToLimit(c, 1)) // false
    println(c.get())                     // 1
}

A CAS loop is “optimistic” synchronization: we assume no one will interfere, try to update, and if someone interfered—retry.

4. AtomicReference<T>: atomically swap a reference to state

After AtomicInteger it’s natural to ask: “OK, what if I need to atomically store not a number, but something more complex?”

That’s where AtomicReference<T> comes in—a container for a reference.

Import:

import java.util.concurrent.atomic.AtomicReference

Mini example: atomic state transition

Let’s make a mini “state machine”:

import java.util.concurrent.atomic.AtomicReference

fun main() {
    val state = AtomicReference("IDLE")

    val ok = state.compareAndSet("IDLE", "RUNNING")
    println("changed=$ok, state=${state.get()}") // changed=true, state=RUNNING
}

The idea is simple: if you want the transition "IDLE" → "RUNNING" to happen strictly once, CAS gives a very clear tool.

Application state as a data class

Now an example closer to reality. Suppose our learning app has processing stats:

  • processed — how many events were processed,
  • lastError — the last error (if any).

We can store this as an immutable object and replace it as a whole.

import java.util.concurrent.atomic.AtomicReference

data class ProcessorState(
    val processed: Int,
    val lastError: String?
)

fun main() {
    val ref = AtomicReference(ProcessorState(processed = 0, lastError = null))

    ref.set(ProcessorState(processed = 10, lastError = "Timeout"))
    println(ref.get()) // ProcessorState(processed=10, lastError=Timeout)
}

The advantage of this approach is that you can atomically replace the entire state at once: with a single set(...) or a CAS operation. This can sometimes simplify your life a lot compared to “two vars next to each other”.

5. CAS on AtomicReference: update state without locks

Now comes the part where many people first feel the “power” of AtomicReference: we can do atomic updates of complex state if we change it by copying, not by “mutating it internally”.

And this is where copy() on a data class fits perfectly: we take the old state, build a new one, and swap the reference.

“Write an error” function via a CAS loop

import java.util.concurrent.atomic.AtomicReference

data class ProcessorState(val processed: Int, val lastError: String?)

fun setError(ref: AtomicReference<ProcessorState>, message: String) {
    while (true) {
        val current = ref.get()
        val next = current.copy(lastError = message)
        if (ref.compareAndSet(current, next)) return
    }
}

Check:

import java.util.concurrent.atomic.AtomicReference

data class ProcessorState(val processed: Int, val lastError: String?)

fun main() {
    val ref = AtomicReference(ProcessorState(0, null))

    setError(ref, "Disk is full")
    println(ref.get()) // ProcessorState(processed=0, lastError=Disk is full)
}

Updating a counter inside the state

Similarly, you can “increment” the processed field without resorting to AtomicInteger:

import java.util.concurrent.atomic.AtomicReference

data class ProcessorState(val processed: Int, val lastError: String?)

fun incProcessed(ref: AtomicReference<ProcessorState>) {
    while (true) {
        val current = ref.get()
        val next = current.copy(processed = current.processed + 1)
        if (ref.compareAndSet(current, next)) return
    }
}

This works correctly, but remember: this strategy creates new state objects (i.e., allocations). Sometimes that’s a fair price for simplicity and integrity; sometimes it’s not. Our goal here is not to “optimize down to the last byte”, but to learn to choose the right tool for the task.

6. An important trap: AtomicReference does not make an object thread-safe

Now comes the moment that regularly breaks projects (and morale). AtomicReference protects replacing the reference, but it doesn’t turn the object that the reference points to into something magically thread-safe.

That is, you can hurt yourself badly like this:

import java.util.concurrent.atomic.AtomicReference

data class MutableBox(var x: Int)

fun main() {
    val ref = AtomicReference(MutableBox(0))

    val box = ref.get()
    box.x++ // this operation did not become thread-safe "because of the atomic"

    println(ref.get().x) // 1
}

In a single thread everything is “kind of fine”. But if multiple coroutines do ref.get().x++, you’ll get a race again—just not on ref, but on the x field.

So the practical rule sounds like this: AtomicReference works well when you store immutable objects inside and replace them entirely. For mutable objects, an atomic is only a “safe reference holder”, nothing more.

7. Applicability limits of atomics

To avoid turning synchronization into a religion (“solve everything with atomics!”), it’s useful to fix a simple selection map.

Task Usually better Why
Event counter, “how many processed” metric
AtomicInteger
Cheap, simple, atomic increment
Single state flag “started/not started” AtomicReference or AtomicBoolean CAS lets you enforce “only one start”
Invariant across multiple fields (“A consistent with B”) Mutex or a different design Need atomicity of a group of operations, not one value
You want to store an object and mutate its fields “inside” Be careful: an atomic won’t save you Internal mutation remains shared mutable state
“Checked a condition → performed an action” CAS loop or Mutex This is check‑then‑act; you need a single atomic block

Separately, note: in the Kotlin ecosystem there are other approaches to atomicity (for example, separate libraries and a discussed common API for multiplatform). But within Kotlin/JVM we stick to the practical path: java.util.concurrent.atomic.*, because it’s widely used and integrates well with coroutines on the JVM.

8. How to use atomics in an app: counters and status

Let’s do a small “practical” piece that you can mentally continue from the lectures about races and Mutex. Imagine we have a simple task processor, and we want to track two things:

  • processedCount — how many tasks were processed,
  • status — a status string: "IDLE", "RUNNING", "STOPPED".

“Before” version: it would have been var

We won’t write the full “bad” code (we already saw it in races), but the idea is: two vars and a bunch of coroutines around them—an almost guaranteed race.

“After” version: AtomicInteger + AtomicReference

import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.atomic.AtomicReference

class ProcessorStats {
    val processedCount = AtomicInteger(0)
    val status = AtomicReference("IDLE")
}

Now let’s create functions used by our “engine”:

import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.atomic.AtomicReference

fun start(status: AtomicReference<String>): Boolean {
    return status.compareAndSet("IDLE", "RUNNING")
}

fun markProcessed(counter: AtomicInteger) {
    counter.incrementAndGet()
}

And a small coroutine-based “simulator”:

import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.atomic.AtomicReference
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking

fun main() = runBlocking {
    val processed = AtomicInteger(0)
    val status = AtomicReference("IDLE")

    println(start(status)) // true
    println(status.get())  // RUNNING

    val jobs = List(1_000) {
        launch(Dispatchers.Default) { markProcessed(processed) }
    }
    jobs.forEach { it.join() }

    println(processed.get()) // 1000
}

This yields a simple rule: “change status with CAS; update the counter with an atomic increment”. No Mutex, because our invariants here really are “one value = one atomic operation”.

9. Common mistakes when working with AtomicInteger and AtomicReference

Mistake №1: assuming that “if I use an atomic, races no longer happen”.
An atomic protects specific operations on a specific value, but if you build logic out of multiple steps (especially check‑then‑act), the race comes back at the algorithm level. In such places, either use a CAS loop or honestly take a Mutex.

Mistake №2: writing get() → “think” → set() and expecting atomicity.
This pattern looks decent, but in a concurrent environment between get() and set() someone else may update the value. If you need an update “based on the current value”, use incrementAndGet(), addAndGet(), compareAndSet(), or a CAS loop.

Mistake №3: using AtomicReference and mutating the object inside.
AtomicReference guarantees atomicity of swapping the reference, but it does not make the object’s fields thread-safe. If there is a MutableList inside, var fields, and you mutate them directly, you’re back in shared mutable state. The reliable style with AtomicReference is to store immutable state and replace it entirely via copy() + CAS.

Mistake №4: trying to “glue” an invariant across multiple values with atomics.
If you have two atomics (AtomicInteger and AtomicReference) and you update them separately, the invariant “they are always consistent” won’t appear by itself. Here you either need a single state object (one AtomicReference<State>), or a Mutex, or a redesign so consistency is ensured by program structure rather than hope.

Mistake №5: writing a CAS loop with no exit or without understanding that it may retry.
A CAS loop is not a “magic replacement for a lock”, but an honest “retry if it didn’t work” strategy. If you forget to handle exit conditions (for example, a limit) or you do heavy work inside, you can get unpleasant effects: extra retries, CPU load, and odd behavior. A CAS loop should be short, and computing next should be fast and deterministic.

Mistake №6: trying to optimize too early and replacing Mutex with atomics “just because it’s faster”.
Yes, atomics are often lighter than locks. But the cost of a mistake in concurrent code is usually higher than the cost of a microscopic optimization. If you can’t clearly express the invariant as “one atomic value and one atomic operation”, write it with Mutex first, and only then think about whether it can be simplified down to atomics.

1
Task
Kotlin SELF,  55 level,  2 lecture
Locked
Atomic toggle
Atomic toggle
1
Task
Kotlin SELF,  55 level,  2 lecture
Locked
A Thousand Clicks
A Thousand Clicks
1
Task
Kotlin SELF,  55 level,  2 lecture
Locked
Attempt limit
Attempt limit
1
Task
Kotlin SELF,  55 level,  2 lecture
Locked
Single start
Single start
Комментарии
  • популярные
  • новые
  • старые
Для того, чтобы оставить комментарий Вы должны sign in
This page has no comments yet